feat(auth)!: codes by mail no longer sign in
ADR 0017, step 3 (D4: nobody signs in with email codes yet, so the password replaces them outright). `emailCode.verifyCode`, `emailSignIn.signIn` and the switch token go, with the register's `verifySignInCode` and `completeEmailSwitch` and their operations between services. `emailCode.sendCode` stays, now without a session (signing up and getting back in have none), as what proves an address for a password's setup, a sign-up, a setup never finished and the resets; `accountRecovery.sendCode`, which did that for the resets, goes. The `switchChallengeId` column stays: Serverpod's challenges still link a completion token, and older servers read it during a deploy. The apps: halkyon_app's email sign-in (its bloc, sheet, gateway calls and the start screen's Continue with email) goes; Learn's profile and Todo's account sheet show `AccountSecuritySection` (a password set up by someone anonymous or signing in with GitHub, whose end it names; the authenticators; changing the password; new recovery codes and phrase), which reads anew after every change the repository makes. BREAKING CHANGE: Learn's `minimumAppBuild` is 22: builds up to 21 sign in with the endpoints that went. Todo was never released and stays at build 1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
735dfa892a
commit
ae57cbe864
16 changed files with 429 additions and 3835 deletions
|
|
@ -3,23 +3,21 @@ import 'dart:convert';
|
|||
import 'package:clock/clock.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:halkyon_base_server/src/auth/code_mail.dart';
|
||||
import 'package:halkyon_base_server/src/auth/identified.dart';
|
||||
import 'package:halkyon_base_server/src/common/network.dart';
|
||||
import 'package:halkyon_base_server/src/common/short_code.dart';
|
||||
import 'package:halkyon_base_server/src/generated/protocol.dart';
|
||||
import 'package:halkyon_base_server/src/halkyon_base.dart';
|
||||
import 'package:halkyon_base_server/src/password/passwords.dart';
|
||||
import 'package:halkyon_base_server/src/register/people.dart';
|
||||
import 'package:serverpod/serverpod.dart';
|
||||
import 'package:serverpod_auth_idp_server/core.dart';
|
||||
|
||||
// Signing in with an email address, by a code mailed to it each time
|
||||
// (Halkyon Learn's ADR 0010). The instance keeps only a keyed hash of the
|
||||
// address: it can find the person and tell whether an address is free from
|
||||
// what someone types, and it mails only an address someone has just typed.
|
||||
// So the login is our own, on Serverpod's helpers (its code challenges and
|
||||
// rate limiter): Serverpod's email sign-in keeps the address itself, in three
|
||||
// tables, and passwords.
|
||||
// An email address, and the codes mailed to it (Halkyon Learn's ADR 0010).
|
||||
// The instance keeps only a keyed hash of the address: it can find the
|
||||
// person and tell whether an address is free from what someone types, and
|
||||
// it mails only an address someone has just typed. So this is our own, on
|
||||
// Serverpod's helpers (its code challenges and rate limiter): Serverpod's
|
||||
// email sign-in keeps the address itself, in three tables. Since ADR 0017 a
|
||||
// code signs nobody in by itself: it proves the address for a password's
|
||||
// setup, a sign-up, a setup never finished and the resets.
|
||||
|
||||
/// Serverpod's name for email sign-in on session rows.
|
||||
const emailMethod = 'email';
|
||||
|
|
@ -193,105 +191,12 @@ Future<UuidValue> sendSignInCode(
|
|||
});
|
||||
}
|
||||
|
||||
/// Accepts [code] for [request] from the person [authUserId]: an address
|
||||
/// nobody has becomes theirs, in one transaction (they become `identified`,
|
||||
/// on their sessions too, and stop being anonymous, keeping everything they
|
||||
/// have; a member's person becomes an account of the register under their
|
||||
/// ID, ADR 0016); their own changes nothing; another person's changes nothing
|
||||
/// here and gives a token that signs in as that person
|
||||
/// ([completeEmailSwitch]), unless that person signs in with a password
|
||||
/// ([signsInWithPassword], ADR 0017).
|
||||
///
|
||||
/// Refused for a wrong code, too many tries, a code no longer valid, a
|
||||
/// second address for someone who has one, or another person's address who
|
||||
/// signs in with a password ([EmailSignInRefusal.usePassword])
|
||||
/// ([EmailSignInRefusedException]); nothing changes then.
|
||||
Future<EmailCodeAccepted> acceptSignInCode(
|
||||
Session session, {
|
||||
required UuidValue authUserId,
|
||||
required UuidValue request,
|
||||
required String code,
|
||||
}) async {
|
||||
final pod = session.serverpod;
|
||||
if (!emailSignInAvailable(pod)) {
|
||||
throw EmailSignInRefusedException(reason: EmailSignInRefusal.unavailable);
|
||||
}
|
||||
return await session.db.transaction((transaction) async {
|
||||
final switchToken = await _refusingAsEmail(
|
||||
() => _challengesOf(pod).verifyChallenge(
|
||||
session,
|
||||
requestId: request,
|
||||
verificationCode: normalizeShortCode(code),
|
||||
transaction: transaction,
|
||||
),
|
||||
);
|
||||
final accepted = (await _requestById(
|
||||
session,
|
||||
request,
|
||||
transaction: transaction,
|
||||
))!;
|
||||
// SELECT * FROM "email_identity"
|
||||
// WHERE "addressHash" = $1 OR "authUserId" = $2
|
||||
final identities = await EmailIdentity.db.find(
|
||||
session,
|
||||
where: (t) =>
|
||||
t.addressHash.equals(accepted.addressHash) |
|
||||
t.authUserId.equals(authUserId),
|
||||
transaction: transaction,
|
||||
);
|
||||
final owner = identities
|
||||
.where((identity) => identity.addressHash == accepted.addressHash)
|
||||
.firstOrNull;
|
||||
if (owner != null && owner.authUserId != authUserId) {
|
||||
if (await signsInWithPassword(
|
||||
session,
|
||||
owner.authUserId,
|
||||
transaction: transaction,
|
||||
)) {
|
||||
throw EmailSignInRefusedException(
|
||||
reason: EmailSignInRefusal.usePassword,
|
||||
);
|
||||
}
|
||||
// Kept until the token is used, or its time is up.
|
||||
return EmailCodeAccepted(switchToken: switchToken);
|
||||
}
|
||||
if (owner == null) {
|
||||
if (identities.isNotEmpty) {
|
||||
throw EmailSignInRefusedException(
|
||||
reason: EmailSignInRefusal.otherAddressLinked,
|
||||
);
|
||||
}
|
||||
await ensurePerson(session, authUserId, transaction: transaction);
|
||||
// INSERT INTO "email_identity" ("authUserId", "addressHash",
|
||||
// "createdAt") VALUES ($1, $2, $3)
|
||||
await EmailIdentity.db.insertRow(
|
||||
session,
|
||||
EmailIdentity(
|
||||
authUserId: authUserId,
|
||||
addressHash: accepted.addressHash,
|
||||
createdAt: clock.now(),
|
||||
),
|
||||
transaction: transaction,
|
||||
);
|
||||
await grantIdentified(session, authUserId, transaction: transaction);
|
||||
// DELETE FROM "serverpod_auth_idp_anonymous_account"
|
||||
// WHERE "authUserId" = $1
|
||||
await AnonymousAccount.db.deleteWhere(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(authUserId),
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
await _forget(session, [accepted], transaction: transaction);
|
||||
return EmailCodeAccepted();
|
||||
});
|
||||
}
|
||||
|
||||
/// The keyed hash of the address [code] proves for [request], once: the
|
||||
/// request goes as it is accepted. For what an address proves without
|
||||
/// becoming anyone's way in: resets of an account (ADR 0017), whose register
|
||||
/// compares the hash with the account's. Refusals as [acceptSignInCode]'s
|
||||
/// ([EmailSignInRefusedException]).
|
||||
/// request goes as it is accepted. A code by mail signs nobody in (ADR
|
||||
/// 0017): it proves an address for a password's setup, a sign-up, a setup
|
||||
/// never finished and the resets, whose register compares the hash with
|
||||
/// the account's. Refused for a wrong code, too many tries, or a code no
|
||||
/// longer valid ([EmailSignInRefusedException]).
|
||||
Future<String> proveAddressCode(
|
||||
Session session, {
|
||||
required UuidValue request,
|
||||
|
|
@ -320,52 +225,6 @@ Future<String> proveAddressCode(
|
|||
});
|
||||
}
|
||||
|
||||
/// The person whose address a code proved, from the token
|
||||
/// [acceptSignInCode] gave; once only. A session for them is the caller's to
|
||||
/// issue (`issueSession`): this server's own, or a member's (ADR 0016).
|
||||
/// Refused once the token was used or its time is up, or for a token it
|
||||
/// never gave ([EmailSignInRefusal.codeExpired]), and for a person who signs
|
||||
/// in with a password since ([EmailSignInRefusal.usePassword]).
|
||||
Future<UuidValue> completeEmailSwitch(
|
||||
Session session, {
|
||||
required String switchToken,
|
||||
}) async {
|
||||
final pod = session.serverpod;
|
||||
if (!emailSignInAvailable(pod)) {
|
||||
throw EmailSignInRefusedException(reason: EmailSignInRefusal.unavailable);
|
||||
}
|
||||
return await session.db.transaction((transaction) async {
|
||||
final request = await _refusingAsEmail(
|
||||
() => _challengesOf(pod).completeChallenge(
|
||||
session,
|
||||
completionToken: switchToken,
|
||||
transaction: transaction,
|
||||
),
|
||||
);
|
||||
// SELECT * FROM "email_identity" WHERE "addressHash" = $1
|
||||
final identity = await EmailIdentity.db.findFirstRow(
|
||||
session,
|
||||
where: (t) => t.addressHash.equals(request.addressHash),
|
||||
transaction: transaction,
|
||||
);
|
||||
await _forget(session, [request], transaction: transaction);
|
||||
if (identity == null) {
|
||||
// The person deleted their account since.
|
||||
throw EmailSignInRefusedException(
|
||||
reason: EmailSignInRefusal.codeExpired,
|
||||
);
|
||||
}
|
||||
if (await signsInWithPassword(
|
||||
session,
|
||||
identity.authUserId,
|
||||
transaction: transaction,
|
||||
)) {
|
||||
throw EmailSignInRefusedException(reason: EmailSignInRefusal.usePassword);
|
||||
}
|
||||
return identity.authUserId;
|
||||
});
|
||||
}
|
||||
|
||||
/// Deletes the code requests past their time at [now], and returns how many:
|
||||
/// hourly, with the rate limit's notes (`PruningFutureCall`).
|
||||
Future<int> forgetExpiredEmailCodes(
|
||||
|
|
@ -462,7 +321,10 @@ SecretChallengeUtil<EmailCodeRequest> _newChallenges(String pepper) {
|
|||
return SecretChallengeUtil(
|
||||
// Codes are short: Argon2id at its usual cost.
|
||||
hashUtil: Argon2HashUtil(hashPepper: pepper, hashSaltLength: 16),
|
||||
// Switch tokens are long and random.
|
||||
// Serverpod's challenges hand out a completion token for every code
|
||||
// accepted; nothing takes it since codes sign nobody in (ADR 0017), and
|
||||
// it goes with its request (`switchChallengeId`, kept as the column the
|
||||
// util links, and as older servers read it during a deploy).
|
||||
completionTokenHash: Argon2HashUtil.forRandomSecrets(
|
||||
hashPepper: pepper,
|
||||
hashSaltLength: 16,
|
||||
|
|
@ -509,8 +371,7 @@ SecretChallengeUtil<EmailCodeRequest> _newChallenges(String pepper) {
|
|||
},
|
||||
),
|
||||
completionConfig: SecretChallengeCompletionConfig(
|
||||
// Switch tokens are long and random, but tries are counted all the
|
||||
// same.
|
||||
// Never completed any more (above); counted all the same.
|
||||
rateLimiter: DatabaseRateLimiter(
|
||||
RateLimiterConfig(
|
||||
domain: 'email_sign_in',
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
### A code accepted: the address is now the signed-in person's (or already
|
||||
### was), or it is another person's and [switchToken] signs in as them.
|
||||
class: EmailCodeAccepted
|
||||
fields:
|
||||
### Null when the address is the signed-in person's; else what signs in as
|
||||
### the person who has it, once, within the code's 15 minutes
|
||||
### (`EmailSignInEndpoint.signIn`).
|
||||
switchToken: String?
|
||||
|
|
@ -1,22 +1,15 @@
|
|||
import 'package:halkyon_base_server/src/auth/email.dart';
|
||||
import 'package:halkyon_base_server/src/auth/membership.dart';
|
||||
import 'package:halkyon_base_server/src/common/network.dart';
|
||||
import 'package:halkyon_base_server/src/generated/protocol.dart';
|
||||
import 'package:halkyon_base_server/src/register/account_register.dart';
|
||||
import 'package:halkyon_base_server/src/register/people.dart';
|
||||
import 'package:serverpod/serverpod.dart';
|
||||
import 'package:serverpod_auth_idp_server/core.dart';
|
||||
|
||||
/// Signing in with an email address, by a code mailed to it:
|
||||
/// the way to keep an anonymous account, as linking GitHub is, and a second
|
||||
/// way in for someone who signs in with GitHub. Signed in only, as every
|
||||
/// device is from its first start. The instance's register mails the codes
|
||||
/// and keeps the addresses' hashes (ADR 0016), this server's own database
|
||||
/// when it holds the register.
|
||||
/// Codes by mail (ADR 0017): what proves an address for a password's setup,
|
||||
/// a sign-up, a setup never finished and the resets. A code signs nobody in
|
||||
/// by itself. Without a session, as signing up and getting back in have
|
||||
/// none. The instance's register mails the codes and keeps the addresses'
|
||||
/// hashes (ADR 0016), this server's own database when it holds the
|
||||
/// register.
|
||||
class EmailCodeEndpoint extends Endpoint {
|
||||
@override
|
||||
bool get requireLogin => true;
|
||||
|
||||
/// Mails a code to [address], worded in [language] (`en`, `de`), and
|
||||
/// returns the request it answers; refusals as in [sendSignInCode].
|
||||
Future<UuidValue> sendCode(
|
||||
|
|
@ -29,22 +22,4 @@ class EmailCodeEndpoint extends Endpoint {
|
|||
language: language,
|
||||
network: networkOf(session),
|
||||
);
|
||||
|
||||
/// Accepts [code] for [request]: the address becomes the caller's unless
|
||||
/// it is another person's, whose token then signs in as them
|
||||
/// (`EmailSignInEndpoint.signIn`); refusals as in [acceptSignInCode].
|
||||
Future<EmailCodeAccepted> verifyCode(
|
||||
Session session, {
|
||||
required UuidValue request,
|
||||
required String code,
|
||||
}) async {
|
||||
final person = session.authenticated!.authUserId;
|
||||
final accepted = await AccountRegister.of(
|
||||
session.serverpod,
|
||||
).verifySignInCode(session, person: person, request: request, code: code);
|
||||
if (accepted.switchToken == null && isMember(session.serverpod)) {
|
||||
await becomeIdentified(session, person);
|
||||
}
|
||||
return accepted;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +0,0 @@
|
|||
import 'package:halkyon_base_server/src/auth/email.dart';
|
||||
import 'package:halkyon_base_server/src/register/account_register.dart';
|
||||
import 'package:halkyon_base_server/src/register/people.dart';
|
||||
import 'package:serverpod/serverpod.dart';
|
||||
import 'package:serverpod_auth_idp_server/core.dart';
|
||||
|
||||
/// Signs in the person whose address a mailed code proved, when the device
|
||||
/// switches to them. Called without a session: Serverpod issues no session
|
||||
/// for another person inside a signed-in call.
|
||||
class EmailSignInEndpoint extends Endpoint {
|
||||
/// A session for the person [switchToken] names; refusals as in
|
||||
/// [completeEmailSwitch].
|
||||
Future<AuthSuccess> signIn(
|
||||
Session session, {
|
||||
required String switchToken,
|
||||
}) async {
|
||||
final account = await AccountRegister.of(
|
||||
session.serverpod,
|
||||
).completeEmailSwitch(session, switchToken: switchToken);
|
||||
return await issueSession(session, account, method: emailMethod);
|
||||
}
|
||||
}
|
||||
|
|
@ -23,5 +23,6 @@ values:
|
|||
### The mail couldn't be sent; nothing was kept of the request.
|
||||
- mailFailed
|
||||
### The account signs in with its password and an authenticator (ADR
|
||||
### 0017): a code by mail no longer signs into it.
|
||||
### 0017): a code by mail no longer signs into it. No longer refused for
|
||||
### since codes sign nobody in; kept, as older apps know it by name.
|
||||
- usePassword
|
||||
|
|
|
|||
|
|
@ -1,75 +0,0 @@
|
|||
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
|
||||
/* To generate run: "serverpod generate" */
|
||||
|
||||
// ignore_for_file: implementation_imports
|
||||
// ignore_for_file: library_private_types_in_public_api
|
||||
// ignore_for_file: non_constant_identifier_names
|
||||
// ignore_for_file: public_member_api_docs
|
||||
// ignore_for_file: type_literal_in_constant_pattern
|
||||
// ignore_for_file: use_super_parameters
|
||||
// ignore_for_file: invalid_use_of_internal_member
|
||||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'package:serverpod/serverpod.dart' as _is;
|
||||
|
||||
/// A code accepted: the address is now the signed-in person's (or already
|
||||
/// was), or it is another person's and [switchToken] signs in as them.
|
||||
abstract class EmailCodeAccepted
|
||||
implements _is.SerializableModel, _is.ProtocolSerialization {
|
||||
EmailCodeAccepted._({this.switchToken});
|
||||
|
||||
factory EmailCodeAccepted({String? switchToken}) = _EmailCodeAcceptedImpl;
|
||||
|
||||
factory EmailCodeAccepted.fromJson(Map<String, dynamic> jsonSerialization) {
|
||||
return EmailCodeAccepted(
|
||||
switchToken: jsonSerialization['switchToken'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
/// Null when the address is the signed-in person's; else what signs in as
|
||||
/// the person who has it, once, within the code's 15 minutes
|
||||
/// (`EmailSignInEndpoint.signIn`).
|
||||
String? switchToken;
|
||||
|
||||
/// Returns a shallow copy of this [EmailCodeAccepted]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
EmailCodeAccepted copyWith({String? switchToken});
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'__className__': 'halkyon_base.EmailCodeAccepted',
|
||||
if (switchToken != null) 'switchToken': switchToken,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJsonForProtocol() {
|
||||
return {
|
||||
'__className__': 'halkyon_base.EmailCodeAccepted',
|
||||
if (switchToken != null) 'switchToken': switchToken,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return _is.SerializationManager.encode(this);
|
||||
}
|
||||
}
|
||||
|
||||
class _Undefined {}
|
||||
|
||||
class _EmailCodeAcceptedImpl extends EmailCodeAccepted {
|
||||
_EmailCodeAcceptedImpl({String? switchToken})
|
||||
: super._(switchToken: switchToken);
|
||||
|
||||
/// Returns a shallow copy of this [EmailCodeAccepted]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
@override
|
||||
EmailCodeAccepted copyWith({Object? switchToken = _Undefined}) {
|
||||
return EmailCodeAccepted(
|
||||
switchToken: switchToken is String? ? switchToken : this.switchToken,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -43,7 +43,8 @@ enum EmailSignInRefusal implements _is.SerializableModel {
|
|||
mailFailed,
|
||||
|
||||
/// The account signs in with its password and an authenticator (ADR
|
||||
/// 0017): a code by mail no longer signs into it.
|
||||
/// 0017): a code by mail no longer signs into it. No longer refused for
|
||||
/// since codes sign nobody in; kept, as older apps know it by name.
|
||||
usePassword;
|
||||
|
||||
static EmailSignInRefusal fromJson(String name) {
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ import 'package:serverpod_auth_idp_server/serverpod_auth_idp_server.dart'
|
|||
import '../auth/account_endpoint.dart' as _ii6ycf6v;
|
||||
import '../auth/anonymous_idp_endpoint.dart' as _in0zita6;
|
||||
import '../auth/email_code_endpoint.dart' as _in3cimlf;
|
||||
import '../auth/email_sign_in_endpoint.dart' as _irqdog5c;
|
||||
import '../auth/github_sign_in_endpoint.dart' as _iasw79m5;
|
||||
import '../auth/github_upgrade_endpoint.dart' as _ivnugbf2;
|
||||
import '../instance/instance_endpoint.dart' as _i3g8bfc2;
|
||||
|
|
@ -57,12 +56,6 @@ class Endpoints extends _is.EndpointDispatch {
|
|||
'emailCode',
|
||||
'halkyon_base',
|
||||
),
|
||||
'emailSignIn': _irqdog5c.EmailSignInEndpoint()
|
||||
..initialize(
|
||||
server,
|
||||
'emailSignIn',
|
||||
'halkyon_base',
|
||||
),
|
||||
'gitHubSignIn': _iasw79m5.GitHubSignInEndpoint()
|
||||
..initialize(
|
||||
server,
|
||||
|
|
@ -199,57 +192,6 @@ class Endpoints extends _is.EndpointDispatch {
|
|||
language: params['language'],
|
||||
),
|
||||
),
|
||||
'verifyCode': _is.MethodConnector(
|
||||
name: 'verifyCode',
|
||||
params: {
|
||||
'request': _is.ParameterDescription(
|
||||
name: 'request',
|
||||
type: _is.getType<_is.UuidValue>(),
|
||||
nullable: false,
|
||||
),
|
||||
'code': _is.ParameterDescription(
|
||||
name: 'code',
|
||||
type: _is.getType<String>(),
|
||||
nullable: false,
|
||||
),
|
||||
},
|
||||
call:
|
||||
(
|
||||
_is.Session session,
|
||||
Map<String, dynamic> params,
|
||||
) async => (endpoints['emailCode'] as _in3cimlf.EmailCodeEndpoint)
|
||||
.verifyCode(
|
||||
session,
|
||||
request: params['request'],
|
||||
code: params['code'],
|
||||
),
|
||||
),
|
||||
},
|
||||
);
|
||||
connectors['emailSignIn'] = _is.EndpointConnector(
|
||||
name: 'emailSignIn',
|
||||
endpoint: endpoints['emailSignIn']!,
|
||||
methodConnectors: {
|
||||
'signIn': _is.MethodConnector(
|
||||
name: 'signIn',
|
||||
params: {
|
||||
'switchToken': _is.ParameterDescription(
|
||||
name: 'switchToken',
|
||||
type: _is.getType<String>(),
|
||||
nullable: false,
|
||||
),
|
||||
},
|
||||
call:
|
||||
(
|
||||
_is.Session session,
|
||||
Map<String, dynamic> params,
|
||||
) async =>
|
||||
(endpoints['emailSignIn'] as _irqdog5c.EmailSignInEndpoint)
|
||||
.signIn(
|
||||
session,
|
||||
switchToken: params['switchToken'],
|
||||
),
|
||||
),
|
||||
},
|
||||
);
|
||||
connectors['gitHubSignIn'] = _is.EndpointConnector(
|
||||
|
|
@ -349,33 +291,6 @@ class Endpoints extends _is.EndpointDispatch {
|
|||
name: 'accountRecovery',
|
||||
endpoint: endpoints['accountRecovery']!,
|
||||
methodConnectors: {
|
||||
'sendCode': _is.MethodConnector(
|
||||
name: 'sendCode',
|
||||
params: {
|
||||
'address': _is.ParameterDescription(
|
||||
name: 'address',
|
||||
type: _is.getType<String>(),
|
||||
nullable: false,
|
||||
),
|
||||
'language': _is.ParameterDescription(
|
||||
name: 'language',
|
||||
type: _is.getType<String>(),
|
||||
nullable: false,
|
||||
),
|
||||
},
|
||||
call:
|
||||
(
|
||||
_is.Session session,
|
||||
Map<String, dynamic> params,
|
||||
) async =>
|
||||
(endpoints['accountRecovery']
|
||||
as _ivnbjhab.AccountRecoveryEndpoint)
|
||||
.sendCode(
|
||||
session,
|
||||
address: params['address'],
|
||||
language: params['language'],
|
||||
),
|
||||
),
|
||||
'resetAuthenticator': _is.MethodConnector(
|
||||
name: 'resetAuthenticator',
|
||||
params: {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ import 'package:serverpod_auth_core_server/serverpod_auth_core_server.dart'
|
|||
as _iacs;
|
||||
import 'package:serverpod_auth_idp_server/serverpod_auth_idp_server.dart'
|
||||
as _iais;
|
||||
import 'auth/email_code_accepted.dart' as _iys46ui1;
|
||||
import 'auth/email_code_request.dart' as _iqxum1j8;
|
||||
import 'auth/email_identity.dart' as _id1dpd4g;
|
||||
import 'auth/email_sign_in_refusal.dart' as _ieyvcag1;
|
||||
|
|
@ -55,7 +54,6 @@ import 'profile/profile_refused_exception.dart' as _idvc5e7p;
|
|||
import 'profile/public_profile.dart' as _ioppe2ep;
|
||||
import 'register/account_deletion_notice.dart' as _i2lyzpm9;
|
||||
import 'register/sessions_ended_notice.dart' as _io5c3tod;
|
||||
export 'auth/email_code_accepted.dart';
|
||||
export 'auth/email_code_request.dart';
|
||||
export 'auth/email_identity.dart';
|
||||
export 'auth/email_sign_in_refusal.dart';
|
||||
|
|
@ -881,9 +879,6 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
}
|
||||
}
|
||||
|
||||
if (t == _iys46ui1.EmailCodeAccepted) {
|
||||
return _iys46ui1.EmailCodeAccepted.fromJson(data) as T;
|
||||
}
|
||||
if (t == _iqxum1j8.EmailCodeRequest) {
|
||||
return _iqxum1j8.EmailCodeRequest.fromJson(data) as T;
|
||||
}
|
||||
|
|
@ -989,10 +984,6 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
if (t == _io5c3tod.SessionsEndedNotice) {
|
||||
return _io5c3tod.SessionsEndedNotice.fromJson(data) as T;
|
||||
}
|
||||
if (t == _is.getType<_iys46ui1.EmailCodeAccepted?>()) {
|
||||
return (data != null ? _iys46ui1.EmailCodeAccepted.fromJson(data) : null)
|
||||
as T;
|
||||
}
|
||||
if (t == _is.getType<_iqxum1j8.EmailCodeRequest?>()) {
|
||||
return (data != null ? _iqxum1j8.EmailCodeRequest.fromJson(data) : null)
|
||||
as T;
|
||||
|
|
@ -1181,7 +1172,6 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
|
||||
static String? getClassNameForType(Type type) {
|
||||
return switch (type) {
|
||||
_iys46ui1.EmailCodeAccepted => 'EmailCodeAccepted',
|
||||
_iqxum1j8.EmailCodeRequest => 'EmailCodeRequest',
|
||||
_id1dpd4g.EmailIdentity => 'EmailIdentity',
|
||||
_ieyvcag1.EmailSignInRefusal => 'EmailSignInRefusal',
|
||||
|
|
@ -1234,8 +1224,6 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
}
|
||||
|
||||
switch (data) {
|
||||
case _iys46ui1.EmailCodeAccepted():
|
||||
return 'EmailCodeAccepted';
|
||||
case _iqxum1j8.EmailCodeRequest():
|
||||
return 'EmailCodeRequest';
|
||||
case _id1dpd4g.EmailIdentity():
|
||||
|
|
@ -1320,9 +1308,6 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
if (dataClassName is! String) {
|
||||
return super.deserializeByClassName(data);
|
||||
}
|
||||
if (dataClassName == 'EmailCodeAccepted') {
|
||||
return deserialize<_iys46ui1.EmailCodeAccepted>(data['data']);
|
||||
}
|
||||
if (dataClassName == 'EmailCodeRequest') {
|
||||
return deserialize<_iqxum1j8.EmailCodeRequest>(data['data']);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,9 +5,6 @@ anonymousIdp:
|
|||
- login:
|
||||
emailCode:
|
||||
- sendCode:
|
||||
- verifyCode:
|
||||
emailSignIn:
|
||||
- signIn:
|
||||
gitHubSignIn:
|
||||
- signIn:
|
||||
gitHubUpgrade:
|
||||
|
|
@ -15,7 +12,6 @@ gitHubUpgrade:
|
|||
instance:
|
||||
- basics:
|
||||
accountRecovery:
|
||||
- sendCode:
|
||||
- resetAuthenticator:
|
||||
- beginKeyProof:
|
||||
- resetPassword:
|
||||
|
|
|
|||
|
|
@ -13,24 +13,10 @@ import 'package:serverpod_auth_idp_server/core.dart';
|
|||
/// without a session, as the person can't sign in. The instance's register
|
||||
/// checks every proof (ADR 0016); a session is this server's.
|
||||
class AccountRecoveryEndpoint extends Endpoint {
|
||||
/// Mails a code to [address], worded in [language] (`en`, `de`), as email
|
||||
/// sign-in mails it, and returns the request it answers: the proof of the
|
||||
/// address that resets take. Refusals as `sendSignInCode`'s.
|
||||
Future<UuidValue> sendCode(
|
||||
Session session, {
|
||||
required String address,
|
||||
required String language,
|
||||
}) => AccountRegister.of(session.serverpod).sendSignInCode(
|
||||
session,
|
||||
address: address,
|
||||
language: language,
|
||||
network: networkOf(session),
|
||||
);
|
||||
|
||||
/// Asks for the reset of the authenticators of the account at [address]:
|
||||
/// the password's proof for [challenge] (`passwordSignIn.begin`), and a
|
||||
/// [code] mailed for [request] or the recovery phrase's [signature] for
|
||||
/// [keyChallenge] ([beginKeyProof]). It takes effect 72 hours later
|
||||
/// [keyChallenge] ([beginKeyProof]); codes by mail come from `emailCode.sendCode`. It takes effect 72 hours later
|
||||
/// unless the owner signs in with a code meanwhile; a mail in [language]
|
||||
/// says so. Refusals as `resetAuthenticator`'s.
|
||||
Future<ResetPending> resetAuthenticator(
|
||||
|
|
|
|||
|
|
@ -119,24 +119,6 @@ abstract interface class AccountRegister {
|
|||
required String network,
|
||||
});
|
||||
|
||||
/// Accepts [code] for [request] from [person]: the address becomes theirs
|
||||
/// (an account of the register, under their ID) unless it is another
|
||||
/// person's, whose switch token it then gives. Refusals as
|
||||
/// `acceptSignInCode`'s.
|
||||
Future<EmailCodeAccepted> verifySignInCode(
|
||||
Session session, {
|
||||
required UuidValue person,
|
||||
required UuidValue request,
|
||||
required String code,
|
||||
});
|
||||
|
||||
/// The account a switch token names, once only. Refusals as
|
||||
/// `completeEmailSwitch`'s.
|
||||
Future<UuidValue> completeEmailSwitch(
|
||||
Session session, {
|
||||
required String switchToken,
|
||||
});
|
||||
|
||||
/// Links the GitHub account [gitHubUserId] to [person] (an account of the
|
||||
/// register, under their ID). Refusals as `linkGitHubAccount`'s.
|
||||
Future<void> linkGitHub(
|
||||
|
|
|
|||
|
|
@ -178,20 +178,6 @@ Future<Map<String, Object?>?> _answer(
|
|||
network: arguments.string('network'),
|
||||
);
|
||||
return {'request': request.uuid};
|
||||
case RegisterOperation.verifySignInCode:
|
||||
final accepted = await register.verifySignInCode(
|
||||
session,
|
||||
person: arguments.id('person'),
|
||||
request: arguments.id('request'),
|
||||
code: arguments.string('code'),
|
||||
);
|
||||
return {'accepted': accepted.toJson()};
|
||||
case RegisterOperation.completeEmailSwitch:
|
||||
final account = await register.completeEmailSwitch(
|
||||
session,
|
||||
switchToken: arguments.string('switchToken'),
|
||||
);
|
||||
return {'account': account.uuid};
|
||||
case RegisterOperation.linkGitHub:
|
||||
await register.linkGitHub(
|
||||
session,
|
||||
|
|
|
|||
|
|
@ -343,25 +343,6 @@ final class LocalRegister implements AccountRegister {
|
|||
network: network,
|
||||
);
|
||||
|
||||
@override
|
||||
Future<EmailCodeAccepted> verifySignInCode(
|
||||
Session session, {
|
||||
required UuidValue person,
|
||||
required UuidValue request,
|
||||
required String code,
|
||||
}) => email.acceptSignInCode(
|
||||
session,
|
||||
authUserId: person,
|
||||
request: request,
|
||||
code: code,
|
||||
);
|
||||
|
||||
@override
|
||||
Future<UuidValue> completeEmailSwitch(
|
||||
Session session, {
|
||||
required String switchToken,
|
||||
}) => email.completeEmailSwitch(session, switchToken: switchToken);
|
||||
|
||||
@override
|
||||
Future<void> linkGitHub(
|
||||
Session session, {
|
||||
|
|
|
|||
|
|
@ -26,8 +26,6 @@ abstract final class RegisterOperation {
|
|||
static const fixHandle = 'fix-handle';
|
||||
static const emailSignInOffered = 'email-sign-in-offered';
|
||||
static const sendSignInCode = 'send-sign-in-code';
|
||||
static const verifySignInCode = 'verify-sign-in-code';
|
||||
static const completeEmailSwitch = 'complete-email-switch';
|
||||
static const linkGitHub = 'link-github';
|
||||
static const accountWithGitHub = 'account-with-github';
|
||||
static const signInWaysOf = 'sign-in-ways-of';
|
||||
|
|
|
|||
|
|
@ -243,33 +243,6 @@ final class RemoteRegister implements AccountRegister {
|
|||
}))['request'],
|
||||
)!;
|
||||
|
||||
@override
|
||||
Future<EmailCodeAccepted> verifySignInCode(
|
||||
Session session, {
|
||||
required UuidValue person,
|
||||
required UuidValue request,
|
||||
required String code,
|
||||
}) async => switch ((await _ask(RegisterOperation.verifySignInCode, {
|
||||
'person': person.uuid,
|
||||
'request': request.uuid,
|
||||
'code': code,
|
||||
}))['accepted']) {
|
||||
final Map<String, Object?> accepted => EmailCodeAccepted.fromJson(accepted),
|
||||
_ => throw RegisterUnavailableException(
|
||||
'${RegisterOperation.verifySignInCode}: no answer',
|
||||
),
|
||||
};
|
||||
|
||||
@override
|
||||
Future<UuidValue> completeEmailSwitch(
|
||||
Session session, {
|
||||
required String switchToken,
|
||||
}) async => _id(
|
||||
(await _ask(RegisterOperation.completeEmailSwitch, {
|
||||
'switchToken': switchToken,
|
||||
}))['account'],
|
||||
)!;
|
||||
|
||||
@override
|
||||
Future<void> linkGitHub(
|
||||
Session session, {
|
||||
|
|
|
|||
Loading…
Reference in a new issue