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:
parent
ab5b7cd015
commit
4e33133733
40 changed files with 564 additions and 3570 deletions
|
|
@ -20,15 +20,13 @@ Endpoints, under the nickname `base` in a host's client (`client.modules.base.em
|
|||
| `accountRecovery` | `resetAuthenticator`, `beginKeyProof`, `resetPassword` (with the recovery phrase), `startAccountReset`, `completeAccountReset` | without a session |
|
||||
| `simpleLogin` | `begin(platform)`: an attempt and SimpleLogin's address to open, at most 30 an hour per network; `complete(attempt, code)`: signed out, a session for the account with that SimpleLogin ID, or for someone new the handle suggested from their name; signed in (`identified` only, `wrongCaller` otherwise), SimpleLogin added to the caller's account; for an account, whether it has its keys, whether its password signs in too and has the keys' proof key (D34), when a reset of its keys takes effect (D31) and its handle, the attempt standing ten minutes for the sign-in; `checkHandle(attempt, handle)`: whether someone new could take a handle, at most 30 an hour per attempt and 60 per network; `signUp(attempt, handle, setup)`, without a session: someone new's account, `identified`, with the handle and the keys the device made; a session; `beginMend(attempt, handle)`, `mend(attempt, proof)`, without a session: a broken link mended with the handle and the recovery phrase, the new identity in the old one's place; a session | anyone |
|
||||
| `keys` | for an account that signs in with SimpleLogin (Halkyon Learn's ADR 0020, D19 and D20): `begin`, `unlock`: the caller's keys for their device, once the password is proven with its own proof key; `set`: the first keys (and handle) of an account SimpleLogin was added to, with a SimpleLogin sign-in of the last ten minutes; `changePassword`, `beginPhrase` and `resetPassword` (a lost password, with the recovery phrase), `setRecoveryKeys`: each with the password's proof (the phrase's) and such a sign-in (`KeysOwnerProof`), a new password ending every other session; the password and the phrase lost (D31–D33), `startReset` with such a sign-in, `completeReset` 72 hours later with another (new keys, every other session ending), `callOffResets` with the password's proof, which any proof of the password or the phrase also does; only an account that signs in with SimpleLogin alone changes here, one whose password signs in only unlocking (D34); the tenth wrong proof in a row ends the session, as ADR 0017's do | signed in |
|
||||
| `gitHubSignIn` | `signIn`: a session for whoever has that GitHub account, unless they sign in with a password; none once `GITHUB_SIGN_IN_ENDS` has come | without a session |
|
||||
| `emailCode` | `sendCode`: a code by mail, which proves the address and signs nobody in | anyone |
|
||||
| `account` | `delete` (with the password's proof where the account has one, `keysProof` for one that signs in with SimpleLogin), `signInWays` | signed in |
|
||||
| `instance` | `basics`: the app it serves, GitHub's client ID if any and when GitHub sign-in ends, whether codes by mail, password sign-in and SimpleLogin are on, the source link, the oldest app build, where data and backups are | anyone |
|
||||
| `instance` | `basics`: the app it serves, whether codes by mail, password sign-in and SimpleLogin are on, the source link, the oldest app build, where data and backups are | anyone |
|
||||
| `profile` | `mine`, `save`, `checkHandle` (with every answer about a new handle, at most 30 an hour per person and 60 per network), `checkLinks` (at most 10 an hour), `setAvatar`, `removeAvatar`, `remove` (what it shows goes, the handle stays, private again: Halkyon Learn's ADR 0020, D27): the caller's own public profile | `identified` |
|
||||
| `profiles` | `byHandle`: anyone's public profile once they went public, whatever the handle's case | signed in |
|
||||
|
||||
- GitHub keeps only the numeric GitHub ID, in Serverpod's own table; email keeps only
|
||||
HMAC-SHA256 of the address, keyed with `emailSignInPepper` (`email_identity`), and its
|
||||
- Email keeps only HMAC-SHA256 of the address, keyed with `emailSignInPepper` (`email_identity`), and its
|
||||
codes as Argon2id hashes (`email_code_request`). Codes are eight Crockford characters,
|
||||
valid 15 minutes, 3 tries; up to 3 valid at once for an address (a fourth voids the
|
||||
oldest, so codes others ask for don't void the one its owner is typing); at most 3 an
|
||||
|
|
@ -103,14 +101,14 @@ Endpoints, under the nickname `base` in a host's client (`client.modules.base.em
|
|||
itself. Never the `Forwarded` header, which Caddy passes on as the visitor wrote it,
|
||||
so never Serverpod's `remoteInfo`. An IPv6 visitor counts by their /64
|
||||
(`networkKeyOf`), which one line or server holds whole.
|
||||
- Refusals are `PasswordRefusedException`, `GitHubSignInRefusedException`,
|
||||
`EmailSignInRefusedException` and `ProfileRefusedException`, each with a reason;
|
||||
`ProfileNotFoundException` where there is no profile.
|
||||
- Refusals are `PasswordRefusedException`, `EmailSignInRefusedException` and
|
||||
`ProfileRefusedException`, each with a reason; `ProfileNotFoundException` where there
|
||||
is no profile.
|
||||
- For the host's own code, `package:halkyon_base_server/base.dart`: the configuration,
|
||||
`tokenManagerBuilders` and `identityProviderBuilders`, the `identified` scope and
|
||||
`grantIdentified`, `deletePerson`, the pruning (`pruneAnonymousPeople`,
|
||||
`forgetOldSignUpAttempts`, `forgetExpiredEmailCodes`, `forgetUnconfirmedAuthenticators`,
|
||||
`forgetGitHubAccounts`, `forgetLapsedResets`), short codes, `sourceCodeUrl`,
|
||||
`forgetLapsedResets`), short codes, `sourceCodeUrl`,
|
||||
`dataLocation`, `backupLocation`, people's profiles as the host's code reads them
|
||||
(`profileOf`, `profilesOf`, `personWithHandle`, `fixHandle`; `saveProfile` for its
|
||||
tests), `accountDomain`, `profilePageUrl`, `addProfilePageRoute`, the register
|
||||
|
|
@ -121,10 +119,10 @@ Endpoints, under the nickname `base` in a host's client (`client.modules.base.em
|
|||
|
||||
## The instance's register (ADR 0016)
|
||||
|
||||
One operator's services share one account per person: the same address, GitHub account
|
||||
and handle reach the same person in each, under one account ID (each service's
|
||||
One operator's services share one account per person: the same address, SimpleLogin
|
||||
account and handle reach the same person in each, under one account ID (each service's
|
||||
`AuthUser` has it), and deleting the account deletes it everywhere. One service holds
|
||||
the register, its database keeping the ways in (`email_identity`, GitHub's IDs), the
|
||||
the register, its database keeping the ways in (`email_identity`, `halkyon_oidc_identity`), the
|
||||
handles and profiles; the others are its members and ask it. An anonymous account left
|
||||
from before stays its service's own. Sessions stay each service's own: every app signs
|
||||
in itself.
|
||||
|
|
@ -149,7 +147,7 @@ in itself.
|
|||
host's Caddyfile never forwards them).
|
||||
- **Signing in on a member:** the app calls the same endpoints in both modes. The member
|
||||
asks the register to mail a code (the device's network passed on, as the register
|
||||
counts codes per network) and to check it, or to say whose GitHub ID it is, and opens a
|
||||
counts codes per network) and to check it, and opens a
|
||||
session of its own for that account (`issueSession`, which makes them a person there
|
||||
under the account's ID the first time). Adding a way in makes the person identified on
|
||||
the register and then on the member; a switch token signs in as the account the
|
||||
|
|
@ -238,12 +236,10 @@ Built on the three parts above, on the register (members forward every step thro
|
|||
3–16 passes, 64 MiB–1 GiB), verifier (1 < v < N, as long as N), public keys (32 bytes
|
||||
each) and both wrappings (24-byte nonce, 112 bytes). It needs an address, and a
|
||||
session alone sets none (ADR 0017's fourth reading): an account with an address proves
|
||||
it anew by a code mailed to it for this (`firstPassword`); an account that signs in
|
||||
with GitHub alone does that within 10 minutes of a GitHub sign-in on the session asking
|
||||
(`gitHubSignInFreshFor`, the session row's method and creation time, a member telling
|
||||
the register; else `signInAgain`, before any code is used), the address it adds proved
|
||||
by a mailed code (`signUp`). Anyone else, an anonymous account left from before among
|
||||
them, sets none (`wrongCredentials`, before any code is used). It signs in once an
|
||||
it anew by a code mailed to it for this (`firstPassword`). Anyone else, an account
|
||||
that signs in with SimpleLogin alone or one left from before with an anonymous account
|
||||
or a GitHub ID among them (Halkyon Learn's ADR 0020, D36), sets none
|
||||
(`wrongCredentials`, before any code is used). It signs in once an
|
||||
authenticator is confirmed; until then a new setup replaces it, never once one was,
|
||||
even while a reset leaves the account without one (a new password comes through the
|
||||
recovery phrase or the account's reset alone).
|
||||
|
|
@ -388,16 +384,11 @@ Built on the three parts above, on the register (members forward every step thro
|
|||
day). The owner's warning of a session ended for its proofs is
|
||||
dismissed only by a session made since (`signedInBefore`). A refusal for a reset not due says when it takes effect
|
||||
(`takesEffectAt`).
|
||||
- **Older ways:** an account that signs in with a password (it had a confirmed
|
||||
authenticator, which a reset may have taken since: `passwordOnly` in its ways in) no
|
||||
longer signs in with GitHub (`usePassword`); no account gets GitHub any more (D25),
|
||||
so it signs in only those who have it; from `GITHUB_SIGN_IN_ENDS` on GitHub signs nobody in
|
||||
(`ended`), the basics stop naming the day (`gitHubSignInEnds`, which apps show until
|
||||
then) and, 30 days later (`gitHubAccountsKeptAfterEnd`, so a date set wrong can be put
|
||||
right first), `forgetGitHubAccounts` deletes every GitHub ID on the register, first
|
||||
deleting the accounts that sign in with GitHub alone (no address, never a password),
|
||||
each on its own (one failing is tried again the next day; one deleted whose host step
|
||||
after failed counts as deleted), the members told once after all of them.
|
||||
- **Older ways:** GitHub signs nobody in (Halkyon Learn's ADR 0020, D36, ending ADR
|
||||
0017's D2 sooner): the module has no GitHub endpoint and reads no GitHub settings. The
|
||||
IDs left in Serverpod's `serverpod_auth_idp_github_account` table, which stays as
|
||||
Serverpod's migrations make it, go with their accounts (`ON DELETE CASCADE`); an
|
||||
operator deletes them before with the SQL of D36.
|
||||
|
||||
## Taking it into a host
|
||||
|
||||
|
|
@ -452,8 +443,7 @@ Built on the three parts above, on the register (members forward every step thro
|
|||
after any literal routes of its own.
|
||||
|
||||
Then schedule the pruning in a future call of the host's own: daily
|
||||
`pruneAnonymousPeople`, `forgetGitHubAccounts` (the register's),
|
||||
`forgetLapsedResets` and `forgetPeopleTheRegisterForgot` (a member's), hourly
|
||||
`pruneAnonymousPeople`, `forgetLapsedResets` and `forgetPeopleTheRegisterForgot` (a member's), hourly
|
||||
`forgetOldSignUpAttempts`, `forgetExpiredEmailCodes`,
|
||||
`forgetUnconfirmedAuthenticators`, `forgetUnfinishedSimpleLogins` (the
|
||||
register's), `deliverDeletionNotices` and `deliverSessionNotices` (the register's). Each marked as one mode's does nothing in
|
||||
|
|
@ -471,8 +461,7 @@ Built on the three parts above, on the register (members forward every step thro
|
|||
version, with no SQL besides. Every service has the module's tables, whichever its
|
||||
mode: a member's stay empty but for its people's sessions.
|
||||
|
||||
The passwords it reads: `serverSideSessionKeyHashPepper`; for GitHub `githubClientId` and
|
||||
`githubClientSecret`; for email `emailSignInPepper` and `emailSmtpHost`,
|
||||
The passwords it reads: `serverSideSessionKeyHashPepper`; for email `emailSignInPepper` and `emailSmtpHost`,
|
||||
`emailSmtpPort`, `emailSmtpUser`, `emailSmtpPassword`, `emailFrom`; for passwords
|
||||
`totpKey`, `recoveryCodeKey` and `signInDecoyKey` (each at least 32 characters, or
|
||||
`addInstanceRoutes` stops the server; all three and email sign-in, or no password
|
||||
|
|
@ -483,14 +472,13 @@ and without it: mending answers decoys); between the
|
|||
instance's services `instanceSecret`. Never change either pepper
|
||||
once people sign in: every session or address hash would be lost; changing `totpKey`
|
||||
voids every authenticator, `recoveryCodeKey` every recovery code. A member needs no email settings of its
|
||||
own (the register mails the codes), but GitHub's (its own OAuth app's, whose callback is
|
||||
its app's). The environment: `ACCOUNT_REGISTER_URL` and `INSTANCE_MEMBERS` (above);
|
||||
own (the register mails the codes). The environment: `ACCOUNT_REGISTER_URL` and `INSTANCE_MEMBERS` (above);
|
||||
`ACCOUNT_DOMAIN`, the domain handles are on, which can never change once anyone follows
|
||||
one; `SIMPLELOGIN_CALLBACK_URL`, the https address the SimpleLogin app registered, where
|
||||
it isn't the register's own page (`/simplelogin-callback` on its web server's public
|
||||
address; development names learn.halkyon.eu's); `SIGN_IN_WAYS`, the ways in the
|
||||
register offers of those set up, `simplelogin`, `password` or both (unset: every one), a
|
||||
name it doesn't know stopping the server (`addInstanceRoutes`); `GITHUB_SIGN_IN_ENDS`, the day GitHub stops signing anyone in (`2027-01-22`);
|
||||
name it doesn't know stopping the server (`addInstanceRoutes`);
|
||||
`SOURCE_URL`, `DATA_LOCATION`, `BACKUP_LOCATION`: where the instance keeps people's data
|
||||
and its backups, each `EU`, a country's code (`DE`) or a subdivision's (ISO 3166-2,
|
||||
`US-CA`, `DE-BY`), the backups also `none`; where the disks are, not which law governs;
|
||||
|
|
|
|||
|
|
@ -11,8 +11,6 @@ export 'src/auth/account.dart';
|
|||
export 'src/auth/auth_config.dart';
|
||||
export 'src/auth/code_mail.dart';
|
||||
export 'src/auth/email.dart';
|
||||
export 'src/auth/github.dart';
|
||||
export 'src/auth/github_sign_in_endpoint.dart' show gitHubAccountToSignIn;
|
||||
export 'src/auth/identified.dart';
|
||||
export 'src/auth/offered_ways.dart';
|
||||
export 'src/auth/scopes.dart';
|
||||
|
|
@ -47,7 +45,6 @@ export 'src/password/key_proof.dart'
|
|||
export 'src/password/password_config.dart';
|
||||
export 'src/password/passwords.dart'
|
||||
show
|
||||
gitHubSignInFreshFor,
|
||||
isKeysPassword,
|
||||
isKeysSetup,
|
||||
isNewPassword,
|
||||
|
|
|
|||
|
|
@ -127,8 +127,8 @@ Future<void> deletePerson(
|
|||
where: (t) => t.authUserId.equals(authUserId),
|
||||
transaction: transaction,
|
||||
);
|
||||
// Their GitHub ID and SimpleLogin identity go with them, and an
|
||||
// anonymous account left from before (ON DELETE CASCADE).
|
||||
// Their SimpleLogin identity goes with them, and what is left from
|
||||
// before, an anonymous account or a GitHub ID (ON DELETE CASCADE).
|
||||
// Gone already on a member the register told first (ADR 0016).
|
||||
// DELETE FROM "serverpod_auth_core_user" WHERE "id" = $1
|
||||
if (person != null) {
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ class AccountEndpoint extends Endpoint {
|
|||
).knownAccounts(session, {person})).contains(person);
|
||||
|
||||
/// How the caller signs in besides their sessions ([SignInWays]): with
|
||||
/// SimpleLogin, a password, an email address or GitHub.
|
||||
/// SimpleLogin, a password or an email address.
|
||||
Future<SignInWays> signInWays(Session session) => AccountRegister.of(
|
||||
session.serverpod,
|
||||
).signInWaysOf(session, session.authenticated!.authUserId);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
import 'package:halkyon_base_server/src/auth/github.dart';
|
||||
import 'package:halkyon_base_server/src/simplelogin/simplelogin_config.dart';
|
||||
import 'package:serverpod/serverpod.dart';
|
||||
import 'package:serverpod_auth_idp_server/core.dart';
|
||||
import 'package:serverpod_auth_idp_server/providers/github.dart';
|
||||
|
||||
// How people authenticate. The server and its tests use this same
|
||||
// configuration, so the tests check what production runs.
|
||||
|
|
@ -19,14 +17,9 @@ final List<TokenManagerBuilder> tokenManagerBuilders = [
|
|||
/// which are the module's own (`passwords.dart`, `email.dart`). No anonymous
|
||||
/// accounts (ADR 0020, D25): every account has a way in.
|
||||
///
|
||||
/// GitHub only where the operator registered a GitHub OAuth app (see
|
||||
/// [gitHubSignInAvailable]); its helpers serve our own slim login
|
||||
/// (`github.dart`), and Serverpod's stock GitHub login endpoint isn't exposed.
|
||||
///
|
||||
/// SimpleLogin (ADR 0020) on the register, where it has the instance's
|
||||
/// SimpleLogin app ([simpleLoginIdpConfig]): halkyon_oidc's sign-in, whose
|
||||
/// parts `simplelogin.dart` uses; its stock endpoint isn't exposed either.
|
||||
/// parts `simplelogin.dart` uses; its stock endpoint isn't exposed.
|
||||
List<IdentityProviderBuilder> identityProviderBuilders(Serverpod pod) => [
|
||||
if (gitHubSignInAvailable(pod)) GitHubIdpConfigFromPasswords(),
|
||||
?simpleLoginIdpConfig(pod),
|
||||
];
|
||||
|
|
|
|||
|
|
@ -1,212 +0,0 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:halkyon_base_server/src/auth/account.dart';
|
||||
import 'package:halkyon_base_server/src/auth/membership.dart';
|
||||
import 'package:halkyon_base_server/src/generated/protocol.dart';
|
||||
import 'package:halkyon_base_server/src/register/deletion_notices.dart';
|
||||
import 'package:serverpod/serverpod.dart';
|
||||
import 'package:serverpod_auth_idp_server/core.dart';
|
||||
import 'package:serverpod_auth_idp_server/providers/github.dart';
|
||||
|
||||
// Signing in with GitHub keeps only the numeric GitHub ID (decided 2026-09-30).
|
||||
// So the login is our own, on Serverpod's GitHub helpers: Serverpod's stock
|
||||
// login also stores the name and the email in a profile and downloads the
|
||||
// avatar to the server.
|
||||
|
||||
/// Serverpod's name for GitHub sign-in on session rows (`GitHubIdp.method`).
|
||||
const gitHubMethod = 'github';
|
||||
|
||||
/// Whether this instance's operator registered a GitHub OAuth app: its client
|
||||
/// ID and secret are in the passwords (`githubClientId`,
|
||||
/// `githubClientSecret`). Without them, GitHub sign-in is off and the app
|
||||
/// hides it. Empty values count as missing: Compose passes unset variables on
|
||||
/// as empty strings.
|
||||
bool gitHubSignInAvailable(Serverpod pod) =>
|
||||
(pod.getPassword('githubClientId') ?? '').isNotEmpty &&
|
||||
(pod.getPassword('githubClientSecret') ?? '').isNotEmpty;
|
||||
|
||||
/// When GitHub stops signing anyone in here (ADR 0017, D2: three months
|
||||
/// after the release that brings passwords): the operator's
|
||||
/// `GITHUB_SIGN_IN_ENDS`, a day (`2027-01-22`) from its start in UTC; null
|
||||
/// while unset. From then on GitHub signs nobody in and links nothing, the
|
||||
/// instance offers it no more, and pruning forgets every GitHub ID
|
||||
/// ([forgetGitHubAccounts]). Throws a [StateError] for a value that is no
|
||||
/// such day, which hosts check as they start (`addInstanceRoutes`).
|
||||
DateTime? gitHubSignInEnds(Serverpod pod) =>
|
||||
_endsOf[pod] ??
|
||||
gitHubSignInEndsFrom(Platform.environment['GITHUB_SIGN_IN_ENDS']);
|
||||
|
||||
/// [value] (`GITHUB_SIGN_IN_ENDS`) as the moment GitHub sign-in ends: the
|
||||
/// day's start in UTC; null for none (unset or empty, as Compose passes an
|
||||
/// unset variable). Throws a [StateError] for anything but a day.
|
||||
DateTime? gitHubSignInEndsFrom(String? value) {
|
||||
final day = value?.trim() ?? '';
|
||||
if (day.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
final parsed = RegExp(r'^\d{4}-\d{2}-\d{2}$').hasMatch(day)
|
||||
? DateTime.tryParse('${day}T00:00:00Z')
|
||||
: null;
|
||||
if (parsed == null || parsed.toIso8601String().substring(0, 10) != day) {
|
||||
throw StateError(
|
||||
'GITHUB_SIGN_IN_ENDS is "$day": it must be a day, such as 2027-01-22.',
|
||||
);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
/// Whether GitHub sign-in has ended on [pod] ([gitHubSignInEnds]).
|
||||
bool gitHubSignInEnded(Serverpod pod) => switch (gitHubSignInEnds(pod)) {
|
||||
final ends? => !clock.now().isBefore(ends),
|
||||
null => false,
|
||||
};
|
||||
|
||||
/// Puts [ends] in place of the operator's `GITHUB_SIGN_IN_ENDS` on [pod], or
|
||||
/// with null theirs back: tests do.
|
||||
void useGitHubSignInEnds(Serverpod pod, DateTime? ends) => _endsOf[pod] = ends;
|
||||
|
||||
final _endsOf = Expando<DateTime>('GitHub sign-in ends');
|
||||
|
||||
/// How long after GitHub sign-in ended ([gitHubSignInEnds]) the accounts
|
||||
/// that signed in with it alone and every GitHub ID are kept before they
|
||||
/// are deleted ([forgetGitHubAccounts]): a date the operator set wrong can
|
||||
/// be put right meanwhile, before anything is lost. The app's texts name it
|
||||
/// (halkyon_app's `passwordSectionGitHubEndsDeletes`): change them with it.
|
||||
const gitHubAccountsKeptAfterEnd = Duration(days: 30);
|
||||
|
||||
/// Deletes every GitHub ID the instance keeps once GitHub sign-in has ended
|
||||
/// ([gitHubSignInEnds]) and [gitHubAccountsKeptAfterEnd] passed at [now],
|
||||
/// and returns how many: nobody signs in with them any more, so nothing is
|
||||
/// kept of them. Daily, with the pruning; on the register, which keeps them
|
||||
/// (ADR 0016): a member does nothing (the reading of the fixes' D1), as it
|
||||
/// keeps no addresses, so everyone there would look as if GitHub were their
|
||||
/// only way in.
|
||||
///
|
||||
/// First the accounts that sign in with GitHub alone (no address, so never
|
||||
/// a password) go, with everything they have, every member told
|
||||
/// (`deletePerson`): no way in is left to them, and an account nobody can
|
||||
/// enter would stay for good, or be claimed by whoever holds a session of
|
||||
/// it (the full reading's M1 and L6). The app tells them for the three
|
||||
/// months before (ADR 0017, D2). One whose deletion fails holds back nobody
|
||||
/// else and keeps its GitHub ID, so the next day's run tries it again; the
|
||||
/// members are told once, after all of them (the reading of the fixes' D2).
|
||||
Future<int> forgetGitHubAccounts(
|
||||
Session session, {
|
||||
required DateTime now,
|
||||
}) async {
|
||||
final pod = session.serverpod;
|
||||
final ends = gitHubSignInEnds(pod);
|
||||
if (isMember(pod) ||
|
||||
ends == null ||
|
||||
now.isBefore(ends.add(gitHubAccountsKeptAfterEnd))) {
|
||||
return 0;
|
||||
}
|
||||
// SELECT * FROM "serverpod_auth_idp_github_account"
|
||||
final gitHubAccounts = await GitHubAccount.db.find(session);
|
||||
final gitHubPeople = {
|
||||
for (final account in gitHubAccounts) account.authUserId,
|
||||
};
|
||||
final failed = <UuidValue>{};
|
||||
if (gitHubPeople.isNotEmpty) {
|
||||
// SELECT * FROM "email_identity" WHERE "authUserId" = ANY($1)
|
||||
final withAddress = {
|
||||
for (final identity in await EmailIdentity.db.find(
|
||||
session,
|
||||
where: (t) => t.authUserId.inSet(gitHubPeople),
|
||||
))
|
||||
identity.authUserId,
|
||||
};
|
||||
final gitHubOnly = gitHubPeople.difference(withAddress);
|
||||
for (final person in gitHubOnly) {
|
||||
try {
|
||||
await deletePerson(session, person, tellMembers: false);
|
||||
} on Object catch (error) {
|
||||
// A step after its transaction may have failed, the account gone
|
||||
// with its GitHub ID all the same (the short reading): only one
|
||||
// still here is tried again.
|
||||
// SELECT … FROM "serverpod_auth_core_user" WHERE "id" = $1
|
||||
final gone = await AuthUser.db.findById(session, person) == null;
|
||||
// Its type only: whose stays out of the log.
|
||||
session.log(
|
||||
gone
|
||||
? 'Deleted an account that signed in with GitHub alone, but a '
|
||||
'step after failed: ${error.runtimeType}.'
|
||||
: 'Deleting an account that signed in with GitHub alone '
|
||||
'failed: ${error.runtimeType}; tried again tomorrow.',
|
||||
level: LogLevel.warning,
|
||||
);
|
||||
if (!gone) {
|
||||
failed.add(person);
|
||||
}
|
||||
}
|
||||
}
|
||||
session.log(
|
||||
'Deleted ${gitHubOnly.length - failed.length} accounts that signed in '
|
||||
'with GitHub alone, at its end.',
|
||||
);
|
||||
await deliverDeletionNotices(session);
|
||||
}
|
||||
// DELETE FROM "serverpod_auth_idp_github_account"
|
||||
// WHERE "authUserId" <> ALL($1)
|
||||
await GitHubAccount.db.deleteWhere(
|
||||
session,
|
||||
where: (t) =>
|
||||
failed.isEmpty ? t.id.notEquals(null) : t.authUserId.notInSet(failed),
|
||||
);
|
||||
// Those deleted with their accounts counted too.
|
||||
return gitHubAccounts
|
||||
.where((account) => !failed.contains(account.authUserId))
|
||||
.length;
|
||||
}
|
||||
|
||||
/// The numeric GitHub ID that [code], from the app's GitHub sign-in, proves.
|
||||
/// Refused once GitHub sign-in has ended ([GitHubSignInRefusal.ended]).
|
||||
///
|
||||
/// The code is exchanged here, with the client secret, which never leaves the
|
||||
/// server. GitHub's answer also carries the name and possibly an email
|
||||
/// address; neither is kept, and the access token is used once and dropped.
|
||||
Future<String> verifiedGitHubUserId(
|
||||
Session session, {
|
||||
required String code,
|
||||
required String codeVerifier,
|
||||
required String redirectUri,
|
||||
}) async {
|
||||
if (gitHubSignInEnded(session.serverpod)) {
|
||||
throw GitHubSignInRefusedException(reason: GitHubSignInRefusal.ended);
|
||||
}
|
||||
if (!gitHubSignInAvailable(session.serverpod)) {
|
||||
throw GitHubSignInRefusedException(
|
||||
reason: GitHubSignInRefusal.unavailable,
|
||||
);
|
||||
}
|
||||
final utils = AuthServices.instance.githubIdp.utils;
|
||||
final accessToken = await utils.exchangeCodeForToken(
|
||||
session,
|
||||
code: code,
|
||||
codeVerifier: codeVerifier,
|
||||
redirectUri: redirectUri,
|
||||
);
|
||||
final details = await utils.fetchAccountDetails(
|
||||
session,
|
||||
accessToken: accessToken,
|
||||
);
|
||||
return details.userIdentifier;
|
||||
}
|
||||
|
||||
/// The person who has the GitHub account [gitHubUserId]; null for nobody.
|
||||
/// A session for them is the caller's to issue (`issueSession`), when it
|
||||
/// switches to them on another device.
|
||||
///
|
||||
/// Nobody is created here: no account gets GitHub any more (ADR 0017, D2).
|
||||
Future<UuidValue?> accountWithGitHub(
|
||||
Session session, {
|
||||
required String gitHubUserId,
|
||||
}) async {
|
||||
// SELECT * FROM "serverpod_auth_idp_github_account" WHERE "userIdentifier" = $1
|
||||
final account = await GitHubAccount.db.findFirstRow(
|
||||
session,
|
||||
where: (t) => t.userIdentifier.equals(gitHubUserId),
|
||||
);
|
||||
return account?.authUserId;
|
||||
}
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
import 'package:halkyon_base_server/src/auth/github.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';
|
||||
|
||||
/// Signs in the person who already has a GitHub account, e.g. when they switch
|
||||
/// to it on another device. Called without a session: Serverpod issues no
|
||||
/// session for another person inside a signed-in call.
|
||||
class GitHubSignInEndpoint extends Endpoint {
|
||||
/// A session for the person with the GitHub account that [code] proves;
|
||||
/// refusals as [gitHubAccountToSignIn]'s.
|
||||
Future<AuthSuccess> signIn(
|
||||
Session session, {
|
||||
required String code,
|
||||
required String codeVerifier,
|
||||
required String redirectUri,
|
||||
}) async {
|
||||
final gitHubUserId = await verifiedGitHubUserId(
|
||||
session,
|
||||
code: code,
|
||||
codeVerifier: codeVerifier,
|
||||
redirectUri: redirectUri,
|
||||
);
|
||||
final account = await gitHubAccountToSignIn(session, gitHubUserId);
|
||||
return await issueSession(session, account, method: gitHubMethod);
|
||||
}
|
||||
}
|
||||
|
||||
/// The account the GitHub account [gitHubUserId] signs into, as the
|
||||
/// instance's register knows it; an account it doesn't know is refused
|
||||
/// ([GitHubSignInRefusal.unknownAccount]), and so is one that signs in with
|
||||
/// a password ([GitHubSignInRefusal.usePassword], ADR 0017: it had a
|
||||
/// confirmed authenticator, `SignInWays.passwordOnly`). GitHub is
|
||||
/// deprecated: a person signed in so without a password sets an address, a
|
||||
/// password and an authenticator next, as the app reads from their ways in
|
||||
/// (`account.signInWays`).
|
||||
Future<UuidValue> gitHubAccountToSignIn(
|
||||
Session session,
|
||||
String gitHubUserId,
|
||||
) async {
|
||||
final register = AccountRegister.of(session.serverpod);
|
||||
final account =
|
||||
await register.accountWithGitHub(session, gitHubUserId) ??
|
||||
(throw GitHubSignInRefusedException(
|
||||
reason: GitHubSignInRefusal.unknownAccount,
|
||||
));
|
||||
if ((await register.signInWaysOf(session, account)).passwordOnly) {
|
||||
throw GitHubSignInRefusedException(
|
||||
reason: GitHubSignInRefusal.usePassword,
|
||||
);
|
||||
}
|
||||
return account;
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
### Why signing in with GitHub, or linking a GitHub account, was refused.
|
||||
enum: GitHubSignInRefusal
|
||||
serialized: byName
|
||||
values:
|
||||
### This instance has no GitHub OAuth app (no githubClientId and
|
||||
### githubClientSecret in its passwords).
|
||||
- unavailable
|
||||
### The GitHub account already belongs to another person.
|
||||
- takenByAnotherPerson
|
||||
### The person already has a different GitHub account.
|
||||
- otherGitHubAccountLinked
|
||||
### No person has this GitHub account; only linking one creates it.
|
||||
- unknownAccount
|
||||
### The account signs in with its password: GitHub is deprecated (ADR 0017),
|
||||
### and can't unlock the account's keys.
|
||||
- usePassword
|
||||
### GitHub accounts are no longer linked (ADR 0017): an address and a
|
||||
### password are the way in.
|
||||
- deprecated
|
||||
### GitHub signs nobody in any more here: its time ended
|
||||
### (`GITHUB_SIGN_IN_ENDS`).
|
||||
- ended
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
### Signing in with GitHub, or linking a GitHub account, was refused; nothing
|
||||
### changed.
|
||||
exception: GitHubSignInRefusedException
|
||||
fields:
|
||||
### Why it was refused.
|
||||
reason: GitHubSignInRefusal
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
### The ways a person signs in, besides their sessions.
|
||||
class: SignInWays
|
||||
fields:
|
||||
gitHub: bool
|
||||
email: bool
|
||||
### SimpleLogin (ADR 0020): the instance's register keeps its ID for the
|
||||
### instance's SimpleLogin app.
|
||||
|
|
@ -18,8 +17,8 @@ fields:
|
|||
### A confirmed authenticator: the password signs in only with one.
|
||||
authenticator: bool, default=false
|
||||
### The account signs in with its password alone: it had a confirmed
|
||||
### authenticator (a reset may have left it without one since), so
|
||||
### neither a code by mail nor GitHub signs into it (ADR 0017).
|
||||
### authenticator (a reset may have left it without one since), so a
|
||||
### code by mail no longer signs into it (ADR 0017).
|
||||
passwordOnly: bool, default=false
|
||||
### A second factor works: a confirmed authenticator, and no reset of the
|
||||
### authenticators that took effect. Where none does, deleting the account
|
||||
|
|
|
|||
|
|
@ -1,70 +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;
|
||||
|
||||
/// Why signing in with GitHub, or linking a GitHub account, was refused.
|
||||
enum GitHubSignInRefusal implements _is.SerializableModel {
|
||||
/// This instance has no GitHub OAuth app (no githubClientId and
|
||||
/// githubClientSecret in its passwords).
|
||||
unavailable,
|
||||
|
||||
/// The GitHub account already belongs to another person.
|
||||
takenByAnotherPerson,
|
||||
|
||||
/// The person already has a different GitHub account.
|
||||
otherGitHubAccountLinked,
|
||||
|
||||
/// No person has this GitHub account; only linking one creates it.
|
||||
unknownAccount,
|
||||
|
||||
/// The account signs in with its password: GitHub is deprecated (ADR 0017),
|
||||
/// and can't unlock the account's keys.
|
||||
usePassword,
|
||||
|
||||
/// GitHub accounts are no longer linked (ADR 0017): an address and a
|
||||
/// password are the way in.
|
||||
deprecated,
|
||||
|
||||
/// GitHub signs nobody in any more here: its time ended
|
||||
/// (`GITHUB_SIGN_IN_ENDS`).
|
||||
ended;
|
||||
|
||||
static GitHubSignInRefusal fromJson(String name) {
|
||||
switch (name) {
|
||||
case 'unavailable':
|
||||
return GitHubSignInRefusal.unavailable;
|
||||
case 'takenByAnotherPerson':
|
||||
return GitHubSignInRefusal.takenByAnotherPerson;
|
||||
case 'otherGitHubAccountLinked':
|
||||
return GitHubSignInRefusal.otherGitHubAccountLinked;
|
||||
case 'unknownAccount':
|
||||
return GitHubSignInRefusal.unknownAccount;
|
||||
case 'usePassword':
|
||||
return GitHubSignInRefusal.usePassword;
|
||||
case 'deprecated':
|
||||
return GitHubSignInRefusal.deprecated;
|
||||
case 'ended':
|
||||
return GitHubSignInRefusal.ended;
|
||||
default:
|
||||
throw ArgumentError(
|
||||
'Value "$name" cannot be converted to "GitHubSignInRefusal"',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String toJson() => name;
|
||||
|
||||
@override
|
||||
String toString() => name;
|
||||
}
|
||||
|
|
@ -1,84 +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;
|
||||
import '../auth/github_sign_in_refusal.dart' as _irgbwy1y;
|
||||
|
||||
/// Signing in with GitHub, or linking a GitHub account, was refused; nothing
|
||||
/// changed.
|
||||
abstract class GitHubSignInRefusedException
|
||||
implements
|
||||
_is.SerializableException,
|
||||
_is.SerializableModel,
|
||||
_is.ProtocolSerialization {
|
||||
GitHubSignInRefusedException._({required this.reason});
|
||||
|
||||
factory GitHubSignInRefusedException({
|
||||
required _irgbwy1y.GitHubSignInRefusal reason,
|
||||
}) = _GitHubSignInRefusedExceptionImpl;
|
||||
|
||||
factory GitHubSignInRefusedException.fromJson(
|
||||
Map<String, dynamic> jsonSerialization,
|
||||
) {
|
||||
return GitHubSignInRefusedException(
|
||||
reason: _irgbwy1y.GitHubSignInRefusal.fromJson(
|
||||
(jsonSerialization['reason'] as String),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Why it was refused.
|
||||
_irgbwy1y.GitHubSignInRefusal reason;
|
||||
|
||||
/// Returns a shallow copy of this [GitHubSignInRefusedException]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
GitHubSignInRefusedException copyWith({
|
||||
_irgbwy1y.GitHubSignInRefusal? reason,
|
||||
});
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'__className__': 'halkyon_base.GitHubSignInRefusedException',
|
||||
'reason': reason.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJsonForProtocol() {
|
||||
return {
|
||||
'__className__': 'halkyon_base.GitHubSignInRefusedException',
|
||||
'reason': reason.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'GitHubSignInRefusedException(reason: $reason)';
|
||||
}
|
||||
}
|
||||
|
||||
class _GitHubSignInRefusedExceptionImpl extends GitHubSignInRefusedException {
|
||||
_GitHubSignInRefusedExceptionImpl({
|
||||
required _irgbwy1y.GitHubSignInRefusal reason,
|
||||
}) : super._(reason: reason);
|
||||
|
||||
/// Returns a shallow copy of this [GitHubSignInRefusedException]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
@override
|
||||
GitHubSignInRefusedException copyWith({
|
||||
_irgbwy1y.GitHubSignInRefusal? reason,
|
||||
}) {
|
||||
return GitHubSignInRefusedException(reason: reason ?? this.reason);
|
||||
}
|
||||
}
|
||||
|
|
@ -16,7 +16,6 @@ import 'package:serverpod/serverpod.dart' as _is;
|
|||
abstract class SignInWays
|
||||
implements _is.SerializableModel, _is.ProtocolSerialization {
|
||||
SignInWays._({
|
||||
required this.gitHub,
|
||||
required this.email,
|
||||
bool? simpleLogin,
|
||||
bool? password,
|
||||
|
|
@ -37,7 +36,6 @@ abstract class SignInWays
|
|||
secondFactorWorks = secondFactorWorks ?? false;
|
||||
|
||||
factory SignInWays({
|
||||
required bool gitHub,
|
||||
required bool email,
|
||||
bool? simpleLogin,
|
||||
bool? password,
|
||||
|
|
@ -53,7 +51,6 @@ abstract class SignInWays
|
|||
|
||||
factory SignInWays.fromJson(Map<String, dynamic> jsonSerialization) {
|
||||
return SignInWays(
|
||||
gitHub: _is.BoolJsonExtension.fromJson(jsonSerialization['gitHub']),
|
||||
email: _is.BoolJsonExtension.fromJson(jsonSerialization['email']),
|
||||
simpleLogin: jsonSerialization['simpleLogin'] == null
|
||||
? null
|
||||
|
|
@ -97,8 +94,6 @@ abstract class SignInWays
|
|||
);
|
||||
}
|
||||
|
||||
bool gitHub;
|
||||
|
||||
bool email;
|
||||
|
||||
/// SimpleLogin (ADR 0020): the instance's register keeps its ID for the
|
||||
|
|
@ -121,8 +116,8 @@ abstract class SignInWays
|
|||
bool authenticator;
|
||||
|
||||
/// The account signs in with its password alone: it had a confirmed
|
||||
/// authenticator (a reset may have left it without one since), so
|
||||
/// neither a code by mail nor GitHub signs into it (ADR 0017).
|
||||
/// authenticator (a reset may have left it without one since), so a
|
||||
/// code by mail no longer signs into it (ADR 0017).
|
||||
bool passwordOnly;
|
||||
|
||||
/// A second factor works: a confirmed authenticator, and no reset of the
|
||||
|
|
@ -147,7 +142,6 @@ abstract class SignInWays
|
|||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
SignInWays copyWith({
|
||||
bool? gitHub,
|
||||
bool? email,
|
||||
bool? simpleLogin,
|
||||
bool? password,
|
||||
|
|
@ -164,7 +158,6 @@ abstract class SignInWays
|
|||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'__className__': 'halkyon_base.SignInWays',
|
||||
'gitHub': gitHub,
|
||||
'email': email,
|
||||
'simpleLogin': simpleLogin,
|
||||
'password': password,
|
||||
|
|
@ -185,7 +178,6 @@ abstract class SignInWays
|
|||
Map<String, dynamic> toJsonForProtocol() {
|
||||
return {
|
||||
'__className__': 'halkyon_base.SignInWays',
|
||||
'gitHub': gitHub,
|
||||
'email': email,
|
||||
'simpleLogin': simpleLogin,
|
||||
'password': password,
|
||||
|
|
@ -212,7 +204,6 @@ class _Undefined {}
|
|||
|
||||
class _SignInWaysImpl extends SignInWays {
|
||||
_SignInWaysImpl({
|
||||
required bool gitHub,
|
||||
required bool email,
|
||||
bool? simpleLogin,
|
||||
bool? password,
|
||||
|
|
@ -225,7 +216,6 @@ class _SignInWaysImpl extends SignInWays {
|
|||
DateTime? authenticatorResetAt,
|
||||
DateTime? accountResetAt,
|
||||
}) : super._(
|
||||
gitHub: gitHub,
|
||||
email: email,
|
||||
simpleLogin: simpleLogin,
|
||||
password: password,
|
||||
|
|
@ -244,7 +234,6 @@ class _SignInWaysImpl extends SignInWays {
|
|||
@_is.useResult
|
||||
@override
|
||||
SignInWays copyWith({
|
||||
bool? gitHub,
|
||||
bool? email,
|
||||
bool? simpleLogin,
|
||||
bool? password,
|
||||
|
|
@ -258,7 +247,6 @@ class _SignInWaysImpl extends SignInWays {
|
|||
Object? accountResetAt = _Undefined,
|
||||
}) {
|
||||
return SignInWays(
|
||||
gitHub: gitHub ?? this.gitHub,
|
||||
email: email ?? this.email,
|
||||
simpleLogin: simpleLogin ?? this.simpleLogin,
|
||||
password: password ?? this.password,
|
||||
|
|
|
|||
|
|
@ -41,7 +41,6 @@ import 'package:serverpod_auth_idp_server/serverpod_auth_idp_server.dart'
|
|||
as _iais;
|
||||
import '../auth/account_endpoint.dart' as _ii6ycf6v;
|
||||
import '../auth/email_code_endpoint.dart' as _in3cimlf;
|
||||
import '../auth/github_sign_in_endpoint.dart' as _iasw79m5;
|
||||
import '../instance/instance_endpoint.dart' as _i3g8bfc2;
|
||||
import '../keys/keys_endpoint.dart' as _icowoccn;
|
||||
import '../password/account_recovery_endpoint.dart' as _ivnbjhab;
|
||||
|
|
@ -67,12 +66,6 @@ class Endpoints extends _is.EndpointDispatch {
|
|||
'emailCode',
|
||||
'halkyon_base',
|
||||
),
|
||||
'gitHubSignIn': _iasw79m5.GitHubSignInEndpoint()
|
||||
..initialize(
|
||||
server,
|
||||
'gitHubSignIn',
|
||||
'halkyon_base',
|
||||
),
|
||||
'instance': _i3g8bfc2.InstanceEndpoint()
|
||||
..initialize(
|
||||
server,
|
||||
|
|
@ -206,44 +199,6 @@ class Endpoints extends _is.EndpointDispatch {
|
|||
),
|
||||
},
|
||||
);
|
||||
connectors['gitHubSignIn'] = _is.EndpointConnector(
|
||||
name: 'gitHubSignIn',
|
||||
endpoint: endpoints['gitHubSignIn']!,
|
||||
methodConnectors: {
|
||||
'signIn': _is.MethodConnector(
|
||||
name: 'signIn',
|
||||
params: {
|
||||
'code': _is.ParameterDescription(
|
||||
name: 'code',
|
||||
type: _is.getType<String>(),
|
||||
nullable: false,
|
||||
),
|
||||
'codeVerifier': _is.ParameterDescription(
|
||||
name: 'codeVerifier',
|
||||
type: _is.getType<String>(),
|
||||
nullable: false,
|
||||
),
|
||||
'redirectUri': _is.ParameterDescription(
|
||||
name: 'redirectUri',
|
||||
type: _is.getType<String>(),
|
||||
nullable: false,
|
||||
),
|
||||
},
|
||||
call:
|
||||
(
|
||||
_is.Session session,
|
||||
Map<String, dynamic> params,
|
||||
) async =>
|
||||
(endpoints['gitHubSignIn'] as _iasw79m5.GitHubSignInEndpoint)
|
||||
.signIn(
|
||||
session,
|
||||
code: params['code'],
|
||||
codeVerifier: params['codeVerifier'],
|
||||
redirectUri: params['redirectUri'],
|
||||
),
|
||||
),
|
||||
},
|
||||
);
|
||||
connectors['instance'] = _is.EndpointConnector(
|
||||
name: 'instance',
|
||||
endpoint: endpoints['instance']!,
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ abstract class InstanceBasics
|
|||
implements _is.SerializableModel, _is.ProtocolSerialization {
|
||||
InstanceBasics._({
|
||||
required this.app,
|
||||
this.gitHubClientId,
|
||||
required this.emailSignIn,
|
||||
required this.sourceCodeUrl,
|
||||
required this.minimumAppBuild,
|
||||
|
|
@ -28,13 +27,11 @@ abstract class InstanceBasics
|
|||
this.backupLocation,
|
||||
bool? passwordSignIn,
|
||||
bool? simpleLogin,
|
||||
this.gitHubSignInEnds,
|
||||
}) : passwordSignIn = passwordSignIn ?? false,
|
||||
simpleLogin = simpleLogin ?? false;
|
||||
|
||||
factory InstanceBasics({
|
||||
required String app,
|
||||
String? gitHubClientId,
|
||||
required bool emailSignIn,
|
||||
required String sourceCodeUrl,
|
||||
required int minimumAppBuild,
|
||||
|
|
@ -42,13 +39,11 @@ abstract class InstanceBasics
|
|||
String? backupLocation,
|
||||
bool? passwordSignIn,
|
||||
bool? simpleLogin,
|
||||
DateTime? gitHubSignInEnds,
|
||||
}) = _InstanceBasicsImpl;
|
||||
|
||||
factory InstanceBasics.fromJson(Map<String, dynamic> jsonSerialization) {
|
||||
return InstanceBasics(
|
||||
app: jsonSerialization['app'] as String,
|
||||
gitHubClientId: jsonSerialization['gitHubClientId'] as String?,
|
||||
emailSignIn: _is.BoolJsonExtension.fromJson(
|
||||
jsonSerialization['emailSignIn'],
|
||||
),
|
||||
|
|
@ -62,11 +57,6 @@ abstract class InstanceBasics
|
|||
simpleLogin: jsonSerialization['simpleLogin'] == null
|
||||
? null
|
||||
: _is.BoolJsonExtension.fromJson(jsonSerialization['simpleLogin']),
|
||||
gitHubSignInEnds: jsonSerialization['gitHubSignInEnds'] == null
|
||||
? null
|
||||
: _is.DateTimeJsonExtension.fromJson(
|
||||
jsonSerialization['gitHubSignInEnds'],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -74,10 +64,6 @@ abstract class InstanceBasics
|
|||
/// configures it: an app accepts only an instance of its own.
|
||||
String app;
|
||||
|
||||
/// The client ID of this instance's GitHub OAuth app, or null when GitHub
|
||||
/// sign-in is off here.
|
||||
String? gitHubClientId;
|
||||
|
||||
/// Whether people can sign in with an email address here: a code mailed
|
||||
/// to it.
|
||||
bool emailSignIn;
|
||||
|
|
@ -108,18 +94,11 @@ abstract class InstanceBasics
|
|||
/// instance's register has a SimpleLogin app, and its operator offers it.
|
||||
bool simpleLogin;
|
||||
|
||||
/// When GitHub sign-in ends here (ADR 0017: it is deprecated), as its
|
||||
/// operator set it (GITHUB_SIGN_IN_ENDS), in UTC; null while no end is set,
|
||||
/// or once it has ended. People who sign in with GitHub add an address, a
|
||||
/// password and an authenticator before then.
|
||||
DateTime? gitHubSignInEnds;
|
||||
|
||||
/// Returns a shallow copy of this [InstanceBasics]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
InstanceBasics copyWith({
|
||||
String? app,
|
||||
String? gitHubClientId,
|
||||
bool? emailSignIn,
|
||||
String? sourceCodeUrl,
|
||||
int? minimumAppBuild,
|
||||
|
|
@ -127,14 +106,12 @@ abstract class InstanceBasics
|
|||
String? backupLocation,
|
||||
bool? passwordSignIn,
|
||||
bool? simpleLogin,
|
||||
DateTime? gitHubSignInEnds,
|
||||
});
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'__className__': 'halkyon_base.InstanceBasics',
|
||||
'app': app,
|
||||
if (gitHubClientId != null) 'gitHubClientId': gitHubClientId,
|
||||
'emailSignIn': emailSignIn,
|
||||
'sourceCodeUrl': sourceCodeUrl,
|
||||
'minimumAppBuild': minimumAppBuild,
|
||||
|
|
@ -142,8 +119,6 @@ abstract class InstanceBasics
|
|||
if (backupLocation != null) 'backupLocation': backupLocation,
|
||||
'passwordSignIn': passwordSignIn,
|
||||
'simpleLogin': simpleLogin,
|
||||
if (gitHubSignInEnds != null)
|
||||
'gitHubSignInEnds': gitHubSignInEnds?.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -152,7 +127,6 @@ abstract class InstanceBasics
|
|||
return {
|
||||
'__className__': 'halkyon_base.InstanceBasics',
|
||||
'app': app,
|
||||
if (gitHubClientId != null) 'gitHubClientId': gitHubClientId,
|
||||
'emailSignIn': emailSignIn,
|
||||
'sourceCodeUrl': sourceCodeUrl,
|
||||
'minimumAppBuild': minimumAppBuild,
|
||||
|
|
@ -160,8 +134,6 @@ abstract class InstanceBasics
|
|||
if (backupLocation != null) 'backupLocation': backupLocation,
|
||||
'passwordSignIn': passwordSignIn,
|
||||
'simpleLogin': simpleLogin,
|
||||
if (gitHubSignInEnds != null)
|
||||
'gitHubSignInEnds': gitHubSignInEnds?.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -176,7 +148,6 @@ class _Undefined {}
|
|||
class _InstanceBasicsImpl extends InstanceBasics {
|
||||
_InstanceBasicsImpl({
|
||||
required String app,
|
||||
String? gitHubClientId,
|
||||
required bool emailSignIn,
|
||||
required String sourceCodeUrl,
|
||||
required int minimumAppBuild,
|
||||
|
|
@ -184,10 +155,8 @@ class _InstanceBasicsImpl extends InstanceBasics {
|
|||
String? backupLocation,
|
||||
bool? passwordSignIn,
|
||||
bool? simpleLogin,
|
||||
DateTime? gitHubSignInEnds,
|
||||
}) : super._(
|
||||
app: app,
|
||||
gitHubClientId: gitHubClientId,
|
||||
emailSignIn: emailSignIn,
|
||||
sourceCodeUrl: sourceCodeUrl,
|
||||
minimumAppBuild: minimumAppBuild,
|
||||
|
|
@ -195,7 +164,6 @@ class _InstanceBasicsImpl extends InstanceBasics {
|
|||
backupLocation: backupLocation,
|
||||
passwordSignIn: passwordSignIn,
|
||||
simpleLogin: simpleLogin,
|
||||
gitHubSignInEnds: gitHubSignInEnds,
|
||||
);
|
||||
|
||||
/// Returns a shallow copy of this [InstanceBasics]
|
||||
|
|
@ -204,7 +172,6 @@ class _InstanceBasicsImpl extends InstanceBasics {
|
|||
@override
|
||||
InstanceBasics copyWith({
|
||||
String? app,
|
||||
Object? gitHubClientId = _Undefined,
|
||||
bool? emailSignIn,
|
||||
String? sourceCodeUrl,
|
||||
int? minimumAppBuild,
|
||||
|
|
@ -212,13 +179,9 @@ class _InstanceBasicsImpl extends InstanceBasics {
|
|||
Object? backupLocation = _Undefined,
|
||||
bool? passwordSignIn,
|
||||
bool? simpleLogin,
|
||||
Object? gitHubSignInEnds = _Undefined,
|
||||
}) {
|
||||
return InstanceBasics(
|
||||
app: app ?? this.app,
|
||||
gitHubClientId: gitHubClientId is String?
|
||||
? gitHubClientId
|
||||
: this.gitHubClientId,
|
||||
emailSignIn: emailSignIn ?? this.emailSignIn,
|
||||
sourceCodeUrl: sourceCodeUrl ?? this.sourceCodeUrl,
|
||||
minimumAppBuild: minimumAppBuild ?? this.minimumAppBuild,
|
||||
|
|
@ -228,9 +191,6 @@ class _InstanceBasicsImpl extends InstanceBasics {
|
|||
: this.backupLocation,
|
||||
passwordSignIn: passwordSignIn ?? this.passwordSignIn,
|
||||
simpleLogin: simpleLogin ?? this.simpleLogin,
|
||||
gitHubSignInEnds: gitHubSignInEnds is DateTime?
|
||||
? gitHubSignInEnds
|
||||
: this.gitHubSignInEnds,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,10 +41,6 @@ enum PasswordRefusal implements _is.SerializableModel {
|
|||
/// The account has a password already.
|
||||
passwordExists,
|
||||
|
||||
/// The account has no email address yet: a password needs one, for
|
||||
/// resets. A code mailed to one makes it the account's.
|
||||
noAddress,
|
||||
|
||||
/// The address the code proved is another account's.
|
||||
addressTaken,
|
||||
|
||||
|
|
@ -84,11 +80,6 @@ enum PasswordRefusal implements _is.SerializableModel {
|
|||
/// sign in again.
|
||||
sessionEnded,
|
||||
|
||||
/// A first password for an account that signs in with GitHub alone takes
|
||||
/// a GitHub sign-in of the last 10 minutes (the fourth reading: a session
|
||||
/// alone sets none): sign in with GitHub again, then set it.
|
||||
signInAgain,
|
||||
|
||||
/// The warning that a session of the account was ended for its wrong
|
||||
/// proofs is dismissed only by a session made since (the full reading's
|
||||
/// L1: another session of a stolen device would dismiss its only use):
|
||||
|
|
@ -111,8 +102,6 @@ enum PasswordRefusal implements _is.SerializableModel {
|
|||
return PasswordRefusal.invalidSetup;
|
||||
case 'passwordExists':
|
||||
return PasswordRefusal.passwordExists;
|
||||
case 'noAddress':
|
||||
return PasswordRefusal.noAddress;
|
||||
case 'addressTaken':
|
||||
return PasswordRefusal.addressTaken;
|
||||
case 'noPassword':
|
||||
|
|
@ -135,8 +124,6 @@ enum PasswordRefusal implements _is.SerializableModel {
|
|||
return PasswordRefusal.codesWait;
|
||||
case 'sessionEnded':
|
||||
return PasswordRefusal.sessionEnded;
|
||||
case 'signInAgain':
|
||||
return PasswordRefusal.signInAgain;
|
||||
case 'signedInBefore':
|
||||
return PasswordRefusal.signedInBefore;
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -25,8 +25,6 @@ import 'auth/email_code_request.dart' as _iqxum1j8;
|
|||
import 'auth/email_identity.dart' as _id1dpd4g;
|
||||
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;
|
||||
import 'auth/github_sign_in_refused_exception.dart' as _i0xfcu5n;
|
||||
import 'auth/sign_in_ways.dart' as _ie6tbh85;
|
||||
import 'instance/instance_basics.dart' as _i8jixqx6;
|
||||
import 'keys/device_keys.dart' as _iz50cwqw;
|
||||
|
|
@ -76,8 +74,6 @@ export 'auth/email_code_request.dart';
|
|||
export 'auth/email_identity.dart';
|
||||
export 'auth/email_sign_in_refusal.dart';
|
||||
export 'auth/email_sign_in_refused_exception.dart';
|
||||
export 'auth/github_sign_in_refusal.dart';
|
||||
export 'auth/github_sign_in_refused_exception.dart';
|
||||
export 'auth/sign_in_ways.dart';
|
||||
export 'instance/instance_basics.dart';
|
||||
export 'keys/device_keys.dart';
|
||||
|
|
@ -1087,12 +1083,6 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
if (t == _iztfgag5.EmailSignInRefusedException) {
|
||||
return _iztfgag5.EmailSignInRefusedException.fromJson(data) as T;
|
||||
}
|
||||
if (t == _ir1gd9p5.GitHubSignInRefusal) {
|
||||
return _ir1gd9p5.GitHubSignInRefusal.fromJson(data) as T;
|
||||
}
|
||||
if (t == _i0xfcu5n.GitHubSignInRefusedException) {
|
||||
return _i0xfcu5n.GitHubSignInRefusedException.fromJson(data) as T;
|
||||
}
|
||||
if (t == _ie6tbh85.SignInWays) {
|
||||
return _ie6tbh85.SignInWays.fromJson(data) as T;
|
||||
}
|
||||
|
|
@ -1247,18 +1237,6 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
: null)
|
||||
as T;
|
||||
}
|
||||
if (t == _is.getType<_ir1gd9p5.GitHubSignInRefusal?>()) {
|
||||
return (data != null
|
||||
? _ir1gd9p5.GitHubSignInRefusal.fromJson(data)
|
||||
: null)
|
||||
as T;
|
||||
}
|
||||
if (t == _is.getType<_i0xfcu5n.GitHubSignInRefusedException?>()) {
|
||||
return (data != null
|
||||
? _i0xfcu5n.GitHubSignInRefusedException.fromJson(data)
|
||||
: null)
|
||||
as T;
|
||||
}
|
||||
if (t == _is.getType<_ie6tbh85.SignInWays?>()) {
|
||||
return (data != null ? _ie6tbh85.SignInWays.fromJson(data) : null) as T;
|
||||
}
|
||||
|
|
@ -1486,8 +1464,6 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
_id1dpd4g.EmailIdentity => 'EmailIdentity',
|
||||
_ieyvcag1.EmailSignInRefusal => 'EmailSignInRefusal',
|
||||
_iztfgag5.EmailSignInRefusedException => 'EmailSignInRefusedException',
|
||||
_ir1gd9p5.GitHubSignInRefusal => 'GitHubSignInRefusal',
|
||||
_i0xfcu5n.GitHubSignInRefusedException => 'GitHubSignInRefusedException',
|
||||
_ie6tbh85.SignInWays => 'SignInWays',
|
||||
_i8jixqx6.InstanceBasics => 'InstanceBasics',
|
||||
_iz50cwqw.DeviceKeys => 'DeviceKeys',
|
||||
|
|
@ -1559,10 +1535,6 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
return 'EmailSignInRefusal';
|
||||
case _iztfgag5.EmailSignInRefusedException():
|
||||
return 'EmailSignInRefusedException';
|
||||
case _ir1gd9p5.GitHubSignInRefusal():
|
||||
return 'GitHubSignInRefusal';
|
||||
case _i0xfcu5n.GitHubSignInRefusedException():
|
||||
return 'GitHubSignInRefusedException';
|
||||
case _ie6tbh85.SignInWays():
|
||||
return 'SignInWays';
|
||||
case _i8jixqx6.InstanceBasics():
|
||||
|
|
@ -1680,12 +1652,6 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
if (dataClassName == 'EmailSignInRefusedException') {
|
||||
return deserialize<_iztfgag5.EmailSignInRefusedException>(data['data']);
|
||||
}
|
||||
if (dataClassName == 'GitHubSignInRefusal') {
|
||||
return deserialize<_ir1gd9p5.GitHubSignInRefusal>(data['data']);
|
||||
}
|
||||
if (dataClassName == 'GitHubSignInRefusedException') {
|
||||
return deserialize<_i0xfcu5n.GitHubSignInRefusedException>(data['data']);
|
||||
}
|
||||
if (dataClassName == 'SignInWays') {
|
||||
return deserialize<_ie6tbh85.SignInWays>(data['data']);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@ account:
|
|||
- signInWays:
|
||||
emailCode:
|
||||
- sendCode:
|
||||
gitHubSignIn:
|
||||
- signIn:
|
||||
instance:
|
||||
- basics:
|
||||
keys:
|
||||
|
|
|
|||
|
|
@ -7,9 +7,6 @@ fields:
|
|||
### Which app this instance serves (`learn` for Halkyon Learn), as its host
|
||||
### configures it: an app accepts only an instance of its own.
|
||||
app: String
|
||||
### The client ID of this instance's GitHub OAuth app, or null when GitHub
|
||||
### sign-in is off here.
|
||||
gitHubClientId: String?
|
||||
### Whether people can sign in with an email address here: a code mailed
|
||||
### to it.
|
||||
emailSignIn: bool
|
||||
|
|
@ -33,8 +30,3 @@ fields:
|
|||
### Whether people sign in with SimpleLogin here (ADR 0020): the
|
||||
### instance's register has a SimpleLogin app, and its operator offers it.
|
||||
simpleLogin: bool, default=false
|
||||
### When GitHub sign-in ends here (ADR 0017: it is deprecated), as its
|
||||
### operator set it (GITHUB_SIGN_IN_ENDS), in UTC; null while no end is set,
|
||||
### or once it has ended. People who sign in with GitHub add an address, a
|
||||
### password and an authenticator before then.
|
||||
gitHubSignInEnds: DateTime?
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import 'package:halkyon_base_server/src/auth/email.dart';
|
||||
import 'package:halkyon_base_server/src/auth/github.dart';
|
||||
import 'package:halkyon_base_server/src/auth/membership.dart';
|
||||
import 'package:halkyon_base_server/src/generated/protocol.dart';
|
||||
import 'package:halkyon_base_server/src/halkyon_base.dart';
|
||||
|
|
@ -14,22 +13,18 @@ import 'package:serverpod/serverpod.dart';
|
|||
/// What every Halkyon instance tells its apps. Public: an app asks before
|
||||
/// anyone is signed in.
|
||||
class InstanceEndpoint extends Endpoint {
|
||||
/// This instance's basics: the app it serves, GitHub sign-in only with the
|
||||
/// operator's OAuth app and until it ends (ADR 0017), email sign-in only
|
||||
/// where the register can mail codes (this server, or on a member the
|
||||
/// register, ADR 0016), password sign-in where the register holds its
|
||||
/// keys, SimpleLogin where the register has the instance's SimpleLogin app
|
||||
/// (ADR 0020), each as its operator offers it, when GitHub sign-in ends,
|
||||
/// where its source is, the oldest app build it works with, and where it
|
||||
/// keeps people's data and its backups.
|
||||
/// This instance's basics: the app it serves, email sign-in only where
|
||||
/// the register can mail codes (this server, or on a member the register,
|
||||
/// ADR 0016), password sign-in where the register holds its keys,
|
||||
/// SimpleLogin where the register has the instance's SimpleLogin app
|
||||
/// (ADR 0020), each as its operator offers it, where its source is, the
|
||||
/// oldest app build it works with, and where it keeps people's data and
|
||||
/// its backups.
|
||||
Future<InstanceBasics> basics(Session session) async {
|
||||
final pod = session.serverpod;
|
||||
final base = HalkyonBase.of(pod);
|
||||
return InstanceBasics(
|
||||
app: base.app,
|
||||
gitHubClientId: gitHubSignInAvailable(pod) && !gitHubSignInEnded(pod)
|
||||
? pod.getPassword('githubClientId')
|
||||
: null,
|
||||
emailSignIn: isMember(pod)
|
||||
? await _registerOffers(session, (register) {
|
||||
return register.emailSignInOffered(session);
|
||||
|
|
@ -49,7 +44,6 @@ class InstanceEndpoint extends Endpoint {
|
|||
minimumAppBuild: base.minimumAppBuild,
|
||||
dataLocation: dataLocation(),
|
||||
backupLocation: backupLocation(),
|
||||
gitHubSignInEnds: gitHubSignInEnded(pod) ? null : gitHubSignInEnds(pod),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue