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
29 changed files with 564 additions and 3570 deletions

View file

@ -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,

View file

@ -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) {

View file

@ -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);

View file

@ -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),
];

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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;
}

View file

@ -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);
}
}

View file

@ -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,

View file

@ -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']!,

View file

@ -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,
);
}
}

View file

@ -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:

View file

@ -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']);
}

View file

@ -3,8 +3,6 @@ account:
- signInWays:
emailCode:
- sendCode:
gitHubSignIn:
- signIn:
instance:
- basics:
keys:

View file

@ -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?

View file

@ -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),
);
}
}

View file

@ -50,10 +50,9 @@ Future<DeviceKeys> unlockKeys(
}
/// Gives [person], an account without keys that SimpleLogin was added to
/// (one of before that signs in with GitHub or a code), the
/// keys [setup] describes, and [handle] where it has none (ADR 0020, 6 and
/// D5), with their SimpleLogin sign-in under [attempt] of the last ten
/// minutes, taken (D20). Nothing else proves the owner: the account has no
/// (one of before), the keys [setup] describes, and [handle] where it has
/// none (ADR 0020, 6 and D5), with their SimpleLogin sign-in under [attempt]
/// of the last ten minutes, taken (D20). Nothing else proves the owner: the account has no
/// password yet, and nothing encrypted under keys of its own.
///
/// Refused ([PasswordRefusedException]) for a setup out of range

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