feat(base,app)!: GitHub signs nobody in any more (ADR 0020, D36)

Every account signs in with SimpleLogin or a password now, so GitHub
goes from the module, the shared app package, Learn and Todo, ahead of
ADR 0017's GITHUB_SIGN_IN_ENDS.

- halkyon_base: no `gitHubSignIn` endpoint, GitHub identity provider or
  GitHub refusals; no `githubClientId`/`githubClientSecret`, no
  `GITHUB_SIGN_IN_ENDS` and no `forgetGitHubAccounts`; the register
  loses `account-with-github`. `SignInWays.gitHub`, the basics'
  `gitHubClientId` and `gitHubSignInEnds`, and `PasswordRefusal`'s
  `signInAgain` and `noAddress` go. A first password takes a code
  mailed to the person's own address; an account without one sets none
  (`wrongCredentials`).
- Learn's server: `instance.info()` without `gitHubClientId`, the
  pruning without GitHub, and `minimumAppBuild` 24, as builds up to 23
  (v0.20.0) require `SignInWays.gitHub`: the server deploys with the
  release of build 24.
- halkyon_app: no GitHub button or problems on the start screen, no
  password offered after a GitHub sign-in, and `ServerpodAuthGateway`
  without the browser half only GitHub used (`(client, base:)`). The
  account section offers a first password only to an account with an
  address. `LoopbackWebAuth` and the apps' schemes stay, SimpleLogin's
  ways back.
- Learn's Profile lists no GitHub; the texts go (English, German).
- Serverpod's `serverpod_auth_idp_github_account` stays, as its
  migrations make it; a row goes with its account, and ADR 0020's D36
  gives operators the SQL to delete those left.
- deploy/: compose.yaml and .env.example without GitHub. The READMEs,
  AGENTS.md files and ADR 0017 amended.

BREAKING CHANGE: builds up to 23 can't read the instance's answers
about an account's ways in; minimumAppBuild is 24.

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-25 12:52:52 +01:00
commit 4e33133733
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
40 changed files with 564 additions and 3570 deletions

View file

@ -39,8 +39,7 @@ Written as the apps are: Dart 3.13's primary constructors, Material from
the instance in use (`AppDependencies`, handed down by `AppScope`), anew on
a switch, the previous ones closed first; `SessionGate` shows the app of
whoever is signed in and asks how to start while nobody is
(`StartScreen`: SimpleLogin, a password, GitHub for those who have it,
another instance; an instance out of reach is said so, with Try again,
(`StartScreen`: SimpleLogin, a password, another instance; an instance out of reach is said so, with Try again,
and one offering no way to create an account too), at a first start as
after signing out; an anonymous account's session, left from before
there were none, is deleted as the app starts (ADR 0020, D25), falling
@ -62,15 +61,13 @@ Written as the apps are: Dart 3.13's primary constructors, Material from
it `hasSnackBars`). An instance out of reach signs out on the device all
the same (Serverpod's `signOutDevice`), so only the device's own storage
fails a sign-out.
- **Signing in** (`lib/src/auth/`): `AuthRepository` on an `AuthGateway`;
`ServerpodAuthGateway` signs in with GitHub those who have it, as it is
added to nobody any more (ADR 0020, D25; the system browser and a loopback
address on Linux and Windows, halkyon_oidc_flutter's `LoopbackWebAuth`),
through the instance's `halkyon_base`, and puts a session granted elsewhere
in place (`useSession`); `SessionBloc` and `SignInWaysCubit`;
`SignInAddresses`, `PersonFolders`. The start screen says why a GitHub
sign-in was refused, and for a GitHub account nobody here has, the way to
create an account in its place.
- **The session** (`lib/src/auth/`): `AuthRepository` on an `AuthGateway`;
`ServerpodAuthGateway` follows the session the app's Serverpod client
keeps, signs out, deletes the account through the instance's
`halkyon_base`, and puts a session granted elsewhere in place
(`useSession`: a password's or SimpleLogin's sign-in; GitHub signs nobody
in since ADR 0020, D36); `SessionBloc` and `SignInWaysCubit`;
`SignInAddresses`, `PersonFolders`.
- **Content sealed with data keys** (`lib/src/privacy/`, ADR 0019):
`ContentCipher` (`SodiumContentCipher`, on the isolate it is used on, as an
app seals and opens rows one by one; `FakeContentCipher` in tests) makes a
@ -199,10 +196,8 @@ Written as the apps are: Dart 3.13's primary constructors, Material from
`GlobalMaterialLocalizations.delegates`.
4. **The frame** (`AppFrame`): the app's name and wordmark word, its icon, an
example address, its theme, delegates, languages (`resolveAppLocale` as
its resolution) and launch screen, and the three sentences only it can
say: what the first start's instance keeps of the person's, what a switch
to another account leaves behind, and where GitHub is added later (shown
only where the instance offers no passwords).
its resolution) and launch screen, and the sentence only it can say: what
the first start's instance keeps of the person's.
5. **Android**: the app's `MainActivity` answers `app.halkyon/screen_guard`
(copy Learn's or Todo's `ScreenGuard.kt` and call `ScreenGuard.attach` in
`configureFlutterEngine`), or secrets show in screenshots and recents.
@ -214,10 +209,7 @@ Written as the apps are: Dart 3.13's primary constructors, Material from
`SodiumAccountCrypto()`, `SecureAccountKeyStore(storage.keysOf(instance))`,
the auth gateway and the app's `SignInAddresses`. Its `auth` is an
`AuthRepository` on `ServerpodAuthGateway(client, base:
client.modules.base, callbackScheme: …, appName: …)`; the scheme is the
app's own (`app.halkyon.learn`), registered with each platform and in the
instance's GitHub OAuth app, whose loopback address on Linux and Windows is
`http://127.0.0.1/github-callback` (port 43823 unless given).
client.modules.base)`.
7. **The tree**:
```dart

View file

@ -14,9 +14,9 @@
/// - **Signing in**: with SimpleLogin (ADR 0020: `SimpleLoginRepository`,
/// its sheet and `SimpleLoginSection`), with a password and TOTP (ADR
/// 0017: `PasswordRepository`, its sheets and `AccountSecuritySection`),
/// and with GitHub while it lasts (`AuthRepository` on
/// `ServerpodAuthGateway`), the account's keys on the device. Every
/// account has a way in: there are no anonymous ones (ADR 0020, D4).
/// the session on `AuthRepository` and `ServerpodAuthGateway`, the
/// account's keys on the device. Every account has a way in: there are no
/// anonymous ones (ADR 0020, D4).
/// - **Sync**: `SyncService` on triggers, the changes still waiting, and
/// where it stands (`SyncStatusBloc`, `SyncStatusCard`, `AppMismatchBanner`,
/// `SyncTroubleBadge`).
@ -39,7 +39,6 @@ export 'src/auth/data/auth_repository.dart';
export 'src/auth/data/serverpod_auth_gateway.dart';
export 'src/auth/data/sign_in_addresses.dart';
export 'src/auth/model/auth_status.dart';
export 'src/auth/model/github_sign_in.dart';
export 'src/auth/model/session_grant.dart';
export 'src/auth/model/sign_in_ways.dart';
export 'src/auth/widget/account_tiles.dart';

View file

