feat(auth): sign in with an email address, keeping only a keyed hash of it

People without GitHub can now keep an anonymous account, and sign in on
another device, with an email address and a code mailed to it each time;
no password. The instance keeps only HMAC-SHA256 of the address, keyed
with its emailSignInPepper (ADR 0010): from what someone types it finds
the person and tells whether an address is free, it mails only an address
someone has just typed, and it can't write to anybody otherwise or show
anybody their address. The device remembers the address it signed in
with, in the person's folder, to show it.

It works as GitHub does. A code for a new address makes it the signed-in
person's, who becomes identified and keeps everything. A code for another
person's address gives a switch token, which signs in as them without a
session; a device holding studysets asks first. Asking for a code answers
alike for every address. Codes are eight Crockford characters, valid 15
minutes and three tries (Serverpod's code challenges); three codes an
hour per address, ten per network. In development the code goes to the
server's log; elsewhere there is no email sign-in until a mail service
is set up (instance.info().emailSignIn).

Someone identified may add the other way in: the Profile tab lists how
they sign in and adds GitHub or an address. They are never switched to
another account: an address or GitHub account that is another's is said
to be so. Switching now deletes the account left behind only when it was
anonymous; it deleted any, which only anonymous people could reach so far.

The migration adds email_identity and email_code_request with their
indexes and foreign keys to Serverpod's challenges; nothing else changes.

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-15 19:42:21 +02:00
commit 1cfd391dbc
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -1778,6 +1778,113 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get profileSwitchConfirm => 'Wechseln';
@override
String get profileContinueWithEmail => 'Mit E-Mail fortfahren';
@override
String get profileSignInWays => 'Anmeldewege';
@override
String get profileWayGitHub => 'GitHub';
@override
String get profileWayEmail => 'E-Mail-Adresse';
@override
String get profileWayLinked => 'Meldet dich an';
@override
String get profileAddGitHub => 'GitHub hinzufügen';
@override
String get profileAddEmail => 'E-Mail-Adresse hinzufügen';
@override
String get profileGitHubTaken =>
'Mit diesem GitHub-Konto meldet sich schon ein anderes Konto an.';
@override
String get emailSignInTitle => 'Mit E-Mail fortfahren';
@override
String get emailSignInIntro =>
'Wir schicken dir einen Code zum Anmelden. Diese Instanz behält nur einen Fingerabdruck deiner Adresse, nie die Adresse selbst, und schreibt ihr nur, wenn du einen Code anforderst.';
@override
String get emailSignInAddress => 'E-Mail-Adresse';
@override
String get emailSignInSend => 'Code senden';
@override
String emailSignInCodeSent(String address) {
return 'Wir haben einen Code an $address geschickt. Er gilt 15 Minuten.';
}
@override
String get emailSignInResent => 'Ein neuer Code ist unterwegs.';
@override
String get emailSignInCode => 'Code';
@override
String get emailSignInContinue => 'Weiter';
@override
String get emailSignInResend => 'Neuen Code senden';
@override
String get emailSignInOtherAddress => 'Andere Adresse verwenden';
@override
String emailSignInSwitchTitle(String address) {
return 'Zum Konto von $address wechseln?';
}
@override
String get emailSignInSwitchMessage =>
'Diese Adresse hat bereits Fortschritt von einem anderen Gerät. Die Lernsets auf diesem Gerät lassen sich noch nicht damit zusammenführen: Wenn du wechselst, werden sie gelöscht.';
@override
String emailSignInTaken(String address) {
return 'Mit $address meldet sich schon ein anderes Konto an. Um zu ihm zu wechseln, melde dich zuerst ab.';
}
@override
String emailSignInDone(String address) {
return 'Mit $address angemeldet.';
}
@override
String get emailSignInUnavailable =>
'Diese Instanz bietet keine Anmeldung mit E-Mail an.';
@override
String get emailSignInNotAnAddress => 'Das ist keine E-Mail-Adresse.';
@override
String get emailSignInTooManyCodes =>
'Zu viele Codes in der letzten Stunde. Versuche es später noch einmal.';
@override
String get emailSignInWrongCode => 'Dieser Code stimmt nicht.';
@override
String get emailSignInTooManyTries =>
'Zu viele falsche Codes. Fordere einen neuen an.';
@override
String get emailSignInCodeExpired =>
'Dieser Code gilt nicht mehr. Fordere einen neuen an.';
@override
String get emailSignInOtherAddressLinked =>
'Du meldest dich schon mit einer anderen Adresse an.';
@override
String get emailSignInFailed =>
'Die Instanz war nicht erreichbar. Versuche es noch einmal.';
@override
String get publicProfileTitle => 'Öffentliches Profil';
@ -1798,7 +1905,7 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get publicProfileAnonymousHint =>
'Melde dich mit GitHub an, um ein öffentliches Profil zu haben und zu veröffentlichen.';
'Melde dich an, um ein öffentliches Profil zu haben und zu veröffentlichen.';
@override
String get publicProfileDisplayName => 'Anzeigename';
@ -2014,7 +2121,7 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String accountRemoveMessage(String name) {
return 'Entfernt das Konto auf $name von diesem Gerät, mit allem, was das Gerät davon hat. Was es synchronisiert hat, bleibt auf $name: Melde dich dort mit GitHub an, um es wiederzuhaben. In ein anonymes Konto kann man sich nicht wieder anmelden.';
return 'Entfernt das Konto auf $name von diesem Gerät, mit allem, was das Gerät davon hat. Was es synchronisiert hat, bleibt auf $name: Melde dich dort wieder an, um es wiederzuhaben. In ein anonymes Konto kann man sich nicht wieder anmelden.';
}
@override
@ -5899,7 +6006,7 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get publishAnonymous =>
'Melde dich mit GitHub an, um ein öffentliches Profil zu haben und zu veröffentlichen.';
'Melde dich an, um ein öffentliches Profil zu haben und zu veröffentlichen.';
@override
String get publishName => 'Name';
@ -6411,7 +6518,7 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get feedbackNeedsIdentity =>
'Melde dich mit GitHub an, um im Original zu melden und vorzuschlagen.';
'Melde dich an, um im Original zu melden und vorzuschlagen.';
@override
String get kanjiExampleWords => 'Wörter damit';