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:
Tizian 「ティツィアーン」 2026-10-20 19:31:49 +02:00
commit ae57cbe864
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
4 changed files with 429 additions and 3835 deletions

View file

@ -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_client/serverpod_client.dart' as _isc;
/// 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 _isc.SerializableModel, _isc.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.
@_isc.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 _isc.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.
@_isc.useResult
@override
EmailCodeAccepted copyWith({Object? switchToken = _Undefined}) {
return EmailCodeAccepted(
switchToken: switchToken is String? ? switchToken : this.switchToken,
);
}
}

View file

@ -43,7 +43,8 @@ enum EmailSignInRefusal implements _isc.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) {

View file

@ -12,8 +12,6 @@
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'dart:async' as _ida;
import 'dart:typed_data' as _idt;
import 'package:halkyon_base_client/src/protocol/auth/email_code_accepted.dart'
as _ioeosfv3;
import 'package:halkyon_base_client/src/protocol/auth/sign_in_ways.dart'
as _ifld8s4r;
import 'package:halkyon_base_client/src/protocol/instance/instance_basics.dart'
@ -98,12 +96,12 @@ class EndpointAnonymousIdp extends _iaic.EndpointAnonymousIdpBase {
);
}
/// 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.
/// {@category Endpoint}
class EndpointEmailCode extends _isc.EndpointRef {
EndpointEmailCode(_isc.EndpointCaller caller) : super(caller);
@ -124,41 +122,6 @@ class EndpointEmailCode extends _isc.EndpointRef {
'language': language,
},
);
/// 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].
_ida.Future<_ioeosfv3.EmailCodeAccepted> verifyCode({
required _isc.UuidValue request,
required String code,
}) => caller.callServerEndpoint<_ioeosfv3.EmailCodeAccepted>(
'halkyon_base.emailCode',
'verifyCode',
{
'request': request,
'code': code,
},
);
}
/// 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.
/// {@category Endpoint}
class EndpointEmailSignIn extends _isc.EndpointRef {
EndpointEmailSignIn(_isc.EndpointCaller caller) : super(caller);
@override
String get name => 'halkyon_base.emailSignIn';
/// A session for the person [switchToken] names; refusals as in
/// [completeEmailSwitch].
_ida.Future<_iacc.AuthSuccess> signIn({required String switchToken}) =>
caller.callServerEndpoint<_iacc.AuthSuccess>(
'halkyon_base.emailSignIn',
'signIn',
{'switchToken': switchToken},
);
}
/// Signs in the person who already has a GitHub account, e.g. when they switch
@ -250,25 +213,10 @@ class EndpointAccountRecovery extends _isc.EndpointRef {
@override
String get name => 'halkyon_base.accountRecovery';
/// 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.
_ida.Future<_isc.UuidValue> sendCode({
required String address,
required String language,
}) => caller.callServerEndpoint<_isc.UuidValue>(
'halkyon_base.accountRecovery',
'sendCode',
{
'address': address,
'language': language,
},
);
/// 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.
_ida.Future<_ippuf33k.ResetPending> resetAuthenticator({
@ -719,7 +667,6 @@ class Caller extends _isc.ModuleEndpointCaller {
account = EndpointAccount(this);
anonymousIdp = EndpointAnonymousIdp(this);
emailCode = EndpointEmailCode(this);
emailSignIn = EndpointEmailSignIn(this);
gitHubSignIn = EndpointGitHubSignIn(this);
gitHubUpgrade = EndpointGitHubUpgrade(this);
instance = EndpointInstance(this);
@ -737,8 +684,6 @@ class Caller extends _isc.ModuleEndpointCaller {
late final EndpointEmailCode emailCode;
late final EndpointEmailSignIn emailSignIn;
late final EndpointGitHubSignIn gitHubSignIn;
late final EndpointGitHubUpgrade gitHubUpgrade;
@ -762,7 +707,6 @@ class Caller extends _isc.ModuleEndpointCaller {
'halkyon_base.account': account,
'halkyon_base.anonymousIdp': anonymousIdp,
'halkyon_base.emailCode': emailCode,
'halkyon_base.emailSignIn': emailSignIn,
'halkyon_base.gitHubSignIn': gitHubSignIn,
'halkyon_base.gitHubUpgrade': gitHubUpgrade,
'halkyon_base.instance': instance,

View file

@ -18,7 +18,6 @@ import 'package:serverpod_auth_core_client/serverpod_auth_core_client.dart'
import 'package:serverpod_auth_idp_client/serverpod_auth_idp_client.dart'
as _iaic;
import 'package:serverpod_client/serverpod_client.dart' as _isc;
import 'auth/email_code_accepted.dart' as _iys46ui1;
import 'auth/email_sign_in_refusal.dart' as _ieyvcag1;
import 'auth/email_sign_in_refused_exception.dart' as _iztfgag5;
import 'auth/github_sign_in_refusal.dart' as _ir1gd9p5;
@ -43,7 +42,6 @@ import 'profile/profile.dart' as _ia5j8fch;
import 'profile/profile_not_found_exception.dart' as _iim3hfcf;
import 'profile/profile_refusal.dart' as _i6j6efd1;
import 'profile/profile_refused_exception.dart' as _idvc5e7p;
export 'auth/email_code_accepted.dart';
export 'auth/email_sign_in_refusal.dart';
export 'auth/email_sign_in_refused_exception.dart';
export 'auth/github_sign_in_refusal.dart';
@ -115,9 +113,6 @@ class Protocol extends _isc.SerializationManager {
}
}
if (t == _iys46ui1.EmailCodeAccepted) {
return _iys46ui1.EmailCodeAccepted.fromJson(data) as T;
}
if (t == _ieyvcag1.EmailSignInRefusal) {
return _ieyvcag1.EmailSignInRefusal.fromJson(data) as T;
}
@ -190,10 +185,6 @@ class Protocol extends _isc.SerializationManager {
if (t == _idvc5e7p.ProfileRefusedException) {
return _idvc5e7p.ProfileRefusedException.fromJson(data) as T;
}
if (t == _isc.getType<_iys46ui1.EmailCodeAccepted?>()) {
return (data != null ? _iys46ui1.EmailCodeAccepted.fromJson(data) : null)
as T;
}
if (t == _isc.getType<_ieyvcag1.EmailSignInRefusal?>()) {
return (data != null ? _ieyvcag1.EmailSignInRefusal.fromJson(data) : null)
as T;
@ -333,7 +324,6 @@ class Protocol extends _isc.SerializationManager {
static String? getClassNameForType(Type type) {
return switch (type) {
_iys46ui1.EmailCodeAccepted => 'EmailCodeAccepted',
_ieyvcag1.EmailSignInRefusal => 'EmailSignInRefusal',
_iztfgag5.EmailSignInRefusedException => 'EmailSignInRefusedException',
_ir1gd9p5.GitHubSignInRefusal => 'GitHubSignInRefusal',
@ -375,8 +365,6 @@ class Protocol extends _isc.SerializationManager {
}
switch (data) {
case _iys46ui1.EmailCodeAccepted():
return 'EmailCodeAccepted';
case _ieyvcag1.EmailSignInRefusal():
return 'EmailSignInRefusal';
case _iztfgag5.EmailSignInRefusedException():
@ -435,9 +423,6 @@ class Protocol extends _isc.SerializationManager {
if (dataClassName is! String) {
return super.deserializeByClassName(data);
}
if (dataClassName == 'EmailCodeAccepted') {
return deserialize<_iys46ui1.EmailCodeAccepted>(data['data']);
}
if (dataClassName == 'EmailSignInRefusal') {
return deserialize<_ieyvcag1.EmailSignInRefusal>(data['data']);
}