@ -44,13 +44,6 @@
"personDataFailed": "Die Daten dieses Geräts konnten nicht geöffnet werden.",
"startSignedOut": "Abgemeldet",
"startOn": "Starte auf {instance}:",
"startWithGitHub": "Mit GitHub anmelden",
"startGitHubFailed": "Anmeldung mit GitHub fehlgeschlagen.",
"startNoGitHubAccount": "Kein Konto hier meldet sich mit diesem GitHub-Konto an.",
"gitHubCreateAccountInstead": "Erstelle stattdessen ein Konto mit deiner E-Mail-Adresse und einem Passwort.",
"gitHubSimpleLoginInstead": "Mit SimpleLogin kannst du stattdessen eins erstellen.",
"gitHubUsePassword": "Dieses Konto meldet sich jetzt mit seiner E-Mail-Adresse und seinem Passwort an, nicht mehr mit GitHub.",
"gitHubEnded": "Die Anmeldung mit GitHub ist auf dieser Instanz beendet.",
"startOtherInstance": "Andere Instanz verwenden",
"startNoWay": "{instance} bietet keinen Weg, ein Konto zu erstellen.",
"syncTitle": "Synchronisierung",
@ -70,7 +63,6 @@
"syncMismatchMore": "Mehr",
"startSignIn": "Anmelden",
"startCreateAccount": "Konto erstellen",
"startGitHubEnds": "Die Anmeldung mit GitHub endet am {date}.",
"passwordAddress": "E-Mail-Adresse",
"passwordPassword": "Passwort",
"passwordNewPassword": "Neues Passwort",
@ -158,7 +150,6 @@
"passwordResetStarted": "Dein Konto kann ab dem {date} zurückgesetzt werden. Wir haben dir eine E-Mail geschickt: Warst du es nicht, melde dich vorher mit deinem Passwort an, um es zu stoppen.",
"passwordSectionTitle": "Passwort und Authenticator",
"passwordSectionFailed": "Wie dein Konto sich anmeldet, konnte nicht gelesen werden.",
"passwordSectionGitHubEnds": "Die Anmeldung mit GitHub endet am {date}. Richte bis dahin ein Passwort ein, um dein Konto zu behalten.",
"passwordSectionSetUp": "Passwort einrichten",
"passwordSectionSetUpHint": "Melde dich mit deiner E-Mail-Adresse, einem Passwort und einer Authenticator-App an.",
"passwordSectionUnfinished": "Fertig einrichten: Authenticator hinzufügen",
@ -189,7 +180,6 @@
"passwordNoReset": "Für diese Adresse wartet kein Zurücksetzen.",
"passwordExists": "Das Konto dieser Adresse hat schon ein Passwort. Melde dich damit an oder komm zurück in dein Konto, falls du es vergessen hast.",
"passwordAddressTaken": "Diese Adresse gehört zu einem anderen Konto.",
"passwordNoAddress": "Dein Konto hat noch keine E-Mail-Adresse.",
"passwordNoPassword": "Dieses Konto hat kein Passwort.",
"passwordWrongCode": "Dieser Code stimmt nicht. Gib den ein, den deine App gerade zeigt.",
"passwordLastAuthenticator": "Dein letzter Authenticator kann nicht weg: Jede Anmeldung braucht einen.",
@ -220,11 +210,6 @@
"passwordAddressUnknown": "Gib die E-Mail-Adresse deines Kontos ein: Dorthin geht der Code.",
"passwordDeleteIntroByMail": "Dein Konto wird mit allem darin gelöscht, in jedem Dienst dieser Instanz. Keiner seiner Authenticators funktioniert gerade, daher bestätigt es ein Code per E-Mail: Gib dein Passwort ein, dann den Code, den wir an deine Adresse schicken.",
"passwordDeleteSendCode": "Code per E-Mail schicken",
"passwordSignInAgain": "Um einem Konto, das sich mit GitHub anmeldet, ein Passwort zu geben, melde dich erst wieder mit GitHub an: Es ist mehr als ein paar Minuten her.",
"passwordSignInWithGitHubAgain": "Wieder mit GitHub anmelden",
"passwordOfferTitle": "Richte ein Passwort ein",
"passwordOfferBody": "Die Anmeldung mit GitHub endet. Richte ein Passwort und eine Authenticator-App ein, um dein Konto zu behalten.",
"passwordOfferLater": "Später",
"passwordAuthenticatorPasswordIntro": "Bestätige zum Hinzufügen mit deinem Passwort, dass du es bist.",
"passwordWarningTitle": "Jemand hat dein Passwort ausprobiert",
"passwordWarningBody": "Am {date} hat ein bei deinem Konto angemeldetes Gerät zehnmal hintereinander ein falsches Passwort oder einen falschen Code gesendet und wurde abgemeldet. Warst du das nicht, hat vielleicht jemand anderes dieses Gerät oder seine Sitzung: Ändere dein Passwort.",
@ -236,7 +221,6 @@
"passwordResetsCallOff": "Absagen",
"passwordCallOffTitle": "Zurücksetzen absagen",
"passwordCallOffIntro": "Dein Passwort und ein Code sagen jedes anstehende Zurücksetzen ab.",
"passwordSectionGitHubEndsDeletes": "Die Anmeldung mit GitHub endet am {date}. Richte bis dahin ein Passwort ein: Ein Konto ohne wird 30 Tage später mit allem darin gelöscht.",
"passwordSignedInBefore": "Nur ein seitdem angemeldetes Gerät kann diese Warnung ausblenden: Melde dich auf diesem neu an oder ändere dein Passwort.",
"simpleLoginContinue": "Weiter mit SimpleLogin",
"simpleLoginNote": "SimpleLogin ist die Anmeldung von Proton, und ein Proton-Konto funktioniert dort auch. SimpleLogin erfährt, dass du {instance} nutzt, und {instance} speichert nur deine SimpleLogin-ID: Dein Name schlägt einmal ein Handle vor, sonst wird nichts gespeichert.",

View file

@ -154,19 +154,6 @@
}
}
},
"startWithGitHub": "Sign in with GitHub",
"startGitHubFailed": "Couldn't sign in with GitHub.",
"startNoGitHubAccount": "No account here signs in with this GitHub account.",
"gitHubCreateAccountInstead": "Create an account with your email address and a password instead.",
"@gitHubCreateAccountInstead": {
"description": "Said after a GitHub account nobody on the instance signs in with, where people sign in with a password and GitHub is added to nobody (ADR 0017)."
},
"gitHubSimpleLoginInstead": "Continue with SimpleLogin to create one instead.",
"@gitHubSimpleLoginInstead": {
"description": "Said after a GitHub account nobody on the instance signs in with, where the instance offers SimpleLogin (ADR 0020): GitHub makes no account."
},
"gitHubUsePassword": "This account signs in with its email address and password now, not with GitHub.",
"gitHubEnded": "Sign-in with GitHub has ended on this instance.",
"startOtherInstance": "Use another instance",
"startNoWay": "{instance} offers no way to create an account.",
"@startNoWay": {
@ -229,14 +216,6 @@
"syncMismatchMore": "More",
"startSignIn": "Sign in",
"startCreateAccount": "Create an account",
"startGitHubEnds": "Sign-in with GitHub ends on {date}.",
"@startGitHubEnds": {
"placeholders": {
"date": {
"type": "String"
}
}
},
"passwordAddress": "Email address",
"passwordPassword": "Password",
"passwordNewPassword": "New password",
@ -394,14 +373,6 @@
},
"passwordSectionTitle": "Password and authenticator",
"passwordSectionFailed": "Couldn't read how your account signs in.",
"passwordSectionGitHubEnds": "Sign-in with GitHub ends on {date}. Set up a password before then to keep your account.",
"@passwordSectionGitHubEnds": {
"placeholders": {
"date": {
"type": "String"
}
}
},
"passwordSectionSetUp": "Set up a password",
"passwordSectionSetUpHint": "Sign in with your email address, a password and an authenticator app.",
"passwordSectionUnfinished": "Finish setting up: add an authenticator",
@ -446,7 +417,6 @@
"passwordNoReset": "No reset waits for this address.",
"passwordExists": "This address's account has a password already. Sign in with it, or get back in if you've forgotten it.",
"passwordAddressTaken": "This address belongs to another account.",
"passwordNoAddress": "Your account has no email address yet.",
"passwordNoPassword": "This account has no password.",
"passwordWrongCode": "This code isn't right. Enter the one your app shows now.",
"passwordLastAuthenticator": "Your last authenticator can't go: every sign-in needs one.",
@ -498,11 +468,6 @@
"passwordAddressUnknown": "Enter your account's email address: the code goes there.",
"passwordDeleteIntroByMail": "Your account goes with everything in it, in every service of this instance. None of its authenticators works now, so a code by mail confirms it: type your password, then the code we email to your address.",
"passwordDeleteSendCode": "Email me a code",
"passwordSignInAgain": "To give an account that signs in with GitHub its password, sign in with GitHub again first: it has been more than a few minutes.",
"passwordSignInWithGitHubAgain": "Sign in with GitHub again",
"passwordOfferTitle": "Set up a password",
"passwordOfferBody": "Sign-in with GitHub is ending. Set up a password and an authenticator app to keep your account.",
"passwordOfferLater": "Later",
"passwordAuthenticatorPasswordIntro": "To add it, confirm it is you with your password.",
"passwordWarningTitle": "Someone tried your password",
"passwordWarningBody": "On {date}, a device signed in to your account sent a wrong password or code ten times in a row, and was signed out. If that wasn't you, someone else may have that device or its session: change your password.",
@ -535,14 +500,6 @@
"passwordResetsCallOff": "Call it off",
"passwordCallOffTitle": "Call off the reset",
"passwordCallOffIntro": "Your password and a code call off every reset waiting.",
"passwordSectionGitHubEndsDeletes": "Sign-in with GitHub ends on {date}. Set up a password before then: an account without one is deleted 30 days later, with everything in it.",
"@passwordSectionGitHubEndsDeletes": {
"placeholders": {
"date": {
"type": "String"
}
}
},
"passwordSignedInBefore": "Only a device signed in since then can dismiss this warning: sign in again on this one, or change your password.",
"simpleLoginContinue": "Continue with SimpleLogin",
"simpleLoginNote": "SimpleLogin is Proton's sign-in, and a Proton account works there too. It learns that you use {instance}, which keeps only your SimpleLogin ID: your name suggests a handle once, and nothing else is kept.",

View file

@ -367,48 +367,6 @@ abstract class HalkyonAppLocalizations {
/// **'Start on {instance}:'**
String startOn(String instance);
/// No description provided for @startWithGitHub.
///
/// In en, this message translates to:
/// **'Sign in with GitHub'**
String get startWithGitHub;
/// No description provided for @startGitHubFailed.
///
/// In en, this message translates to:
/// **'Couldn\'t sign in with GitHub.'**
String get startGitHubFailed;
/// No description provided for @startNoGitHubAccount.
///
/// In en, this message translates to:
/// **'No account here signs in with this GitHub account.'**
String get startNoGitHubAccount;
/// Said after a GitHub account nobody on the instance signs in with, where people sign in with a password and GitHub is added to nobody (ADR 0017).
///
/// In en, this message translates to:
/// **'Create an account with your email address and a password instead.'**
String get gitHubCreateAccountInstead;
/// Said after a GitHub account nobody on the instance signs in with, where the instance offers SimpleLogin (ADR 0020): GitHub makes no account.
///
/// In en, this message translates to:
/// **'Continue with SimpleLogin to create one instead.'**
String get gitHubSimpleLoginInstead;
/// No description provided for @gitHubUsePassword.
///
/// In en, this message translates to:
/// **'This account signs in with its email address and password now, not with GitHub.'**
String get gitHubUsePassword;
/// No description provided for @gitHubEnded.
///
/// In en, this message translates to:
/// **'Sign-in with GitHub has ended on this instance.'**
String get gitHubEnded;
/// No description provided for @startOtherInstance.
///
/// In en, this message translates to:
@ -523,12 +481,6 @@ abstract class HalkyonAppLocalizations {
/// **'Create an account'**
String get startCreateAccount;
/// No description provided for @startGitHubEnds.
///
/// In en, this message translates to:
/// **'Sign-in with GitHub ends on {date}.'**
String startGitHubEnds(String date);
/// No description provided for @passwordAddress.
///
/// In en, this message translates to:
@ -1051,12 +1003,6 @@ abstract class HalkyonAppLocalizations {
/// **'Couldn\'t read how your account signs in.'**
String get passwordSectionFailed;
/// No description provided for @passwordSectionGitHubEnds.
///
/// In en, this message translates to:
/// **'Sign-in with GitHub ends on {date}. Set up a password before then to keep your account.'**
String passwordSectionGitHubEnds(String date);
/// No description provided for @passwordSectionSetUp.
///
/// In en, this message translates to:
@ -1237,12 +1183,6 @@ abstract class HalkyonAppLocalizations {
/// **'This address belongs to another account.'**
String get passwordAddressTaken;
/// No description provided for @passwordNoAddress.
///
/// In en, this message translates to:
/// **'Your account has no email address yet.'**
String get passwordNoAddress;
/// No description provided for @passwordNoPassword.
///
/// In en, this message translates to:
@ -1423,36 +1363,6 @@ abstract class HalkyonAppLocalizations {
/// **'Email me a code'**
String get passwordDeleteSendCode;
/// No description provided for @passwordSignInAgain.
///
/// In en, this message translates to:
/// **'To give an account that signs in with GitHub its password, sign in with GitHub again first: it has been more than a few minutes.'**
String get passwordSignInAgain;
/// No description provided for @passwordSignInWithGitHubAgain.
///
/// In en, this message translates to:
/// **'Sign in with GitHub again'**
String get passwordSignInWithGitHubAgain;
/// No description provided for @passwordOfferTitle.
///
/// In en, this message translates to:
/// **'Set up a password'**
String get passwordOfferTitle;
/// No description provided for @passwordOfferBody.
///
/// In en, this message translates to:
/// **'Sign-in with GitHub is ending. Set up a password and an authenticator app to keep your account.'**
String get passwordOfferBody;
/// No description provided for @passwordOfferLater.
///
/// In en, this message translates to:
/// **'Later'**
String get passwordOfferLater;
/// No description provided for @passwordAuthenticatorPasswordIntro.
///
/// In en, this message translates to:
@ -1519,12 +1429,6 @@ abstract class HalkyonAppLocalizations {
/// **'Your password and a code call off every reset waiting.'**
String get passwordCallOffIntro;
/// No description provided for @passwordSectionGitHubEndsDeletes.
///
/// In en, this message translates to:
/// **'Sign-in with GitHub ends on {date}. Set up a password before then: an account without one is deleted 30 days later, with everything in it.'**
String passwordSectionGitHubEndsDeletes(String date);
/// No description provided for @passwordSignedInBefore.
///
/// In en, this message translates to:

View file

@ -175,32 +175,6 @@ class HalkyonAppLocalizationsDe extends HalkyonAppLocalizations {
return 'Starte auf $instance:';
}
@override
String get startWithGitHub => 'Mit GitHub anmelden';
@override
String get startGitHubFailed => 'Anmeldung mit GitHub fehlgeschlagen.';
@override
String get startNoGitHubAccount =>
'Kein Konto hier meldet sich mit diesem GitHub-Konto an.';
@override
String get gitHubCreateAccountInstead =>
'Erstelle stattdessen ein Konto mit deiner E-Mail-Adresse und einem Passwort.';
@override
String get gitHubSimpleLoginInstead =>
'Mit SimpleLogin kannst du stattdessen eins erstellen.';
@override
String get gitHubUsePassword =>
'Dieses Konto meldet sich jetzt mit seiner E-Mail-Adresse und seinem Passwort an, nicht mehr mit GitHub.';
@override
String get gitHubEnded =>
'Die Anmeldung mit GitHub ist auf dieser Instanz beendet.';
@override
String get startOtherInstance => 'Andere Instanz verwenden';
@ -278,11 +252,6 @@ class HalkyonAppLocalizationsDe extends HalkyonAppLocalizations {
@override
String get startCreateAccount => 'Konto erstellen';
@override
String startGitHubEnds(String date) {
return 'Die Anmeldung mit GitHub endet am $date.';
}
@override
String get passwordAddress => 'E-Mail-Adresse';
@ -597,11 +566,6 @@ class HalkyonAppLocalizationsDe extends HalkyonAppLocalizations {
String get passwordSectionFailed =>
'Wie dein Konto sich anmeldet, konnte nicht gelesen werden.';
@override
String passwordSectionGitHubEnds(String date) {
return 'Die Anmeldung mit GitHub endet am $date. Richte bis dahin ein Passwort ein, um dein Konto zu behalten.';
}
@override
String get passwordSectionSetUp => 'Passwort einrichten';
@ -713,9 +677,6 @@ class HalkyonAppLocalizationsDe extends HalkyonAppLocalizations {
String get passwordAddressTaken =>
'Diese Adresse gehört zu einem anderen Konto.';
@override
String get passwordNoAddress => 'Dein Konto hat noch keine E-Mail-Adresse.';
@override
String get passwordNoPassword => 'Dieses Konto hat kein Passwort.';
@ -834,23 +795,6 @@ class HalkyonAppLocalizationsDe extends HalkyonAppLocalizations {
@override
String get passwordDeleteSendCode => 'Code per E-Mail schicken';
@override
String get passwordSignInAgain =>
'Um einem Konto, das sich mit GitHub anmeldet, ein Passwort zu geben, melde dich erst wieder mit GitHub an: Es ist mehr als ein paar Minuten her.';
@override
String get passwordSignInWithGitHubAgain => 'Wieder mit GitHub anmelden';
@override
String get passwordOfferTitle => 'Richte ein Passwort ein';
@override
String get passwordOfferBody =>
'Die Anmeldung mit GitHub endet. Richte ein Passwort und eine Authenticator-App ein, um dein Konto zu behalten.';
@override
String get passwordOfferLater => 'Später';
@override
String get passwordAuthenticatorPasswordIntro =>
'Bestätige zum Hinzufügen mit deinem Passwort, dass du es bist.';
@ -892,11 +836,6 @@ class HalkyonAppLocalizationsDe extends HalkyonAppLocalizations {
String get passwordCallOffIntro =>
'Dein Passwort und ein Code sagen jedes anstehende Zurücksetzen ab.';
@override
String passwordSectionGitHubEndsDeletes(String date) {
return 'Die Anmeldung mit GitHub endet am $date. Richte bis dahin ein Passwort ein: Ein Konto ohne wird 30 Tage später mit allem darin gelöscht.';
}
@override
String get passwordSignedInBefore =>
'Nur ein seitdem angemeldetes Gerät kann diese Warnung ausblenden: Melde dich auf diesem neu an oder ändere dein Passwort.';

View file

@ -173,31 +173,6 @@ class HalkyonAppLocalizationsEn extends HalkyonAppLocalizations {
return 'Start on $instance:';
}
@override
String get startWithGitHub => 'Sign in with GitHub';
@override
String get startGitHubFailed => 'Couldn\'t sign in with GitHub.';
@override
String get startNoGitHubAccount =>
'No account here signs in with this GitHub account.';
@override
String get gitHubCreateAccountInstead =>
'Create an account with your email address and a password instead.';
@override
String get gitHubSimpleLoginInstead =>
'Continue with SimpleLogin to create one instead.';
@override
String get gitHubUsePassword =>
'This account signs in with its email address and password now, not with GitHub.';
@override
String get gitHubEnded => 'Sign-in with GitHub has ended on this instance.';
@override
String get startOtherInstance => 'Use another instance';
@ -274,11 +249,6 @@ class HalkyonAppLocalizationsEn extends HalkyonAppLocalizations {
@override
String get startCreateAccount => 'Create an account';
@override
String startGitHubEnds(String date) {
return 'Sign-in with GitHub ends on $date.';
}
@override
String get passwordAddress => 'Email address';
@ -584,11 +554,6 @@ class HalkyonAppLocalizationsEn extends HalkyonAppLocalizations {
String get passwordSectionFailed =>
'Couldn\'t read how your account signs in.';
@override
String passwordSectionGitHubEnds(String date) {
return 'Sign-in with GitHub ends on $date. Set up a password before then to keep your account.';
}
@override
String get passwordSectionSetUp => 'Set up a password';
@ -697,9 +662,6 @@ class HalkyonAppLocalizationsEn extends HalkyonAppLocalizations {
@override
String get passwordAddressTaken => 'This address belongs to another account.';
@override
String get passwordNoAddress => 'Your account has no email address yet.';
@override
String get passwordNoPassword => 'This account has no password.';
@ -816,23 +778,6 @@ class HalkyonAppLocalizationsEn extends HalkyonAppLocalizations {
@override
String get passwordDeleteSendCode => 'Email me a code';
@override
String get passwordSignInAgain =>
'To give an account that signs in with GitHub its password, sign in with GitHub again first: it has been more than a few minutes.';
@override
String get passwordSignInWithGitHubAgain => 'Sign in with GitHub again';
@override
String get passwordOfferTitle => 'Set up a password';
@override
String get passwordOfferBody =>
'Sign-in with GitHub is ending. Set up a password and an authenticator app to keep your account.';
@override
String get passwordOfferLater => 'Later';
@override
String get passwordAuthenticatorPasswordIntro =>
'To add it, confirm it is you with your password.';
@ -874,11 +819,6 @@ class HalkyonAppLocalizationsEn extends HalkyonAppLocalizations {
String get passwordCallOffIntro =>
'Your password and a code call off every reset waiting.';
@override
String passwordSectionGitHubEndsDeletes(String date) {
return 'Sign-in with GitHub ends on $date. Set up a password before then: an account without one is deleted 30 days later, with everything in it.';
}
@override
String get passwordSignedInBefore =>
'Only a device signed in since then can dismiss this warning: sign in again on this one, or change your password.';

View file

@ -4,7 +4,6 @@ import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:halkyon_app/src/auth/data/auth_repository.dart';
import 'package:halkyon_app/src/auth/model/auth_status.dart';
import 'package:halkyon_app/src/auth/model/github_sign_in.dart';
/// Keeps a person signed in: follows who is signed in, and signs out on
/// request. Nobody signed in (at a first start, after signing out or
@ -16,13 +15,6 @@ class SessionBloc(final AuthRepository _auth)
extends Bloc<SessionEvent, SessionState> {
this : super(const SessionStateLoading()) {
on<SessionEventStarted>(_onStarted);
on<SessionEventGitHubRequested>(_onGitHubRequested);
on<SessionEventPasswordOfferDone>(
(event, emit) => emit(_stateOf(_auth.status)),
);
on<SessionEventSignedOut>(
(event, emit) => emit(SessionStateSignedOut(problem: event.problem)),
);
on<SessionEventSignOutRequested>(_onSignOutRequested);
on<SessionEventDeleteRequested>(_onDeleteRequested);
}
@ -44,48 +36,6 @@ class SessionBloc(final AuthRepository _auth)
await emit.forEach<AuthStatus>(_auth.statusChanges, onData: _stateOf);
}
/// Signing in with GitHub: the GitHub account's person, if there is one.
Future<void> _onGitHubRequested(
SessionEventGitHubRequested event,
Emitter<SessionState> emit,
) async {
emit(const SessionStateSignedOut(busy: true));
// The person signed in with GitHub is offered a password first (ADR
// 0017, D2: "at their next sign-in"), while that sign-in is recent
// enough for the instance to take one (the fourth reading).
_offerPassword = true;
try {
if (!await _auth.switchToGitHub()) {
_offerPassword = false;
emit(const SessionStateSignedOut());
}
// Signed in: the status change shows the offer, then the person's
// app.
} on GitHubSignInFailed catch (e) {
_offerPassword = false;
emit(
SessionStateSignedOut(
problem: switch (e.problem) {
GitHubSignInProblem.unknownAccount =>
SignedOutProblem.noGitHubAccount,
GitHubSignInProblem.usePassword => SignedOutProblem.usePassword,
GitHubSignInProblem.ended => SignedOutProblem.gitHubEnded,
GitHubSignInProblem.deprecated ||
GitHubSignInProblem.refused => SignedOutProblem.failed,
},
),
);
} on Object catch (error, st) {
_offerPassword = false;
addError(error, st);
emit(const SessionStateSignedOut(problem: SignedOutProblem.failed));
}
}
/// Whether the next person signed in comes from a GitHub sign-in, and is
/// offered a password before their app ([SessionStateGitHubSignedIn]).
bool _offerPassword = false;
Future<void> _onSignOutRequested(
SessionEventSignOutRequested event,
Emitter<SessionState> emit,
@ -139,15 +89,9 @@ class SessionBloc(final AuthRepository _auth)
// Nothing works in an anonymous account any more (D25): only one that
// began before the app started could be signed in, and it has ended.
SignedIn(isIdentified: false) => const SessionStateSignedOut(),
SignedIn() when _offerPassword => _offered(status),
SignedIn() => SessionStateActive(status),
SignedOut() => const SessionStateSignedOut(),
};
SessionState _offered(SignedIn person) {
_offerPassword = false;
return SessionStateGitHubSignedIn(person);
}
}
/// What happens to the session.
@ -160,20 +104,6 @@ sealed class const SessionEvent() with Equatable {
/// session left from before (ADR 0020, D25).
final class const SessionEventStarted() extends SessionEvent;
/// Sign in with GitHub, once signed out.
final class const SessionEventGitHubRequested() extends SessionEvent;
/// Signed out, [problem] if any saying why the last way to start failed.
final class const SessionEventSignedOut({final SignedOutProblem? problem})
extends SessionEvent {
@override
List<Object?> get props => [problem];
}
/// The password offered after a GitHub sign-in was set up, put off, or not
/// needed: on to the person's app.
final class const SessionEventPasswordOfferDone() extends SessionEvent;
/// The person confirmed that they want to sign out.
final class const SessionEventSignOutRequested() extends SessionEvent;
@ -229,42 +159,8 @@ enum SessionProblem() {
deleteFailed,
}
/// [person] signed in with GitHub just now: before their app, a password
/// is offered where the instance offers one and they have none (ADR 0017,
/// D2), while the sign-in is recent enough to set it.
final class const SessionStateGitHubSignedIn(
/// Who signed in.
final SignedIn person,
) extends SessionState {
@override
List<Object?> get props => [person];
}
/// Nobody is signed in: the app asks how to start.
final class const SessionStateSignedOut({
/// Whether a way to start is under way.
final bool busy = false,
/// Why the last way to start failed; null for none.
final SignedOutProblem? problem,
}) extends SessionState {
final class const SessionStateSignedOut() extends SessionState {
@override
List<Object?> get props => [busy, problem];
}
/// Why a way to start failed.
enum SignedOutProblem() {
/// No account has the GitHub account: GitHub makes none (ADR 0020, D25),
/// so the person creates one where the instance offers a way.
noGitHubAccount,
/// The GitHub account's person signs in with their address and password
/// now (ADR 0017).
usePassword,
/// GitHub signs nobody in here any more.
gitHubEnded,
/// Anything else, as no connection.
failed,
List<Object?> get props => const [];
}

View file

@ -18,15 +18,6 @@ abstract interface class AuthGateway() {
/// and so on all their devices, and forgets the session here.
Future<void> deleteAccount();
/// Whether this instance signs people in with GitHub: those who have it
/// already, as GitHub is added to nobody any more (ADR 0020, D25).
Future<bool> gitHubSignInOffered();
/// Authorizes with GitHub in the browser and signs this device in as the
/// person who has that GitHub account; false when the person cancelled
/// on GitHub.
Future<bool> switchToGitHub();
/// How the signed-in person signs in.
Future<SignInWays> signInWays();

View file

@ -50,12 +50,6 @@ class AuthRepository(
/// and all their devices; the UI asks first.
Future<void> deleteAccount() => _gateway.deleteAccount();
/// Whether this instance signs in with GitHub those who have it.
Future<bool> canSignInWithGitHub() => _gateway.gitHubSignInOffered();
/// Signs this device in as the person who has the GitHub account.
Future<bool> switchToGitHub() => _gateway.switchToGitHub();
/// How the signed-in person signs in.
Future<SignInWays> signInWays() => _gateway.signInWays();

View file

@ -1,19 +1,10 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter_web_auth_2_platform_interface/flutter_web_auth_2_platform_interface.dart';
import 'package:halkyon_app/src/auth/data/auth_gateway.dart';
import 'package:halkyon_app/src/auth/model/auth_status.dart';
import 'package:halkyon_app/src/auth/model/github_sign_in.dart';
import 'package:halkyon_app/src/auth/model/session_grant.dart';
import 'package:halkyon_app/src/auth/model/sign_in_ways.dart';
import 'package:halkyon_app/src/base/base_client.dart';
import 'package:halkyon_base_client/halkyon_base_client.dart'
show GitHubSignInRefusal, GitHubSignInRefusedException;
import 'package:halkyon_base_client/halkyon_base_client.dart' as base_client;
import 'package:halkyon_oidc_flutter/halkyon_oidc_flutter.dart'
show LoopbackWebAuth;
import 'package:http/http.dart' as http;
import 'package:serverpod_auth_idp_flutter/serverpod_auth_idp_flutter.dart';
/// [AuthGateway] on top of an app's Serverpod client and its session
@ -24,22 +15,6 @@ import 'package:serverpod_auth_idp_flutter/serverpod_auth_idp_flutter.dart';
class ServerpodAuthGateway(
final ServerpodClientShared _client, {
required final base_client.Caller _base,
/// The app's own URL scheme, which GitHub sends the browser back to on
/// Android, iOS and macOS (`app.halkyon.learn`), registered with the
/// system for the app and in the instance's GitHub OAuth app.
required final String callbackScheme,
/// The port on 127.0.0.1 the app listens on for GitHub's answer on Linux
/// and Windows.
final int loopbackPort = 43823,
/// The app's name, as the browser's page says to go back to it.
required final String appName,
/// What the clients it makes for itself send through (to sign in as
/// another person); tests give a fake one.
final http.Client Function()? _httpClient,
}) implements AuthGateway {
this {
_client.auth.authInfoListenable.addListener(_onAuthInfoChanged);
@ -51,28 +26,6 @@ class ServerpodAuthGateway(
/// an anonymous account's, left from before they went.
static const _identifiedScope = 'identified';
/// Where GitHub sends the browser back after authorizing: the app's own
/// scheme on Android, iOS and macOS; on Linux and Windows a loopback address
/// the app listens on meanwhile (GitHub accepts any port there, so the
/// OAuth app registers `http://127.0.0.1/github-callback`). There,
/// [LoopbackWebAuth] takes the whole address, to answer only its path.
late final _gitHubCallback = Platform.isLinux || Platform.isWindows
? (
redirectUri: 'http://127.0.0.1:$loopbackPort/github-callback',
urlScheme: 'http://127.0.0.1:$loopbackPort/github-callback',
)
: (
redirectUri: '$callbackScheme://github-callback',
urlScheme: callbackScheme,
);
/// The browser half of signing in with GitHub on Linux and Windows, in
/// place of flutter_web_auth_2's own, which stops waiting whenever the
/// window regains focus. Elsewhere, the package's.
late final _loopback = Platform.isLinux || Platform.isWindows
? LoopbackWebAuth(appName: appName)
: null;
@override
AuthStatus get status => switch (_client.auth.authInfo) {
null => const SignedOut(),
@ -98,85 +51,16 @@ class ServerpodAuthGateway(
await _client.auth.updateSignedInUser(null);
}
@override
Future<bool> gitHubSignInOffered() async =>
(await _base.instance.basics()).gitHubClientId != null;
/// The client ID of this instance's GitHub OAuth app; null when GitHub
/// sign-in is off on this instance.
Future<String?> _gitHubClientId() async =>
(await _base.instance.basics()).gitHubClientId;
@override
Future<bool> switchToGitHub() async {
final authorization = await _authorizeWithGitHub();
if (authorization == null) {
return false;
}
// Serverpod issues no session for another person inside a signed-in
// call, so a client without a session asks for it.
final signedOut = _clientOfItsOwn();
final AuthSuccess person;
try {
person = await signedOut.modules.gitHubSignIn.signIn(
code: authorization.code,
codeVerifier: authorization.codeVerifier,
redirectUri: authorization.redirectUri,
);
} on GitHubSignInRefusedException catch (error) {
throw _gitHubFailure(error);
} finally {
signedOut.close();
}
await _client.auth.updateSignedInUser(person);
return true;
}
/// GitHub's authorization in the browser, or null when the person
/// cancelled it.
Future<GitHubSignInResult?> _authorizeWithGitHub() async {
final clientId = await _gitHubClientId();
if (clientId == null) {
throw StateError('This instance offers no GitHub sign-in.');
}
await GitHubSignInService.instance.ensureInitialized(
clientId: clientId,
redirectUri: _gitHubCallback.redirectUri,
callbackUrlScheme: _gitHubCallback.urlScheme,
// On Linux and Windows the system browser and a loopback address, not
// an embedded webview, as the OAuth best practice for native apps
// (RFC 8252) asks.
useWebview: false,
);
if (_loopback case final loopback?) {
// Here, after the package has put its own in place at startup.
FlutterWebAuth2Platform.instance = loopback;
}
try {
return await GitHubSignInService.instance.signIn();
} on OAuth2PkceUserCancelledException {
return null;
}
}
@override
Future<SignInWays> signInWays() async {
final ways = await _base.account.signInWays();
return SignInWays(gitHub: ways.gitHub, email: ways.email);
return SignInWays(email: ways.email);
}
@override
Future<void> useSession(SessionGrant grant) =>
_client.auth.updateSignedInUser(grant.session as AuthSuccess);
/// The instance's refusal of a GitHub sign-in as [GitHubSignInFailed].
static GitHubSignInFailed _gitHubFailure(GitHubSignInRefusedException e) =>
GitHubSignInFailed(gitHubSignInProblemOf(e.reason));
/// A client of the instance's halkyon_base, holding no session.
BaseClient _clientOfItsOwn() =>
BaseClient(_client.host, httpClientOverride: _httpClient?.call());
/// Stops listening; the owner calls it when the app shuts down.
Future<void> dispose() async {
_client.auth.authInfoListenable.removeListener(_onAuthInfoChanged);
@ -185,17 +69,3 @@ class ServerpodAuthGateway(
void _onAuthInfoChanged() => _changes.add(status);
}
/// The instance's refusal [reason] of a GitHub sign-in as the app's problem,
/// which the screens put into words.
GitHubSignInProblem gitHubSignInProblemOf(GitHubSignInRefusal reason) =>
switch (reason) {
GitHubSignInRefusal.unknownAccount => GitHubSignInProblem.unknownAccount,
GitHubSignInRefusal.usePassword => GitHubSignInProblem.usePassword,
GitHubSignInRefusal.deprecated => GitHubSignInProblem.deprecated,
GitHubSignInRefusal.ended => GitHubSignInProblem.ended,
GitHubSignInRefusal.unavailable ||
GitHubSignInRefusal.takenByAnotherPerson ||
GitHubSignInRefusal.otherGitHubAccountLinked =>
GitHubSignInProblem.refused,
};

View file

@ -1,28 +0,0 @@
/// Why signing in with GitHub didn't go through, beyond a cancel.
enum GitHubSignInProblem() {
/// No person has this GitHub account: GitHub makes no account (ADR 0020,
/// D25).
unknownAccount,
/// The account signs in with its address and password now, not with
/// GitHub (ADR 0017).
usePassword,
/// GitHub is added to nobody where people sign in with a password
/// (ADR 0017, D2).
deprecated,
/// GitHub signs nobody in here any more: its time ended.
ended,
/// The instance refused it otherwise: it offers no GitHub sign-in, or the
/// person already has a different GitHub account.
refused,
}
/// Signing in with GitHub didn't go through, for [problem].
final class const GitHubSignInFailed(final GitHubSignInProblem problem)
implements Exception {
@override
String toString() => 'GitHubSignInFailed(${problem.name})';
}

View file

@ -1,15 +1,12 @@
import 'package:equatable/equatable.dart';
/// How a person signs in, besides the sessions of their devices: with
/// GitHub, with an email address, both, or neither (with SimpleLogin alone,
/// which `SimpleLoginStandingCubit` tells).
final class const SignInWays({
required final bool gitHub,
required final bool email,
}) with Equatable {
/// Neither, as long as nothing was read.
static const none = SignInWays(gitHub: false, email: false);
/// How a person signs in, besides the sessions of their devices: with an
/// email address, or not (with SimpleLogin alone, which
/// `SimpleLoginStandingCubit` tells).
final class const SignInWays({required final bool email}) with Equatable {
/// No way, as long as nothing was read.
static const none = SignInWays(email: false);
@override
List<Object?> get props => [gitHub, email];
List<Object?> get props => [email];
}

View file

@ -1,16 +1,9 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:halkyon_app/l10n/halkyon_app_localizations.dart';
import 'package:halkyon_app/src/app/app_frame.dart';
import 'package:halkyon_app/src/app/app_scope.dart';
import 'package:halkyon_app/src/auth/bloc/session_bloc.dart';
import 'package:halkyon_app/src/auth/model/auth_status.dart';
import 'package:halkyon_app/src/auth/widget/start_screen.dart';
import 'package:halkyon_app/src/instance/instance_scope.dart';
import 'package:halkyon_app/src/password/bloc/account_setup_cubit.dart';
import 'package:halkyon_app/src/password/model/account_security.dart';
import 'package:halkyon_app/src/password/widget/account_setup_sheet.dart';
import 'package:halkyon_app/src/password/widget/password_texts.dart';
import 'package:halkyon_ui/halkyon_ui.dart';
import 'package:material_ui/material_ui.dart';
/// Shows the app of whoever is signed in, and asks how to start while
@ -49,13 +42,9 @@ class const SessionGate({
look: look(context),
home: AppFrameScope.of(context).launchScreen,
),
final SessionStateSignedOut signedOut => PlainApp(
SessionStateSignedOut() => PlainApp(
look: look(context),
home: StartScreen(state: signedOut),
),
SessionStateGitHubSignedIn() => PlainApp(
look: look(context),
home: const _PasswordOffer(),
home: const StartScreen(),
),
SessionStateActive(:final person) => KeyedSubtree(
key: ValueKey(person.authUserId),
@ -65,110 +54,3 @@ class const SessionGate({
),
);
}
/// After a GitHub sign-in, a password for an account that has none, where
/// the instance offers them (ADR 0017, D2: GitHub sign-in ends), while the
/// sign-in is recent enough for the instance to take one; else, or once
/// set up or put off, on to the person's app.
class const _PasswordOffer() extends StatefulWidget {
@override
State<_PasswordOffer> createState() => _PasswordOfferState();
}
class _PasswordOfferState() extends State<_PasswordOffer> {
late final Future<AccountSecurity?> _security =
AppScope.of(
context,
).passwords.security().then<AccountSecurity?>(
(security) => security,
onError: (Object _) => null,
);
void _done() =>
context.read<SessionBloc>().add(const SessionEventPasswordOfferDone());
Future<void> _setUp(AccountSecurity security) async {
final address = security.hasAddress
? await AppScope.of(context).auth.emailAddress()
: null;
if (!mounted) {
return;
}
await showAccountSetupSheet(
context,
mode: AccountSetupMode.setPassword,
instance:
InstanceScope.maybeOf(context)?.choice.current?.name ??
AppScope.of(context).serverName,
hasAddress: security.hasAddress,
hasHandle: security.hasHandle,
address: address,
);
if (mounted) {
_done();
}
}
@override
Widget build(BuildContext context) {
final l10n = HalkyonAppLocalizations.of(context);
final theme = Theme.of(context);
return FutureBuilder<AccountSecurity?>(
future: _security,
builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
return const Scaffold(body: Center(child: AppLoadingIndicator()));
}
final security = snapshot.data;
if (security == null || !security.offered || security.hasPassword) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
_done();
}
});
return const Scaffold(body: Center(child: AppLoadingIndicator()));
}
return Scaffold(
body: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 480),
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
l10n.passwordOfferTitle,
style: theme.textTheme.headlineSmall,
),
const SizedBox(height: 12),
Text(switch (security.gitHubEnds) {
final ends? =>
(security.hasAddress
? l10n.passwordSectionGitHubEnds
: l10n.passwordSectionGitHubEndsDeletes)(
passwordMoment(context, ends),
),
null => l10n.passwordOfferBody,
}),
const SizedBox(height: 24),
FilledButton(
onPressed: () => _setUp(security),
child: Text(l10n.passwordSectionSetUp),
),
const SizedBox(height: 8),
TextButton(
onPressed: _done,
child: Text(l10n.passwordOfferLater),
),
],
),
),
),
),
);
},
);
}
}

View file

@ -1,8 +1,6 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:halkyon_app/l10n/halkyon_app_localizations.dart';
import 'package:halkyon_app/src/app/app_frame.dart';
import 'package:halkyon_app/src/app/app_scope.dart';
import 'package:halkyon_app/src/auth/bloc/session_bloc.dart';
import 'package:halkyon_app/src/instance/instance_scope.dart';
import 'package:halkyon_app/src/instance/widget/account_switcher_sheet.dart';
import 'package:halkyon_app/src/password/bloc/account_setup_cubit.dart';
@ -10,7 +8,6 @@ import 'package:halkyon_app/src/password/model/password_failed.dart';
import 'package:halkyon_app/src/password/widget/account_recovery_sheet.dart';
import 'package:halkyon_app/src/password/widget/account_setup_sheet.dart';
import 'package:halkyon_app/src/password/widget/password_sign_in_sheet.dart';
import 'package:halkyon_app/src/password/widget/password_texts.dart';
import 'package:halkyon_app/src/simplelogin/model/simplelogin_failed.dart';
import 'package:halkyon_app/src/simplelogin/widget/simplelogin_sheet.dart';
import 'package:halkyon_ui/halkyon_ui.dart';
@ -18,25 +15,16 @@ import 'package:material_ui/material_ui.dart';
/// How to start while nobody is signed in: on the instance in use, with
/// SimpleLogin where it offers it (ADR 0020), signing in with a password or
/// creating an account where it offers password sign-in (ADR 0017), with
/// GitHub where it offers it (saying when it ends), those who have it
/// alone; or on another instance. There is no starting without an account
/// (ADR 0020, D4): an instance offering no way to create one says so, and
/// one out of reach is asked again. Nothing is made until the person
/// chooses.
class const StartScreen({required final SessionStateSignedOut state, super.key})
extends StatefulWidget {
/// creating an account where it offers password sign-in (ADR 0017); or on
/// another instance. There is no starting without an account (ADR 0020,
/// D4): an instance offering no way to create one says so, and one out of
/// reach is asked again. Nothing is made until the person chooses.
class const StartScreen({super.key}) extends StatefulWidget {
@override
State<StartScreen> createState() => _StartScreenState();
}
typedef _Offers = ({
bool reached,
bool simpleLogin,
bool gitHub,
bool password,
DateTime? gitHubEnds,
});
typedef _Offers = ({bool reached, bool simpleLogin, bool password});
class _StartScreenState() extends State<StartScreen> {
late Future<_Offers> _offers = _ask();
@ -45,7 +33,7 @@ class _StartScreenState() extends State<StartScreen> {
/// nothing, and an instance none of them reached is out of reach.
Future<_Offers> _ask() async {
final dependencies = AppScope.of(context);
Future<({bool offered, DateTime? gitHubEnds})?> passwords() async {
Future<bool?> passwords() async {
try {
return await dependencies.passwords.offers();
} on PasswordFailed {
@ -61,33 +49,17 @@ class _StartScreenState() extends State<StartScreen> {
}
}
Future<bool?> gitHub() async {
try {
return await dependencies.auth.canSignInWithGitHub();
} on Exception {
return null;
}
}
final (simpleLoginOffered, gitHubOffered, password) = await (
final (simpleLoginOffered, passwordOffered) = await (
simpleLogin(),
gitHub(),
passwords(),
).wait;
return (
reached:
simpleLoginOffered != null ||
gitHubOffered != null ||
password != null,
reached: simpleLoginOffered != null || passwordOffered != null,
simpleLogin: simpleLoginOffered ?? false,
gitHub: gitHubOffered ?? false,
password: password?.offered ?? false,
gitHubEnds: password?.gitHubEnds,
password: passwordOffered ?? false,
);
}
void _send(SessionEvent event) => context.read<SessionBloc>().add(event);
/// Signs in with a password; a forgotten one leads to getting back in,
/// and a reset to finish to the account's setup. Signed in, the person's
/// app takes this screen's place.
@ -116,7 +88,6 @@ class _StartScreenState() extends State<StartScreen> {
final instance =
InstanceScope.maybeOf(context)?.choice.current?.name ??
AppScope.of(context).serverName;
final busy = widget.state.busy;
return Scaffold(
body: SafeArea(
child: Center(
@ -144,9 +115,7 @@ class _StartScreenState() extends State<StartScreen> {
if (InstanceScope.maybeOf(context) != null) ...[
const SizedBox(height: 24),
TextButton(
onPressed: busy
? null
: () => showAccountSwitcher(context),
onPressed: () => showAccountSwitcher(context),
child: Text(l10n.startOtherInstance),
),
],
@ -162,13 +131,7 @@ class _StartScreenState() extends State<StartScreen> {
],
);
}
final (
:reached,
:simpleLogin,
:gitHub,
:password,
:gitHubEnds,
) = asked;
final (:reached, :simpleLogin, :password) = asked;
if (!reached) {
return Column(
mainAxisSize: MainAxisSize.min,
@ -204,12 +167,8 @@ class _StartScreenState() extends State<StartScreen> {
],
if (simpleLogin) ...[
FilledButton(
onPressed: busy
? null
: () => showSimpleLoginSheet(
context,
instance: instance,
),
onPressed: () =>
showSimpleLoginSheet(context, instance: instance),
child: Text(l10n.simpleLoginContinue),
),
const SizedBox(height: 8),
@ -223,85 +182,25 @@ class _StartScreenState() extends State<StartScreen> {
if (password) ...[
if (simpleLogin)
OutlinedButton(
onPressed: busy ? null : () => _signIn(instance),
onPressed: () => _signIn(instance),
child: Text(l10n.startSignIn),
)
else
FilledButton(
onPressed: busy ? null : () => _signIn(instance),
onPressed: () => _signIn(instance),
child: Text(l10n.startSignIn),
),
const SizedBox(height: 8),
OutlinedButton(
onPressed: busy
? null
: () => showAccountSetupSheet(
context,
mode: AccountSetupMode.signUp,
instance: instance,
),
onPressed: () => showAccountSetupSheet(
context,
mode: AccountSetupMode.signUp,
instance: instance,
),
child: Text(l10n.startCreateAccount),
),
const SizedBox(height: 8),
],
if (gitHub) ...[
if (password || simpleLogin)
OutlinedButton(
onPressed: busy
? null
: () => _send(
const SessionEventGitHubRequested(),
),
child: busy
? const ButtonLoadingIndicator()
: Text(l10n.startWithGitHub),
)
else
FilledButton(
onPressed: busy
? null
: () => _send(
const SessionEventGitHubRequested(),
),
child: busy
? const ButtonLoadingIndicator()
: Text(l10n.startWithGitHub),
),
if (gitHubEnds case final ends?) ...[
const SizedBox(height: 4),
Text(
l10n.startGitHubEnds(passwordMoment(context, ends)),
textAlign: TextAlign.center,
style: note,
),
],
const SizedBox(height: 8),
],
if (widget.state.problem case final problem?) ...[
const SizedBox(height: 16),
Text(
switch (problem) {
// Whole sentences: GitHub makes no account (ADR
// 0020, D25), so one is created in its place,
// where the instance offers a way.
SignedOutProblem.noGitHubAccount => [
l10n.startNoGitHubAccount,
if (simpleLogin)
l10n.gitHubSimpleLoginInstead
else if (password)
l10n.gitHubCreateAccountInstead,
].join(' '),
SignedOutProblem.usePassword =>
l10n.gitHubUsePassword,
SignedOutProblem.gitHubEnded => l10n.gitHubEnded,
SignedOutProblem.failed => l10n.startGitHubFailed,
},
textAlign: TextAlign.center,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.error,
),
),
],
...otherInstance,
],
);

View file

@ -15,10 +15,9 @@ enum AccountSetupMode() {
/// and no password yet): put in place once its authenticator is set up.
signUp,
/// A password for the signed-in person (signing in with GitHub): they
/// keep everything. A session alone sets
/// none: an address of theirs takes a code mailed to it, GitHub a recent
/// sign-in (the fourth reading).
/// A password for the signed-in person, who has an address: they keep
/// everything. A session alone sets none: the address takes a code mailed
/// to it.
setPassword,
/// The reset of a whole account, once due: a new password, and new keys
@ -138,11 +137,6 @@ class AccountSetupCubit(
/// person's own); null where the setup asks for it.
final String? address,
/// Whether the signed-in person has an address already, which the code
/// then goes to as proof that they are its owner
/// ([AccountSetupMode.setPassword], [MailCodePurpose.firstPassword]).
final bool _hasAddress = false,
/// Whether the signed-in person has a handle already
/// ([AccountSetupMode.setPassword]); one without takes one with their
/// first password (ADR 0020, D28).
@ -303,9 +297,9 @@ class AccountSetupCubit(
/// What the codes mailed here are for.
MailCodePurpose get _codePurpose => switch (_mode) {
AccountSetupMode.finishReset => MailCodePurpose.accountReset,
AccountSetupMode.setPassword when _hasAddress =>
MailCodePurpose.firstPassword,
_ => MailCodePurpose.signUp,
// The signed-in person's own address, the code their proof of it.
AccountSetupMode.setPassword => MailCodePurpose.firstPassword,
AccountSetupMode.signUp => MailCodePurpose.signUp,
};
/// Mails a new code.

View file

@ -17,7 +17,6 @@ final class const PasswordSignedIn({
/// How the signed-in person signs in, as the instance knows it.
typedef PasswordWays = ({
bool gitHub,
bool email,
bool password,
@ -61,9 +60,8 @@ abstract interface class PasswordGateway() {
/// gets nothing the real one takes.
String get instanceUrl;
/// Whether the instance offers password sign-in, and until when GitHub
/// sign-in lasts there (null for no end set).
Future<({bool offered, DateTime? gitHubEnds})> offers();
/// Whether the instance offers password sign-in.
Future<bool> offers();
/// Mails a code for [purpose] to [address], worded in [language]; the
/// request it answers. Only [purpose] takes it.

View file

@ -132,9 +132,8 @@ class PasswordRepository(
unawaited(_changes.close());
}
/// Whether the instance offers password sign-in, and when GitHub sign-in
/// ends there (null for no end set, or no GitHub sign-in).
Future<({bool offered, DateTime? gitHubEnds})> offers() => _gateway.offers();
/// Whether the instance offers password sign-in.
Future<bool> offers() => _gateway.offers();
/// Mails a code for [purpose] to [address] in [language]; the request it
/// answers.
@ -280,10 +279,10 @@ class PasswordRepository(
needsAuthenticator: true,
);
/// Gives the signed-in person [material]'s password, with [address]
/// proved by a [code] mailed for [request] where they have none (one who
/// signs in with GitHub, ADR 0017, D2), and [handle] where they have none
/// (ADR 0020, D28), keeping everything. Their keys go into the keyring.
/// Gives the signed-in person [material]'s password, their [address]
/// proved by a [code] mailed for [request], and [handle] where they have
/// none (ADR 0020, D28), keeping everything. Their keys go into the
/// keyring.
Future<void> setPassword(
NewAccountMaterial material, {
String? address,
@ -460,14 +459,12 @@ class PasswordRepository(
// `wait` would wrap the two in a ParallelWaitError, which no caller
// takes for a refusal (the account section then showed nothing offline).
final waysRead = _gateway.ways()..ignore();
final offers = await _gateway.offers();
final offered = await _gateway.offers();
final ways = await waysRead;
return AccountSecurity(
offered: offers.offered,
offered: offered,
hasAddress: ways.email,
hasHandle: ways.handle,
gitHub: ways.gitHub,
gitHubEnds: offers.gitHubEnds,
hasPassword: ways.password,
secondFactorWorks: ways.secondFactorWorks,
sessionEndedAt: ways.sessionEndedForProofsAt,

View file

@ -32,15 +32,8 @@ class ServerpodPasswordGateway(
String get instanceUrl => srp.canonicalInstanceUrl(_client.host);
@override
Future<({bool offered, DateTime? gitHubEnds})> offers() => _failing(() async {
final basics = await _base.instance.basics();
return (
offered: basics.passwordSignIn,
gitHubEnds: basics.gitHubClientId == null
? null
: basics.gitHubSignInEnds,
);
});
Future<bool> offers() =>
_failing(() async => (await _base.instance.basics()).passwordSignIn);
@override
Future<String> sendCode(
@ -234,7 +227,6 @@ class ServerpodPasswordGateway(
Future<PasswordWays> ways() => _failing(() async {
final ways = await _base.account.signInWays();
return (
gitHub: ways.gitHub,
email: ways.email,
password: ways.password,
handle: ways.handle,
@ -477,7 +469,6 @@ PasswordProblem passwordProblemOf(
protocol.PasswordRefusal.setupIncomplete => PasswordProblem.setupIncomplete,
protocol.PasswordRefusal.invalidSetup => PasswordProblem.invalidSetup,
protocol.PasswordRefusal.passwordExists => PasswordProblem.passwordExists,
protocol.PasswordRefusal.noAddress => PasswordProblem.noAddress,
protocol.PasswordRefusal.addressTaken => PasswordProblem.addressTaken,
protocol.PasswordRefusal.noPassword => PasswordProblem.noPassword,
protocol.PasswordRefusal.unknownAuthenticator =>
@ -495,6 +486,5 @@ PasswordProblem passwordProblemOf(
protocol.PasswordRefusal.otherInstance => PasswordProblem.instanceUnproven,
protocol.PasswordRefusal.codesWait => PasswordProblem.codesWait,
protocol.PasswordRefusal.sessionEnded => PasswordProblem.sessionEnded,
protocol.PasswordRefusal.signInAgain => PasswordProblem.signInAgain,
protocol.PasswordRefusal.signedInBefore => PasswordProblem.signedInBefore,
};

View file

@ -14,12 +14,6 @@ final class const AccountSecurity({
/// one where they have none (D28).
required final bool hasHandle,
/// Whether the person signs in with GitHub.
required final bool gitHub,
/// When GitHub sign-in ends on the instance; null for no end set.
final DateTime? gitHubEnds,
/// Whether the person has a password.
required final bool hasPassword,
@ -54,8 +48,6 @@ final class const AccountSecurity({
offered,
hasAddress,
hasHandle,
gitHub,
gitHubEnds,
hasPassword,
secondFactorWorks,
sessionEndedAt,

Some files were not shown because too many files have changed in this diff Show more