feat(auth): a password with TOTP on the register, and resets
The register's side of signing in with an address, a password that never leaves the device (SRP-6a) and a code from an authenticator (ADR 0017), with members forwarding every step through AccountRegister: - password.set: salt, Argon2id's costs, SRP's verifier, the public keys and the private keys wrapped twice, all made on the device; with the address a mailed code proves, an anonymous person becomes identified under their ID. It signs in once an authenticator is confirmed. - passwordSignIn.begin and complete: SRP's two steps with a TOTP or recovery code. An address nobody has is answered from a decoy key as an account would be; every wrong credential is refused alike. - authenticator: secrets sealed under totpKey (XChaCha20-Poly1305, bound to their row), each code accepted once, ten recovery codes with the first, the last one kept. - accountRecovery: a lost authenticator reset 72 hours after the password and a mailed code or the recovery phrase ask, a lost password set anew with the phrase's signature, or the account reset with new keys 72 hours after a mailed code asks; a mail at each start, a sign-in calling it off. - An account with a password no longer signs in by a mailed code or GitHub; no new GitHub account is linked where passwords are offered, and from GITHUB_SIGN_IN_ENDS GitHub signs nobody in and its IDs go. Five tables, added by the module's migration; deletePerson deletes their rows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
200a2df32f
commit
a3a8ed3890
96 changed files with 22557 additions and 60 deletions
|
|
@ -2,8 +2,8 @@
|
|||
/// (`HalkyonBase.configure`), how people authenticate, the account's
|
||||
/// deletion and the pruning, email sign-in's helpers, the instance's
|
||||
/// basics, people's public profiles and their pages, the instance's register
|
||||
/// (ADR 0016), SRP's register side, TOTP and recovery codes (ADR 0017), and
|
||||
/// the module's models.
|
||||
/// (ADR 0016), signing in with a password and TOTP and getting back into an
|
||||
/// account (ADR 0017), and the module's models.
|
||||
library;
|
||||
|
||||
export 'src/auth/account.dart';
|
||||
|
|
@ -11,6 +11,7 @@ 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/scopes.dart';
|
||||
export 'src/common/short_code.dart';
|
||||
|
|
@ -18,6 +19,22 @@ export 'src/generated/protocol.dart' hide Protocol;
|
|||
export 'src/halkyon_base.dart';
|
||||
export 'src/instance/data_location.dart';
|
||||
export 'src/instance/source_code.dart';
|
||||
export 'src/password/authenticators.dart'
|
||||
show
|
||||
forgetUnconfirmedAuthenticators,
|
||||
maxAuthenticators,
|
||||
unconfirmedAuthenticatorLifetime;
|
||||
export 'src/password/key_proof.dart' show keyProofMessage;
|
||||
export 'src/password/password_config.dart';
|
||||
export 'src/password/passwords.dart'
|
||||
show
|
||||
newPasswordCosts,
|
||||
passwordGroup,
|
||||
passwordMethod,
|
||||
resetDelay,
|
||||
signInChallengeLifetime,
|
||||
signsInWithPassword,
|
||||
wrappedKeysLength;
|
||||
export 'src/profile/link_verification.dart';
|
||||
export 'src/profile/profile_page.dart' show addProfilePageRoute, profilePage;
|
||||
export 'src/profile/profile_rules.dart'
|
||||
|
|
|
|||
|
|
@ -12,8 +12,9 @@ import 'package:serverpod_auth_idp_server/core.dart';
|
|||
/// this order: their sessions end first, since a device with a valid session
|
||||
/// would upload everything again; then the host deletes their data in its
|
||||
/// own tables ([HalkyonBase.deletePersonData], in the order given); then
|
||||
/// their public profile with its avatar, their address's hash and the person
|
||||
/// with their other identities. On the instance's register, an account (an
|
||||
/// their public profile with its avatar, their address's hash, their
|
||||
/// password with their keys, authenticators and recovery codes, and the
|
||||
/// person with their other identities. On the instance's register, an account (an
|
||||
/// identified person) going leaves a notice for each member in the same
|
||||
/// transaction, and the members are told once it holds (ADR 0016). Once the
|
||||
/// transaction holds, what the host's deletions left for afterwards runs.
|
||||
|
|
@ -77,6 +78,38 @@ Future<void> deletePerson(Session session, UuidValue authUserId) async {
|
|||
where: (t) => t.authUserId.equals(authUserId),
|
||||
transaction: transaction,
|
||||
);
|
||||
// Their password with their keys, authenticators, recovery codes, resets
|
||||
// and sign-ins begun (ADR 0017).
|
||||
// DELETE FROM "account_password" WHERE "authUserId" = $1
|
||||
await AccountPassword.db.deleteWhere(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(authUserId),
|
||||
transaction: transaction,
|
||||
);
|
||||
// DELETE FROM "totp_authenticator" WHERE "authUserId" = $1
|
||||
await TotpAuthenticator.db.deleteWhere(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(authUserId),
|
||||
transaction: transaction,
|
||||
);
|
||||
// DELETE FROM "recovery_code" WHERE "authUserId" = $1
|
||||
await RecoveryCode.db.deleteWhere(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(authUserId),
|
||||
transaction: transaction,
|
||||
);
|
||||
// DELETE FROM "account_reset" WHERE "authUserId" = $1
|
||||
await AccountReset.db.deleteWhere(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(authUserId),
|
||||
transaction: transaction,
|
||||
);
|
||||
// DELETE FROM "sign_in_challenge" WHERE "authUserId" = $1
|
||||
await SignInChallenge.db.deleteWhere(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(authUserId),
|
||||
transaction: transaction,
|
||||
);
|
||||
// Their GitHub id and anonymous account go with them (ON DELETE CASCADE).
|
||||
// Gone already on a member the register told first (ADR 0016).
|
||||
// DELETE FROM "serverpod_auth_core_user" WHERE "id" = $1
|
||||
|
|
@ -133,8 +166,9 @@ Future<int> pruneAbandonedAnonymousPeople(
|
|||
/// rate limit looks back ([anonymousSignUpLimit]) at [now], and returns how
|
||||
/// many: each holds the IP address it came from, which the instance keeps no
|
||||
/// longer than the limit needs it. Serverpod itself deletes them only for its
|
||||
/// own email sign-in, which Halkyon servers don't use. Email sign-in's notes
|
||||
/// go too: none of its limits looks back further.
|
||||
/// own email sign-in, which Halkyon servers don't use. Email sign-in's and
|
||||
/// password sign-in's notes go too: none of their limits looks back
|
||||
/// further.
|
||||
Future<int> forgetOldSignUpAttempts(
|
||||
Session session, {
|
||||
required DateTime now,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:halkyon_base_server/src/generated/protocol.dart';
|
||||
import 'package:mailer/mailer.dart' as mailer;
|
||||
import 'package:mailer/smtp_server.dart';
|
||||
import 'package:serverpod/serverpod.dart';
|
||||
|
|
@ -5,9 +6,10 @@ import 'package:serverpod/serverpod.dart';
|
|||
// The mail that carries an email sign-in code (Halkyon Learn's ADR 0010):
|
||||
// plain text, the code in its subject so it shows in the notification and
|
||||
// the system can offer it, no links, no tracking. Sent over SMTP, through any
|
||||
// service the operator chooses (Proton's, for one).
|
||||
// service the operator chooses (Proton's, for one). The same service tells
|
||||
// an account's address of a reset asked for (ADR 0017).
|
||||
|
||||
/// Mails sign-in codes.
|
||||
/// Mails sign-in codes, and notices of resets.
|
||||
abstract interface class CodeMailer {
|
||||
/// Mails [code] to [to], worded in [language] (`en` or `de`).
|
||||
Future<void> send(
|
||||
|
|
@ -16,6 +18,17 @@ abstract interface class CodeMailer {
|
|||
required String code,
|
||||
required String language,
|
||||
});
|
||||
|
||||
/// Tells [to], worded in [language], that a reset of the account's [kind]
|
||||
/// was asked for, taking effect at [takesEffectAt] unless its owner signs
|
||||
/// in before ([resetMail]).
|
||||
Future<void> sendResetNotice(
|
||||
Session session, {
|
||||
required String to,
|
||||
required AccountResetKind kind,
|
||||
required DateTime takesEffectAt,
|
||||
required String language,
|
||||
});
|
||||
}
|
||||
|
||||
/// Writes codes into the server's log, for development, where nobody has a
|
||||
|
|
@ -31,6 +44,17 @@ final class LogCodeMailer implements CodeMailer {
|
|||
required String code,
|
||||
required String language,
|
||||
}) async => session.log('Email sign-in code for $to: $code');
|
||||
|
||||
@override
|
||||
Future<void> sendResetNotice(
|
||||
Session session, {
|
||||
required String to,
|
||||
required AccountResetKind kind,
|
||||
required DateTime takesEffectAt,
|
||||
required String language,
|
||||
}) async => session.log(
|
||||
'Reset notice for $to: ${kind.name}, ${_utc(takesEffectAt)}',
|
||||
);
|
||||
}
|
||||
|
||||
/// A code's mail, worded in [language] (`en`, else German for `de`): its
|
||||
|
|
@ -74,6 +98,81 @@ final class LogCodeMailer implements CodeMailer {
|
|||
),
|
||||
};
|
||||
|
||||
/// A reset's notice (ADR 0017), worded in [language] (`en`, else German for
|
||||
/// `de`): its subject and its body. [kind] says what goes at [takesEffectAt]
|
||||
/// (shown in UTC), [app] and [instance] as in [codeMail]. It names no code
|
||||
/// and no link: whoever asked must not be helped by it, and the owner needs
|
||||
/// none to call it off, only to sign in.
|
||||
({String subject, String body}) resetMail({
|
||||
required AccountResetKind kind,
|
||||
required DateTime takesEffectAt,
|
||||
required String language,
|
||||
required String app,
|
||||
required String instance,
|
||||
}) {
|
||||
final when = _utc(takesEffectAt);
|
||||
return switch ((language, kind)) {
|
||||
('de', AccountResetKind.authenticator) => (
|
||||
subject: 'Die Anmeldecodes deines $app-Kontos werden abgeschaltet',
|
||||
body:
|
||||
'Jemand hat mit deinem Passwort angefragt, die Codes deiner '
|
||||
'Authenticator-App für dein Konto auf $instance abzuschalten. Das '
|
||||
'geschieht am $when, außer du meldest dich vorher mit deinem '
|
||||
'Passwort und einem Code deiner Authenticator-App an: Das bricht es '
|
||||
'ab.\n'
|
||||
'\n'
|
||||
'Warst du es, meldest du dich danach mit deinem Passwort allein an '
|
||||
'und richtest wieder eine Authenticator-App ein.\n'
|
||||
'\n'
|
||||
'Warst du es nicht, melde dich jetzt an: Wer gefragt hat, kennt dein '
|
||||
'Passwort.\n',
|
||||
),
|
||||
('de', AccountResetKind.account) => (
|
||||
subject: 'Dein $app-Konto wird zurückgesetzt',
|
||||
body:
|
||||
'Jemand hat mit einem Code an diese Adresse angefragt, dein Konto '
|
||||
'auf $instance zurückzusetzen: ein neues Passwort und neue '
|
||||
'Schlüssel. Was mit den alten Schlüsseln verschlüsselt ist, kann '
|
||||
'danach niemand mehr lesen. Das geschieht frühestens am $when, außer '
|
||||
'du meldest dich vorher mit deinem Passwort an: Das bricht es ab.\n'
|
||||
'\n'
|
||||
'Warst du es nicht, melde dich jetzt an.\n',
|
||||
),
|
||||
(_, AccountResetKind.authenticator) => (
|
||||
subject: 'The sign-in codes of your $app account will be turned off',
|
||||
body:
|
||||
'Someone asked, with your password, to turn off the codes from your '
|
||||
'authenticator app for your account on $instance. This happens on '
|
||||
'$when, unless you sign in with your password and a code from your '
|
||||
'authenticator app before then: that calls it off.\n'
|
||||
'\n'
|
||||
'If it was you, sign in with your password alone after that, and '
|
||||
'set up an authenticator app again.\n'
|
||||
'\n'
|
||||
"If it wasn't you, sign in now: whoever asked knows your password.\n",
|
||||
),
|
||||
(_, AccountResetKind.account) => (
|
||||
subject: 'Your $app account will be reset',
|
||||
body:
|
||||
'Someone asked, with a code mailed to this address, to reset your '
|
||||
'account on $instance: a new password and new keys. What was '
|
||||
'encrypted with the old keys can no longer be read by anyone. This '
|
||||
'happens on $when at the earliest, unless you sign in with your '
|
||||
'password before then: that calls it off.\n'
|
||||
'\n'
|
||||
"If it wasn't you, sign in now.\n",
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/// [time] as a reset's notice names it: to the minute, in UTC.
|
||||
String _utc(DateTime time) {
|
||||
final utc = time.toUtc();
|
||||
String two(int value) => value.toString().padLeft(2, '0');
|
||||
return '${utc.year}-${two(utc.month)}-${two(utc.day)} '
|
||||
'${two(utc.hour)}:${two(utc.minute)} UTC';
|
||||
}
|
||||
|
||||
/// Mails codes through an SMTP service, over TLS only: directly on port
|
||||
/// 465, upgraded with STARTTLS elsewhere (Proton's: `smtp.protonmail.ch`,
|
||||
/// 587). It logs nothing of a mail, neither its address nor its code.
|
||||
|
|
@ -134,19 +233,39 @@ final class SmtpCodeMailer implements CodeMailer {
|
|||
required String language,
|
||||
}) => mail(to: to, code: code, language: language);
|
||||
|
||||
@override
|
||||
Future<void> sendResetNotice(
|
||||
Session session, {
|
||||
required String to,
|
||||
required AccountResetKind kind,
|
||||
required DateTime takesEffectAt,
|
||||
required String language,
|
||||
}) => _deliver(
|
||||
to,
|
||||
resetMail(
|
||||
kind: kind,
|
||||
takesEffectAt: takesEffectAt,
|
||||
language: language,
|
||||
app: _app,
|
||||
instance: _instance,
|
||||
),
|
||||
);
|
||||
|
||||
/// Mails [code] to [to], worded in [language]; throws what the service
|
||||
/// answered when it took no mail.
|
||||
Future<void> mail({
|
||||
required String to,
|
||||
required String code,
|
||||
required String language,
|
||||
}) async {
|
||||
final mail = codeMail(
|
||||
code: code,
|
||||
language: language,
|
||||
app: _app,
|
||||
instance: _instance,
|
||||
);
|
||||
}) => _deliver(
|
||||
to,
|
||||
codeMail(code: code, language: language, app: _app, instance: _instance),
|
||||
);
|
||||
|
||||
Future<void> _deliver(
|
||||
String to,
|
||||
({String subject, String body}) mail,
|
||||
) async {
|
||||
await mailer.send(
|
||||
mailer.Message()
|
||||
..from = _from
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import 'package:halkyon_base_server/src/auth/identified.dart';
|
|||
import 'package:halkyon_base_server/src/common/short_code.dart';
|
||||
import 'package:halkyon_base_server/src/generated/protocol.dart';
|
||||
import 'package:halkyon_base_server/src/halkyon_base.dart';
|
||||
import 'package:halkyon_base_server/src/password/passwords.dart';
|
||||
import 'package:halkyon_base_server/src/register/people.dart';
|
||||
import 'package:serverpod/serverpod.dart';
|
||||
import 'package:serverpod_auth_idp_server/core.dart';
|
||||
|
|
@ -197,11 +198,13 @@ Future<UuidValue> sendSignInCode(
|
|||
/// have; a member's person becomes an account of the register under their
|
||||
/// ID, ADR 0016); their own changes nothing; another person's changes nothing
|
||||
/// here and gives a token that signs in as that person
|
||||
/// ([completeEmailSwitch]).
|
||||
/// ([completeEmailSwitch]), unless that person signs in with a password
|
||||
/// ([signsInWithPassword], ADR 0017).
|
||||
///
|
||||
/// Refused for a wrong code, too many tries, a code no longer valid, or a
|
||||
/// second address for someone who has one ([EmailSignInRefusedException]);
|
||||
/// nothing changes then.
|
||||
/// Refused for a wrong code, too many tries, a code no longer valid, a
|
||||
/// second address for someone who has one, or another person's address who
|
||||
/// signs in with a password ([EmailSignInRefusal.usePassword])
|
||||
/// ([EmailSignInRefusedException]); nothing changes then.
|
||||
Future<EmailCodeAccepted> acceptSignInCode(
|
||||
Session session, {
|
||||
required UuidValue authUserId,
|
||||
|
|
@ -239,6 +242,15 @@ Future<EmailCodeAccepted> acceptSignInCode(
|
|||
.where((identity) => identity.addressHash == accepted.addressHash)
|
||||
.firstOrNull;
|
||||
if (owner != null && owner.authUserId != authUserId) {
|
||||
if (await signsInWithPassword(
|
||||
session,
|
||||
owner.authUserId,
|
||||
transaction: transaction,
|
||||
)) {
|
||||
throw EmailSignInRefusedException(
|
||||
reason: EmailSignInRefusal.usePassword,
|
||||
);
|
||||
}
|
||||
// Kept until the token is used, or its time is up.
|
||||
return EmailCodeAccepted(switchToken: switchToken);
|
||||
}
|
||||
|
|
@ -274,11 +286,45 @@ Future<EmailCodeAccepted> acceptSignInCode(
|
|||
});
|
||||
}
|
||||
|
||||
/// The keyed hash of the address [code] proves for [request], once: the
|
||||
/// request goes as it is accepted. For what an address proves without
|
||||
/// becoming anyone's way in: resets of an account (ADR 0017), whose register
|
||||
/// compares the hash with the account's. Refusals as [acceptSignInCode]'s
|
||||
/// ([EmailSignInRefusedException]).
|
||||
Future<String> proveAddressCode(
|
||||
Session session, {
|
||||
required UuidValue request,
|
||||
required String code,
|
||||
}) async {
|
||||
final pod = session.serverpod;
|
||||
if (!emailSignInAvailable(pod)) {
|
||||
throw EmailSignInRefusedException(reason: EmailSignInRefusal.unavailable);
|
||||
}
|
||||
return await session.db.transaction((transaction) async {
|
||||
await _refusingAsEmail(
|
||||
() => _challengesOf(pod).verifyChallenge(
|
||||
session,
|
||||
requestId: request,
|
||||
verificationCode: normalizeShortCode(code),
|
||||
transaction: transaction,
|
||||
),
|
||||
);
|
||||
final proven = (await _requestById(
|
||||
session,
|
||||
request,
|
||||
transaction: transaction,
|
||||
))!;
|
||||
await _forget(session, [proven], transaction: transaction);
|
||||
return proven.addressHash;
|
||||
});
|
||||
}
|
||||
|
||||
/// The person whose address a code proved, from the token
|
||||
/// [acceptSignInCode] gave; once only. A session for them is the caller's to
|
||||
/// issue (`issueSession`): this server's own, or a member's (ADR 0016).
|
||||
/// Refused once the token was used or its time is up, or for a token it
|
||||
/// never gave ([EmailSignInRefusal.codeExpired]).
|
||||
/// never gave ([EmailSignInRefusal.codeExpired]), and for a person who signs
|
||||
/// in with a password since ([EmailSignInRefusal.usePassword]).
|
||||
Future<UuidValue> completeEmailSwitch(
|
||||
Session session, {
|
||||
required String switchToken,
|
||||
|
|
@ -308,6 +354,13 @@ Future<UuidValue> completeEmailSwitch(
|
|||
reason: EmailSignInRefusal.codeExpired,
|
||||
);
|
||||
}
|
||||
if (await signsInWithPassword(
|
||||
session,
|
||||
identity.authUserId,
|
||||
transaction: transaction,
|
||||
)) {
|
||||
throw EmailSignInRefusedException(reason: EmailSignInRefusal.usePassword);
|
||||
}
|
||||
return identity.authUserId;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,3 +22,6 @@ values:
|
|||
- otherAddressLinked
|
||||
### The mail couldn't be sent; nothing was kept of the request.
|
||||
- mailFailed
|
||||
### The account signs in with its password and an authenticator (ADR
|
||||
### 0017): a code by mail no longer signs into it.
|
||||
- usePassword
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:halkyon_base_server/src/auth/identified.dart';
|
||||
import 'package:halkyon_base_server/src/generated/protocol.dart';
|
||||
import 'package:halkyon_base_server/src/password/password_config.dart';
|
||||
import 'package:halkyon_base_server/src/register/people.dart';
|
||||
import 'package:serverpod/serverpod.dart';
|
||||
import 'package:serverpod_auth_idp_server/core.dart';
|
||||
|
|
@ -22,7 +26,70 @@ 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');
|
||||
|
||||
/// Deletes every GitHub ID the instance keeps once GitHub sign-in has ended
|
||||
/// at [now] ([gitHubSignInEnds]), 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).
|
||||
Future<int> forgetGitHubAccounts(
|
||||
Session session, {
|
||||
required DateTime now,
|
||||
}) async {
|
||||
final ends = gitHubSignInEnds(session.serverpod);
|
||||
if (ends == null || now.isBefore(ends)) {
|
||||
return 0;
|
||||
}
|
||||
// DELETE FROM "serverpod_auth_idp_github_account"
|
||||
final forgotten = await GitHubAccount.db.deleteWhere(
|
||||
session,
|
||||
where: (t) => t.id.notEquals(null),
|
||||
);
|
||||
return forgotten.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
|
||||
|
|
@ -33,6 +100,9 @@ Future<String> verifiedGitHubUserId(
|
|||
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,
|
||||
|
|
@ -61,7 +131,9 @@ Future<String> verifiedGitHubUserId(
|
|||
///
|
||||
/// Linking the account the person already has changes nothing. Refused, with
|
||||
/// nothing changed, when the account belongs to someone else or the person
|
||||
/// has another GitHub account ([GitHubSignInRefusedException]).
|
||||
/// has another GitHub account, and wherever people sign in with a password
|
||||
/// (ADR 0017: GitHub is deprecated, [GitHubSignInRefusal.deprecated])
|
||||
/// ([GitHubSignInRefusedException]).
|
||||
Future<void> linkGitHubAccount(
|
||||
Session session, {
|
||||
required UuidValue authUserId,
|
||||
|
|
@ -82,6 +154,11 @@ Future<void> linkGitHubAccount(
|
|||
)) {
|
||||
return;
|
||||
}
|
||||
if (passwordSignInAvailable(session.serverpod)) {
|
||||
throw GitHubSignInRefusedException(
|
||||
reason: GitHubSignInRefusal.deprecated,
|
||||
);
|
||||
}
|
||||
if (accounts.any((account) => account.userIdentifier == gitHubUserId)) {
|
||||
throw GitHubSignInRefusedException(
|
||||
reason: GitHubSignInRefusal.takenByAnotherPerson,
|
||||
|
|
|
|||
|
|
@ -9,9 +9,8 @@ import 'package:serverpod_auth_idp_server/core.dart';
|
|||
/// 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,
|
||||
/// whom the instance's register knows by it; an account it doesn't know
|
||||
/// is refused ([GitHubSignInRefusal.unknownAccount]).
|
||||
/// A session for the person with the GitHub account that [code] proves;
|
||||
/// refusals as [gitHubAccountToSignIn]'s.
|
||||
Future<AuthSuccess> signIn(
|
||||
Session session, {
|
||||
required String code,
|
||||
|
|
@ -24,13 +23,33 @@ class GitHubSignInEndpoint extends Endpoint {
|
|||
codeVerifier: codeVerifier,
|
||||
redirectUri: redirectUri,
|
||||
);
|
||||
final account =
|
||||
await AccountRegister.of(
|
||||
session.serverpod,
|
||||
).accountWithGitHub(session, gitHubUserId) ??
|
||||
(throw GitHubSignInRefusedException(
|
||||
reason: GitHubSignInRefusal.unknownAccount,
|
||||
));
|
||||
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). 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,
|
||||
));
|
||||
final ways = await register.signInWaysOf(session, account);
|
||||
if (ways.password && ways.authenticator) {
|
||||
throw GitHubSignInRefusedException(
|
||||
reason: GitHubSignInRefusal.usePassword,
|
||||
);
|
||||
}
|
||||
return account;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,3 +11,12 @@ values:
|
|||
- 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
|
||||
|
|
|
|||
|
|
@ -3,3 +3,7 @@ class: SignInWays
|
|||
fields:
|
||||
gitHub: bool
|
||||
email: bool
|
||||
### A password (ADR 0017), which an address needs for resets.
|
||||
password: bool, default=false
|
||||
### A confirmed authenticator: the password signs in only with one.
|
||||
authenticator: bool, default=false
|
||||
|
|
|
|||
|
|
@ -40,7 +40,11 @@ enum EmailSignInRefusal implements _is.SerializableModel {
|
|||
otherAddressLinked,
|
||||
|
||||
/// The mail couldn't be sent; nothing was kept of the request.
|
||||
mailFailed;
|
||||
mailFailed,
|
||||
|
||||
/// The account signs in with its password and an authenticator (ADR
|
||||
/// 0017): a code by mail no longer signs into it.
|
||||
usePassword;
|
||||
|
||||
static EmailSignInRefusal fromJson(String name) {
|
||||
switch (name) {
|
||||
|
|
@ -60,6 +64,8 @@ enum EmailSignInRefusal implements _is.SerializableModel {
|
|||
return EmailSignInRefusal.otherAddressLinked;
|
||||
case 'mailFailed':
|
||||
return EmailSignInRefusal.mailFailed;
|
||||
case 'usePassword':
|
||||
return EmailSignInRefusal.usePassword;
|
||||
default:
|
||||
throw ArgumentError(
|
||||
'Value "$name" cannot be converted to "EmailSignInRefusal"',
|
||||
|
|
|
|||
|
|
@ -25,7 +25,19 @@ enum GitHubSignInRefusal implements _is.SerializableModel {
|
|||
otherGitHubAccountLinked,
|
||||
|
||||
/// No person has this GitHub account; only linking one creates it.
|
||||
unknownAccount;
|
||||
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) {
|
||||
|
|
@ -37,6 +49,12 @@ enum GitHubSignInRefusal implements _is.SerializableModel {
|
|||
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"',
|
||||
|
|
|
|||
|
|
@ -18,17 +18,28 @@ abstract class SignInWays
|
|||
SignInWays._({
|
||||
required this.gitHub,
|
||||
required this.email,
|
||||
});
|
||||
bool? password,
|
||||
bool? authenticator,
|
||||
}) : password = password ?? false,
|
||||
authenticator = authenticator ?? false;
|
||||
|
||||
factory SignInWays({
|
||||
required bool gitHub,
|
||||
required bool email,
|
||||
bool? password,
|
||||
bool? authenticator,
|
||||
}) = _SignInWaysImpl;
|
||||
|
||||
factory SignInWays.fromJson(Map<String, dynamic> jsonSerialization) {
|
||||
return SignInWays(
|
||||
gitHub: _is.BoolJsonExtension.fromJson(jsonSerialization['gitHub']),
|
||||
email: _is.BoolJsonExtension.fromJson(jsonSerialization['email']),
|
||||
password: jsonSerialization['password'] == null
|
||||
? null
|
||||
: _is.BoolJsonExtension.fromJson(jsonSerialization['password']),
|
||||
authenticator: jsonSerialization['authenticator'] == null
|
||||
? null
|
||||
: _is.BoolJsonExtension.fromJson(jsonSerialization['authenticator']),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -36,12 +47,20 @@ abstract class SignInWays
|
|||
|
||||
bool email;
|
||||
|
||||
/// A password (ADR 0017), which an address needs for resets.
|
||||
bool password;
|
||||
|
||||
/// A confirmed authenticator: the password signs in only with one.
|
||||
bool authenticator;
|
||||
|
||||
/// Returns a shallow copy of this [SignInWays]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
SignInWays copyWith({
|
||||
bool? gitHub,
|
||||
bool? email,
|
||||
bool? password,
|
||||
bool? authenticator,
|
||||
});
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
|
|
@ -49,6 +68,8 @@ abstract class SignInWays
|
|||
'__className__': 'halkyon_base.SignInWays',
|
||||
'gitHub': gitHub,
|
||||
'email': email,
|
||||
'password': password,
|
||||
'authenticator': authenticator,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -58,6 +79,8 @@ abstract class SignInWays
|
|||
'__className__': 'halkyon_base.SignInWays',
|
||||
'gitHub': gitHub,
|
||||
'email': email,
|
||||
'password': password,
|
||||
'authenticator': authenticator,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -71,9 +94,13 @@ class _SignInWaysImpl extends SignInWays {
|
|||
_SignInWaysImpl({
|
||||
required bool gitHub,
|
||||
required bool email,
|
||||
bool? password,
|
||||
bool? authenticator,
|
||||
}) : super._(
|
||||
gitHub: gitHub,
|
||||
email: email,
|
||||
password: password,
|
||||
authenticator: authenticator,
|
||||
);
|
||||
|
||||
/// Returns a shallow copy of this [SignInWays]
|
||||
|
|
@ -83,10 +110,14 @@ class _SignInWaysImpl extends SignInWays {
|
|||
SignInWays copyWith({
|
||||
bool? gitHub,
|
||||
bool? email,
|
||||
bool? password,
|
||||
bool? authenticator,
|
||||
}) {
|
||||
return SignInWays(
|
||||
gitHub: gitHub ?? this.gitHub,
|
||||
email: email ?? this.email,
|
||||
password: password ?? this.password,
|
||||
authenticator: authenticator ?? this.authenticator,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,10 @@
|
|||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'dart:typed_data' as _idt;
|
||||
import 'package:halkyon_base_server/src/generated/password/new_password.dart'
|
||||
as _ijye8mcv;
|
||||
import 'package:halkyon_base_server/src/generated/password/password_setup.dart'
|
||||
as _ikhyee47;
|
||||
import 'package:serverpod/serverpod.dart' as _is;
|
||||
import 'package:serverpod_auth_core_server/serverpod_auth_core_server.dart'
|
||||
as _iacs;
|
||||
|
|
@ -23,6 +27,10 @@ import '../auth/email_sign_in_endpoint.dart' as _irqdog5c;
|
|||
import '../auth/github_sign_in_endpoint.dart' as _iasw79m5;
|
||||
import '../auth/github_upgrade_endpoint.dart' as _ivnugbf2;
|
||||
import '../instance/instance_endpoint.dart' as _i3g8bfc2;
|
||||
import '../password/account_recovery_endpoint.dart' as _ivnbjhab;
|
||||
import '../password/authenticator_endpoint.dart' as _i3xqawxj;
|
||||
import '../password/password_endpoint.dart' as _ig7fyg1b;
|
||||
import '../password/password_sign_in_endpoint.dart' as _ik13m562;
|
||||
import '../profile/profile_endpoint.dart' as _i6ky944g;
|
||||
|
||||
class Endpoints extends _is.EndpointDispatch {
|
||||
|
|
@ -71,6 +79,30 @@ class Endpoints extends _is.EndpointDispatch {
|
|||
'instance',
|
||||
'halkyon_base',
|
||||
),
|
||||
'accountRecovery': _ivnbjhab.AccountRecoveryEndpoint()
|
||||
..initialize(
|
||||
server,
|
||||
'accountRecovery',
|
||||
'halkyon_base',
|
||||
),
|
||||
'authenticator': _i3xqawxj.AuthenticatorEndpoint()
|
||||
..initialize(
|
||||
server,
|
||||
'authenticator',
|
||||
'halkyon_base',
|
||||
),
|
||||
'password': _ig7fyg1b.PasswordEndpoint()
|
||||
..initialize(
|
||||
server,
|
||||
'password',
|
||||
'halkyon_base',
|
||||
),
|
||||
'passwordSignIn': _ik13m562.PasswordSignInEndpoint()
|
||||
..initialize(
|
||||
server,
|
||||
'passwordSignIn',
|
||||
'halkyon_base',
|
||||
),
|
||||
'profile': _i6ky944g.ProfileEndpoint()
|
||||
..initialize(
|
||||
server,
|
||||
|
|
@ -311,6 +343,481 @@ class Endpoints extends _is.EndpointDispatch {
|
|||
),
|
||||
},
|
||||
);
|
||||
connectors['accountRecovery'] = _is.EndpointConnector(
|
||||
name: 'accountRecovery',
|
||||
endpoint: endpoints['accountRecovery']!,
|
||||
methodConnectors: {
|
||||
'sendCode': _is.MethodConnector(
|
||||
name: 'sendCode',
|
||||
params: {
|
||||
'address': _is.ParameterDescription(
|
||||
name: 'address',
|
||||
type: _is.getType<String>(),
|
||||
nullable: false,
|
||||
),
|
||||
'language': _is.ParameterDescription(
|
||||
name: 'language',
|
||||
type: _is.getType<String>(),
|
||||
nullable: false,
|
||||
),
|
||||
},
|
||||
call:
|
||||
(
|
||||
_is.Session session,
|
||||
Map<String, dynamic> params,
|
||||
) async =>
|
||||
(endpoints['accountRecovery']
|
||||
as _ivnbjhab.AccountRecoveryEndpoint)
|
||||
.sendCode(
|
||||
session,
|
||||
address: params['address'],
|
||||
language: params['language'],
|
||||
),
|
||||
),
|
||||
'resetAuthenticator': _is.MethodConnector(
|
||||
name: 'resetAuthenticator',
|
||||
params: {
|
||||
'challenge': _is.ParameterDescription(
|
||||
name: 'challenge',
|
||||
type: _is.getType<_is.UuidValue>(),
|
||||
nullable: false,
|
||||
),
|
||||
'clientValue': _is.ParameterDescription(
|
||||
name: 'clientValue',
|
||||
type: _is.getType<_idt.ByteData>(),
|
||||
nullable: false,
|
||||
),
|
||||
'clientProof': _is.ParameterDescription(
|
||||
name: 'clientProof',
|
||||
type: _is.getType<_idt.ByteData>(),
|
||||
nullable: false,
|
||||
),
|
||||
'address': _is.ParameterDescription(
|
||||
name: 'address',
|
||||
type: _is.getType<String>(),
|
||||
nullable: false,
|
||||
),
|
||||
'language': _is.ParameterDescription(
|
||||
name: 'language',
|
||||
type: _is.getType<String>(),
|
||||
nullable: false,
|
||||
),
|
||||
'request': _is.ParameterDescription(
|
||||
name: 'request',
|
||||
type: _is.getType<_is.UuidValue?>(),
|
||||
nullable: true,
|
||||
),
|
||||
'code': _is.ParameterDescription(
|
||||
name: 'code',
|
||||
type: _is.getType<String?>(),
|
||||
nullable: true,
|
||||
),
|
||||
'keyChallenge': _is.ParameterDescription(
|
||||
name: 'keyChallenge',
|
||||
type: _is.getType<_is.UuidValue?>(),
|
||||
nullable: true,
|
||||
),
|
||||
'signature': _is.ParameterDescription(
|
||||
name: 'signature',
|
||||
type: _is.getType<_idt.ByteData?>(),
|
||||
nullable: true,
|
||||
),
|
||||
},
|
||||
call:
|
||||
(
|
||||
_is.Session session,
|
||||
Map<String, dynamic> params,
|
||||
) async =>
|
||||
(endpoints['accountRecovery']
|
||||
as _ivnbjhab.AccountRecoveryEndpoint)
|
||||
.resetAuthenticator(
|
||||
session,
|
||||
challenge: params['challenge'],
|
||||
clientValue: params['clientValue'],
|
||||
clientProof: params['clientProof'],
|
||||
address: params['address'],
|
||||
language: params['language'],
|
||||
request: params['request'],
|
||||
code: params['code'],
|
||||
keyChallenge: params['keyChallenge'],
|
||||
signature: params['signature'],
|
||||
),
|
||||
),
|
||||
'beginKeyProof': _is.MethodConnector(
|
||||
name: 'beginKeyProof',
|
||||
params: {
|
||||
'address': _is.ParameterDescription(
|
||||
name: 'address',
|
||||
type: _is.getType<String>(),
|
||||
nullable: false,
|
||||
),
|
||||
},
|
||||
call:
|
||||
(
|
||||
_is.Session session,
|
||||
Map<String, dynamic> params,
|
||||
) async =>
|
||||
(endpoints['accountRecovery']
|
||||
as _ivnbjhab.AccountRecoveryEndpoint)
|
||||
.beginKeyProof(
|
||||
session,
|
||||
address: params['address'],
|
||||
),
|
||||
),
|
||||
'resetPassword': _is.MethodConnector(
|
||||
name: 'resetPassword',
|
||||
params: {
|
||||
'challenge': _is.ParameterDescription(
|
||||
name: 'challenge',
|
||||
type: _is.getType<_is.UuidValue>(),
|
||||
nullable: false,
|
||||
),
|
||||
'signature': _is.ParameterDescription(
|
||||
name: 'signature',
|
||||
type: _is.getType<_idt.ByteData>(),
|
||||
nullable: false,
|
||||
),
|
||||
'password': _is.ParameterDescription(
|
||||
name: 'password',
|
||||
type: _is.getType<_ijye8mcv.NewPassword>(),
|
||||
nullable: false,
|
||||
),
|
||||
'totpCode': _is.ParameterDescription(
|
||||
name: 'totpCode',
|
||||
type: _is.getType<String?>(),
|
||||
nullable: true,
|
||||
),
|
||||
'recoveryCode': _is.ParameterDescription(
|
||||
name: 'recoveryCode',
|
||||
type: _is.getType<String?>(),
|
||||
nullable: true,
|
||||
),
|
||||
},
|
||||
call:
|
||||
(
|
||||
_is.Session session,
|
||||
Map<String, dynamic> params,
|
||||
) async =>
|
||||
(endpoints['accountRecovery']
|
||||
as _ivnbjhab.AccountRecoveryEndpoint)
|
||||
.resetPassword(
|
||||
session,
|
||||
challenge: params['challenge'],
|
||||
signature: params['signature'],
|
||||
password: params['password'],
|
||||
totpCode: params['totpCode'],
|
||||
recoveryCode: params['recoveryCode'],
|
||||
),
|
||||
),
|
||||
'startAccountReset': _is.MethodConnector(
|
||||
name: 'startAccountReset',
|
||||
params: {
|
||||
'address': _is.ParameterDescription(
|
||||
name: 'address',
|
||||
type: _is.getType<String>(),
|
||||
nullable: false,
|
||||
),
|
||||
'language': _is.ParameterDescription(
|
||||
name: 'language',
|
||||
type: _is.getType<String>(),
|
||||
nullable: false,
|
||||
),
|
||||
'request': _is.ParameterDescription(
|
||||
name: 'request',
|
||||
type: _is.getType<_is.UuidValue>(),
|
||||
nullable: false,
|
||||
),
|
||||
'code': _is.ParameterDescription(
|
||||
name: 'code',
|
||||
type: _is.getType<String>(),
|
||||
nullable: false,
|
||||
),
|
||||
},
|
||||
call:
|
||||
(
|
||||
_is.Session session,
|
||||
Map<String, dynamic> params,
|
||||
) async =>
|
||||
(endpoints['accountRecovery']
|
||||
as _ivnbjhab.AccountRecoveryEndpoint)
|
||||
.startAccountReset(
|
||||
session,
|
||||
address: params['address'],
|
||||
language: params['language'],
|
||||
request: params['request'],
|
||||
code: params['code'],
|
||||
),
|
||||
),
|
||||
'completeAccountReset': _is.MethodConnector(
|
||||
name: 'completeAccountReset',
|
||||
params: {
|
||||
'address': _is.ParameterDescription(
|
||||
name: 'address',
|
||||
type: _is.getType<String>(),
|
||||
nullable: false,
|
||||
),
|
||||
'request': _is.ParameterDescription(
|
||||
name: 'request',
|
||||
type: _is.getType<_is.UuidValue>(),
|
||||
nullable: false,
|
||||
),
|
||||
'code': _is.ParameterDescription(
|
||||
name: 'code',
|
||||
type: _is.getType<String>(),
|
||||
nullable: false,
|
||||
),
|
||||
'setup': _is.ParameterDescription(
|
||||
name: 'setup',
|
||||
type: _is.getType<_ikhyee47.PasswordSetup>(),
|
||||
nullable: false,
|
||||
),
|
||||
},
|
||||
call:
|
||||
(
|
||||
_is.Session session,
|
||||
Map<String, dynamic> params,
|
||||
) async =>
|
||||
(endpoints['accountRecovery']
|
||||
as _ivnbjhab.AccountRecoveryEndpoint)
|
||||
.completeAccountReset(
|
||||
session,
|
||||
address: params['address'],
|
||||
request: params['request'],
|
||||
code: params['code'],
|
||||
setup: params['setup'],
|
||||
),
|
||||
),
|
||||
},
|
||||
);
|
||||
connectors['authenticator'] = _is.EndpointConnector(
|
||||
name: 'authenticator',
|
||||
endpoint: endpoints['authenticator']!,
|
||||
methodConnectors: {
|
||||
'enrol': _is.MethodConnector(
|
||||
name: 'enrol',
|
||||
params: {
|
||||
'name': _is.ParameterDescription(
|
||||
name: 'name',
|
||||
type: _is.getType<String>(),
|
||||
nullable: false,
|
||||
),
|
||||
'label': _is.ParameterDescription(
|
||||
name: 'label',
|
||||
type: _is.getType<String>(),
|
||||
nullable: false,
|
||||
),
|
||||
},
|
||||
call:
|
||||
(
|
||||
_is.Session session,
|
||||
Map<String, dynamic> params,
|
||||
) async =>
|
||||
(endpoints['authenticator']
|
||||
as _i3xqawxj.AuthenticatorEndpoint)
|
||||
.enrol(
|
||||
session,
|
||||
name: params['name'],
|
||||
label: params['label'],
|
||||
),
|
||||
),
|
||||
'confirm': _is.MethodConnector(
|
||||
name: 'confirm',
|
||||
params: {
|
||||
'authenticator': _is.ParameterDescription(
|
||||
name: 'authenticator',
|
||||
type: _is.getType<_is.UuidValue>(),
|
||||
nullable: false,
|
||||
),
|
||||
'code': _is.ParameterDescription(
|
||||
name: 'code',
|
||||
type: _is.getType<String>(),
|
||||
nullable: false,
|
||||
),
|
||||
},
|
||||
call:
|
||||
(
|
||||
_is.Session session,
|
||||
Map<String, dynamic> params,
|
||||
) async =>
|
||||
(endpoints['authenticator']
|
||||
as _i3xqawxj.AuthenticatorEndpoint)
|
||||
.confirm(
|
||||
session,
|
||||
authenticator: params['authenticator'],
|
||||
code: params['code'],
|
||||
),
|
||||
),
|
||||
'list': _is.MethodConnector(
|
||||
name: 'list',
|
||||
params: {},
|
||||
call:
|
||||
(
|
||||
_is.Session session,
|
||||
Map<String, dynamic> params,
|
||||
) async =>
|
||||
(endpoints['authenticator']
|
||||
as _i3xqawxj.AuthenticatorEndpoint)
|
||||
.list(session),
|
||||
),
|
||||
'remove': _is.MethodConnector(
|
||||
name: 'remove',
|
||||
params: {
|
||||
'authenticator': _is.ParameterDescription(
|
||||
name: 'authenticator',
|
||||
type: _is.getType<_is.UuidValue>(),
|
||||
nullable: false,
|
||||
),
|
||||
'code': _is.ParameterDescription(
|
||||
name: 'code',
|
||||
type: _is.getType<String?>(),
|
||||
nullable: true,
|
||||
),
|
||||
},
|
||||
call:
|
||||
(
|
||||
_is.Session session,
|
||||
Map<String, dynamic> params,
|
||||
) async =>
|
||||
(endpoints['authenticator']
|
||||
as _i3xqawxj.AuthenticatorEndpoint)
|
||||
.remove(
|
||||
session,
|
||||
authenticator: params['authenticator'],
|
||||
code: params['code'],
|
||||
),
|
||||
),
|
||||
'newRecoveryCodes': _is.MethodConnector(
|
||||
name: 'newRecoveryCodes',
|
||||
params: {
|
||||
'code': _is.ParameterDescription(
|
||||
name: 'code',
|
||||
type: _is.getType<String>(),
|
||||
nullable: false,
|
||||
),
|
||||
},
|
||||
call:
|
||||
(
|
||||
_is.Session session,
|
||||
Map<String, dynamic> params,
|
||||
) async =>
|
||||
(endpoints['authenticator']
|
||||
as _i3xqawxj.AuthenticatorEndpoint)
|
||||
.newRecoveryCodes(
|
||||
session,
|
||||
code: params['code'],
|
||||
),
|
||||
),
|
||||
},
|
||||
);
|
||||
connectors['password'] = _is.EndpointConnector(
|
||||
name: 'password',
|
||||
endpoint: endpoints['password']!,
|
||||
methodConnectors: {
|
||||
'set': _is.MethodConnector(
|
||||
name: 'set',
|
||||
params: {
|
||||
'setup': _is.ParameterDescription(
|
||||
name: 'setup',
|
||||
type: _is.getType<_ikhyee47.PasswordSetup>(),
|
||||
nullable: false,
|
||||
),
|
||||
'request': _is.ParameterDescription(
|
||||
name: 'request',
|
||||
type: _is.getType<_is.UuidValue?>(),
|
||||
nullable: true,
|
||||
),
|
||||
'code': _is.ParameterDescription(
|
||||
name: 'code',
|
||||
type: _is.getType<String?>(),
|
||||
nullable: true,
|
||||
),
|
||||
},
|
||||
call:
|
||||
(
|
||||
_is.Session session,
|
||||
Map<String, dynamic> params,
|
||||
) async =>
|
||||
(endpoints['password'] as _ig7fyg1b.PasswordEndpoint).set(
|
||||
session,
|
||||
setup: params['setup'],
|
||||
request: params['request'],
|
||||
code: params['code'],
|
||||
),
|
||||
),
|
||||
},
|
||||
);
|
||||
connectors['passwordSignIn'] = _is.EndpointConnector(
|
||||
name: 'passwordSignIn',
|
||||
endpoint: endpoints['passwordSignIn']!,
|
||||
methodConnectors: {
|
||||
'begin': _is.MethodConnector(
|
||||
name: 'begin',
|
||||
params: {
|
||||
'address': _is.ParameterDescription(
|
||||
name: 'address',
|
||||
type: _is.getType<String>(),
|
||||
nullable: false,
|
||||
),
|
||||
},
|
||||
call:
|
||||
(
|
||||
_is.Session session,
|
||||
Map<String, dynamic> params,
|
||||
) async =>
|
||||
(endpoints['passwordSignIn']
|
||||
as _ik13m562.PasswordSignInEndpoint)
|
||||
.begin(
|
||||
session,
|
||||
address: params['address'],
|
||||
),
|
||||
),
|
||||
'complete': _is.MethodConnector(
|
||||
name: 'complete',
|
||||
params: {
|
||||
'challenge': _is.ParameterDescription(
|
||||
name: 'challenge',
|
||||
type: _is.getType<_is.UuidValue>(),
|
||||
nullable: false,
|
||||
),
|
||||
'clientValue': _is.ParameterDescription(
|
||||
name: 'clientValue',
|
||||
type: _is.getType<_idt.ByteData>(),
|
||||
nullable: false,
|
||||
),
|
||||
'clientProof': _is.ParameterDescription(
|
||||
name: 'clientProof',
|
||||
type: _is.getType<_idt.ByteData>(),
|
||||
nullable: false,
|
||||
),
|
||||
'totpCode': _is.ParameterDescription(
|
||||
name: 'totpCode',
|
||||
type: _is.getType<String?>(),
|
||||
nullable: true,
|
||||
),
|
||||
'recoveryCode': _is.ParameterDescription(
|
||||
name: 'recoveryCode',
|
||||
type: _is.getType<String?>(),
|
||||
nullable: true,
|
||||
),
|
||||
},
|
||||
call:
|
||||
(
|
||||
_is.Session session,
|
||||
Map<String, dynamic> params,
|
||||
) async =>
|
||||
(endpoints['passwordSignIn']
|
||||
as _ik13m562.PasswordSignInEndpoint)
|
||||
.complete(
|
||||
session,
|
||||
challenge: params['challenge'],
|
||||
clientValue: params['clientValue'],
|
||||
clientProof: params['clientProof'],
|
||||
totpCode: params['totpCode'],
|
||||
recoveryCode: params['recoveryCode'],
|
||||
),
|
||||
),
|
||||
},
|
||||
);
|
||||
connectors['profile'] = _is.EndpointConnector(
|
||||
name: 'profile',
|
||||
endpoint: endpoints['profile']!,
|
||||
|
|
|
|||
|
|
@ -26,7 +26,8 @@ abstract class InstanceBasics
|
|||
required this.minimumAppBuild,
|
||||
this.dataLocation,
|
||||
this.backupLocation,
|
||||
});
|
||||
bool? passwordSignIn,
|
||||
}) : passwordSignIn = passwordSignIn ?? false;
|
||||
|
||||
factory InstanceBasics({
|
||||
required String app,
|
||||
|
|
@ -36,6 +37,7 @@ abstract class InstanceBasics
|
|||
required int minimumAppBuild,
|
||||
String? dataLocation,
|
||||
String? backupLocation,
|
||||
bool? passwordSignIn,
|
||||
}) = _InstanceBasicsImpl;
|
||||
|
||||
factory InstanceBasics.fromJson(Map<String, dynamic> jsonSerialization) {
|
||||
|
|
@ -49,6 +51,9 @@ abstract class InstanceBasics
|
|||
minimumAppBuild: jsonSerialization['minimumAppBuild'] as int,
|
||||
dataLocation: jsonSerialization['dataLocation'] as String?,
|
||||
backupLocation: jsonSerialization['backupLocation'] as String?,
|
||||
passwordSignIn: jsonSerialization['passwordSignIn'] == null
|
||||
? null
|
||||
: _is.BoolJsonExtension.fromJson(jsonSerialization['passwordSignIn']),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -82,6 +87,10 @@ abstract class InstanceBasics
|
|||
/// `none` where it keeps none. Null where the operator says nothing.
|
||||
String? backupLocation;
|
||||
|
||||
/// Whether people sign in with a password and TOTP here (ADR 0017): the
|
||||
/// operator set the keys it needs.
|
||||
bool passwordSignIn;
|
||||
|
||||
/// Returns a shallow copy of this [InstanceBasics]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
|
|
@ -93,6 +102,7 @@ abstract class InstanceBasics
|
|||
int? minimumAppBuild,
|
||||
String? dataLocation,
|
||||
String? backupLocation,
|
||||
bool? passwordSignIn,
|
||||
});
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
|
|
@ -105,6 +115,7 @@ abstract class InstanceBasics
|
|||
'minimumAppBuild': minimumAppBuild,
|
||||
if (dataLocation != null) 'dataLocation': dataLocation,
|
||||
if (backupLocation != null) 'backupLocation': backupLocation,
|
||||
'passwordSignIn': passwordSignIn,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -119,6 +130,7 @@ abstract class InstanceBasics
|
|||
'minimumAppBuild': minimumAppBuild,
|
||||
if (dataLocation != null) 'dataLocation': dataLocation,
|
||||
if (backupLocation != null) 'backupLocation': backupLocation,
|
||||
'passwordSignIn': passwordSignIn,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -139,6 +151,7 @@ class _InstanceBasicsImpl extends InstanceBasics {
|
|||
required int minimumAppBuild,
|
||||
String? dataLocation,
|
||||
String? backupLocation,
|
||||
bool? passwordSignIn,
|
||||
}) : super._(
|
||||
app: app,
|
||||
gitHubClientId: gitHubClientId,
|
||||
|
|
@ -147,6 +160,7 @@ class _InstanceBasicsImpl extends InstanceBasics {
|
|||
minimumAppBuild: minimumAppBuild,
|
||||
dataLocation: dataLocation,
|
||||
backupLocation: backupLocation,
|
||||
passwordSignIn: passwordSignIn,
|
||||
);
|
||||
|
||||
/// Returns a shallow copy of this [InstanceBasics]
|
||||
|
|
@ -161,6 +175,7 @@ class _InstanceBasicsImpl extends InstanceBasics {
|
|||
int? minimumAppBuild,
|
||||
Object? dataLocation = _Undefined,
|
||||
Object? backupLocation = _Undefined,
|
||||
bool? passwordSignIn,
|
||||
}) {
|
||||
return InstanceBasics(
|
||||
app: app ?? this.app,
|
||||
|
|
@ -174,6 +189,7 @@ class _InstanceBasicsImpl extends InstanceBasics {
|
|||
backupLocation: backupLocation is String?
|
||||
? backupLocation
|
||||
: this.backupLocation,
|
||||
passwordSignIn: passwordSignIn ?? this.passwordSignIn,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,946 @@
|
|||
/* 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 'dart:typed_data' as _idt;
|
||||
import 'package:serverpod/serverpod.dart' as _is;
|
||||
|
||||
/// An account's password as the register keeps it (ADR 0017): nothing that
|
||||
/// signs in or decrypts in the password's place. The salt and Argon2id's
|
||||
/// costs the device derives the password's keys with, SRP's verifier
|
||||
/// (v = g^x mod N, PAD to N's length), the account's public keys, and its
|
||||
/// private keys wrapped under the password's key and again under the
|
||||
/// recovery phrase's, which only a device can open.
|
||||
abstract class AccountPassword
|
||||
implements _is.TableRow<int?>, _is.ProtocolSerialization {
|
||||
AccountPassword._({
|
||||
this.id,
|
||||
required this.authUserId,
|
||||
required this.salt,
|
||||
required this.kdfAlgorithm,
|
||||
required this.kdfOpsLimit,
|
||||
required this.kdfMemLimit,
|
||||
required this.verifier,
|
||||
required this.encryptionPublicKey,
|
||||
required this.signingPublicKey,
|
||||
required this.passwordKeysNonce,
|
||||
required this.passwordKeysCipherText,
|
||||
required this.recoveryKeysNonce,
|
||||
required this.recoveryKeysCipherText,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
factory AccountPassword({
|
||||
int? id,
|
||||
required _is.UuidValue authUserId,
|
||||
required _idt.ByteData salt,
|
||||
required String kdfAlgorithm,
|
||||
required int kdfOpsLimit,
|
||||
required int kdfMemLimit,
|
||||
required _idt.ByteData verifier,
|
||||
required _idt.ByteData encryptionPublicKey,
|
||||
required _idt.ByteData signingPublicKey,
|
||||
required _idt.ByteData passwordKeysNonce,
|
||||
required _idt.ByteData passwordKeysCipherText,
|
||||
required _idt.ByteData recoveryKeysNonce,
|
||||
required _idt.ByteData recoveryKeysCipherText,
|
||||
required DateTime createdAt,
|
||||
required DateTime updatedAt,
|
||||
}) = _AccountPasswordImpl;
|
||||
|
||||
factory AccountPassword.fromJson(Map<String, dynamic> jsonSerialization) {
|
||||
return AccountPassword(
|
||||
id: jsonSerialization['id'] as int?,
|
||||
authUserId: _is.UuidValueJsonExtension.fromJson(
|
||||
jsonSerialization['authUserId'],
|
||||
),
|
||||
salt: _is.ByteDataJsonExtension.fromJson(jsonSerialization['salt']),
|
||||
kdfAlgorithm: jsonSerialization['kdfAlgorithm'] as String,
|
||||
kdfOpsLimit: jsonSerialization['kdfOpsLimit'] as int,
|
||||
kdfMemLimit: jsonSerialization['kdfMemLimit'] as int,
|
||||
verifier: _is.ByteDataJsonExtension.fromJson(
|
||||
jsonSerialization['verifier'],
|
||||
),
|
||||
encryptionPublicKey: _is.ByteDataJsonExtension.fromJson(
|
||||
jsonSerialization['encryptionPublicKey'],
|
||||
),
|
||||
signingPublicKey: _is.ByteDataJsonExtension.fromJson(
|
||||
jsonSerialization['signingPublicKey'],
|
||||
),
|
||||
passwordKeysNonce: _is.ByteDataJsonExtension.fromJson(
|
||||
jsonSerialization['passwordKeysNonce'],
|
||||
),
|
||||
passwordKeysCipherText: _is.ByteDataJsonExtension.fromJson(
|
||||
jsonSerialization['passwordKeysCipherText'],
|
||||
),
|
||||
recoveryKeysNonce: _is.ByteDataJsonExtension.fromJson(
|
||||
jsonSerialization['recoveryKeysNonce'],
|
||||
),
|
||||
recoveryKeysCipherText: _is.ByteDataJsonExtension.fromJson(
|
||||
jsonSerialization['recoveryKeysCipherText'],
|
||||
),
|
||||
createdAt: _is.DateTimeJsonExtension.fromJson(
|
||||
jsonSerialization['createdAt'],
|
||||
),
|
||||
updatedAt: _is.DateTimeJsonExtension.fromJson(
|
||||
jsonSerialization['updatedAt'],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static final t = AccountPasswordTable();
|
||||
|
||||
static const db = AccountPasswordRepository._();
|
||||
|
||||
@override
|
||||
int? id;
|
||||
|
||||
/// The account: its person's ID (deleted with them, `deletePerson`).
|
||||
_is.UuidValue authUserId;
|
||||
|
||||
/// Argon2id's salt (16 bytes).
|
||||
_idt.ByteData salt;
|
||||
|
||||
/// Argon2id's version as libsodium names it (`argon2id13`).
|
||||
String kdfAlgorithm;
|
||||
|
||||
/// Argon2id's passes.
|
||||
int kdfOpsLimit;
|
||||
|
||||
/// Argon2id's memory, in bytes.
|
||||
int kdfMemLimit;
|
||||
|
||||
/// SRP's verifier, big-endian, as long as N.
|
||||
_idt.ByteData verifier;
|
||||
|
||||
/// The account's X25519 public key, which data keys are sealed to.
|
||||
_idt.ByteData encryptionPublicKey;
|
||||
|
||||
/// The account's Ed25519 public key, which its signatures check against.
|
||||
_idt.ByteData signingPublicKey;
|
||||
|
||||
/// The private keys under the password's key: XChaCha20-Poly1305's nonce.
|
||||
_idt.ByteData passwordKeysNonce;
|
||||
|
||||
/// …and what it sealed.
|
||||
_idt.ByteData passwordKeysCipherText;
|
||||
|
||||
/// The private keys under the recovery phrase's key: the nonce.
|
||||
_idt.ByteData recoveryKeysNonce;
|
||||
|
||||
/// …and what it sealed.
|
||||
_idt.ByteData recoveryKeysCipherText;
|
||||
|
||||
DateTime createdAt;
|
||||
|
||||
DateTime updatedAt;
|
||||
|
||||
@override
|
||||
_is.Table<int?> get table => t;
|
||||
|
||||
/// Returns a shallow copy of this [AccountPassword]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
AccountPassword copyWith({
|
||||
int? id,
|
||||
_is.UuidValue? authUserId,
|
||||
_idt.ByteData? salt,
|
||||
String? kdfAlgorithm,
|
||||
int? kdfOpsLimit,
|
||||
int? kdfMemLimit,
|
||||
_idt.ByteData? verifier,
|
||||
_idt.ByteData? encryptionPublicKey,
|
||||
_idt.ByteData? signingPublicKey,
|
||||
_idt.ByteData? passwordKeysNonce,
|
||||
_idt.ByteData? passwordKeysCipherText,
|
||||
_idt.ByteData? recoveryKeysNonce,
|
||||
_idt.ByteData? recoveryKeysCipherText,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
});
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'__className__': 'halkyon_base.AccountPassword',
|
||||
if (id != null) 'id': id,
|
||||
'authUserId': authUserId.toJson(),
|
||||
'salt': salt.toJson(),
|
||||
'kdfAlgorithm': kdfAlgorithm,
|
||||
'kdfOpsLimit': kdfOpsLimit,
|
||||
'kdfMemLimit': kdfMemLimit,
|
||||
'verifier': verifier.toJson(),
|
||||
'encryptionPublicKey': encryptionPublicKey.toJson(),
|
||||
'signingPublicKey': signingPublicKey.toJson(),
|
||||
'passwordKeysNonce': passwordKeysNonce.toJson(),
|
||||
'passwordKeysCipherText': passwordKeysCipherText.toJson(),
|
||||
'recoveryKeysNonce': recoveryKeysNonce.toJson(),
|
||||
'recoveryKeysCipherText': recoveryKeysCipherText.toJson(),
|
||||
'createdAt': createdAt.toJson(),
|
||||
'updatedAt': updatedAt.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJsonForProtocol() {
|
||||
return {};
|
||||
}
|
||||
|
||||
static AccountPasswordInclude include() {
|
||||
return AccountPasswordInclude._();
|
||||
}
|
||||
|
||||
static AccountPasswordIncludeList includeList({
|
||||
_is.WhereExpressionBuilder<AccountPasswordTable>? where,
|
||||
int? limit,
|
||||
int? offset,
|
||||
_is.OrderByBuilder<AccountPasswordTable>? orderBy,
|
||||
_is.OrderByListBuilder<AccountPasswordTable>? orderByList,
|
||||
AccountPasswordInclude? include,
|
||||
}) {
|
||||
return AccountPasswordIncludeList._(
|
||||
where: where,
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
orderBy: orderBy?.call(AccountPassword.t),
|
||||
orderByList: orderByList?.call(AccountPassword.t),
|
||||
include: include,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return _is.SerializationManager.encode(this);
|
||||
}
|
||||
}
|
||||
|
||||
class _Undefined {}
|
||||
|
||||
class _AccountPasswordImpl extends AccountPassword {
|
||||
_AccountPasswordImpl({
|
||||
int? id,
|
||||
required _is.UuidValue authUserId,
|
||||
required _idt.ByteData salt,
|
||||
required String kdfAlgorithm,
|
||||
required int kdfOpsLimit,
|
||||
required int kdfMemLimit,
|
||||
required _idt.ByteData verifier,
|
||||
required _idt.ByteData encryptionPublicKey,
|
||||
required _idt.ByteData signingPublicKey,
|
||||
required _idt.ByteData passwordKeysNonce,
|
||||
required _idt.ByteData passwordKeysCipherText,
|
||||
required _idt.ByteData recoveryKeysNonce,
|
||||
required _idt.ByteData recoveryKeysCipherText,
|
||||
required DateTime createdAt,
|
||||
required DateTime updatedAt,
|
||||
}) : super._(
|
||||
id: id,
|
||||
authUserId: authUserId,
|
||||
salt: salt,
|
||||
kdfAlgorithm: kdfAlgorithm,
|
||||
kdfOpsLimit: kdfOpsLimit,
|
||||
kdfMemLimit: kdfMemLimit,
|
||||
verifier: verifier,
|
||||
encryptionPublicKey: encryptionPublicKey,
|
||||
signingPublicKey: signingPublicKey,
|
||||
passwordKeysNonce: passwordKeysNonce,
|
||||
passwordKeysCipherText: passwordKeysCipherText,
|
||||
recoveryKeysNonce: recoveryKeysNonce,
|
||||
recoveryKeysCipherText: recoveryKeysCipherText,
|
||||
createdAt: createdAt,
|
||||
updatedAt: updatedAt,
|
||||
);
|
||||
|
||||
/// Returns a shallow copy of this [AccountPassword]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
@override
|
||||
AccountPassword copyWith({
|
||||
Object? id = _Undefined,
|
||||
_is.UuidValue? authUserId,
|
||||
_idt.ByteData? salt,
|
||||
String? kdfAlgorithm,
|
||||
int? kdfOpsLimit,
|
||||
int? kdfMemLimit,
|
||||
_idt.ByteData? verifier,
|
||||
_idt.ByteData? encryptionPublicKey,
|
||||
_idt.ByteData? signingPublicKey,
|
||||
_idt.ByteData? passwordKeysNonce,
|
||||
_idt.ByteData? passwordKeysCipherText,
|
||||
_idt.ByteData? recoveryKeysNonce,
|
||||
_idt.ByteData? recoveryKeysCipherText,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
}) {
|
||||
return AccountPassword(
|
||||
id: id is int? ? id : this.id,
|
||||
authUserId: authUserId ?? this.authUserId,
|
||||
salt: salt ?? this.salt.clone(),
|
||||
kdfAlgorithm: kdfAlgorithm ?? this.kdfAlgorithm,
|
||||
kdfOpsLimit: kdfOpsLimit ?? this.kdfOpsLimit,
|
||||
kdfMemLimit: kdfMemLimit ?? this.kdfMemLimit,
|
||||
verifier: verifier ?? this.verifier.clone(),
|
||||
encryptionPublicKey:
|
||||
encryptionPublicKey ?? this.encryptionPublicKey.clone(),
|
||||
signingPublicKey: signingPublicKey ?? this.signingPublicKey.clone(),
|
||||
passwordKeysNonce: passwordKeysNonce ?? this.passwordKeysNonce.clone(),
|
||||
passwordKeysCipherText:
|
||||
passwordKeysCipherText ?? this.passwordKeysCipherText.clone(),
|
||||
recoveryKeysNonce: recoveryKeysNonce ?? this.recoveryKeysNonce.clone(),
|
||||
recoveryKeysCipherText:
|
||||
recoveryKeysCipherText ?? this.recoveryKeysCipherText.clone(),
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AccountPasswordUpdateTable extends _is.UpdateTable<AccountPasswordTable> {
|
||||
AccountPasswordUpdateTable(super.table);
|
||||
|
||||
_is.ColumnValue<_is.UuidValue, _is.UuidValue> authUserId(
|
||||
_is.UuidValue value,
|
||||
) => _is.ColumnValue(
|
||||
table.authUserId,
|
||||
value,
|
||||
);
|
||||
|
||||
_is.ColumnValue<_idt.ByteData, _idt.ByteData> salt(_idt.ByteData value) =>
|
||||
_is.ColumnValue(
|
||||
table.salt,
|
||||
value,
|
||||
);
|
||||
|
||||
_is.ColumnValue<String, String> kdfAlgorithm(String value) => _is.ColumnValue(
|
||||
table.kdfAlgorithm,
|
||||
value,
|
||||
);
|
||||
|
||||
_is.ColumnValue<int, int> kdfOpsLimit(int value) => _is.ColumnValue(
|
||||
table.kdfOpsLimit,
|
||||
value,
|
||||
);
|
||||
|
||||
_is.ColumnValue<int, int> kdfMemLimit(int value) => _is.ColumnValue(
|
||||
table.kdfMemLimit,
|
||||
value,
|
||||
);
|
||||
|
||||
_is.ColumnValue<_idt.ByteData, _idt.ByteData> verifier(_idt.ByteData value) =>
|
||||
_is.ColumnValue(
|
||||
table.verifier,
|
||||
value,
|
||||
);
|
||||
|
||||
_is.ColumnValue<_idt.ByteData, _idt.ByteData> encryptionPublicKey(
|
||||
_idt.ByteData value,
|
||||
) => _is.ColumnValue(
|
||||
table.encryptionPublicKey,
|
||||
value,
|
||||
);
|
||||
|
||||
_is.ColumnValue<_idt.ByteData, _idt.ByteData> signingPublicKey(
|
||||
_idt.ByteData value,
|
||||
) => _is.ColumnValue(
|
||||
table.signingPublicKey,
|
||||
value,
|
||||
);
|
||||
|
||||
_is.ColumnValue<_idt.ByteData, _idt.ByteData> passwordKeysNonce(
|
||||
_idt.ByteData value,
|
||||
) => _is.ColumnValue(
|
||||
table.passwordKeysNonce,
|
||||
value,
|
||||
);
|
||||
|
||||
_is.ColumnValue<_idt.ByteData, _idt.ByteData> passwordKeysCipherText(
|
||||
_idt.ByteData value,
|
||||
) => _is.ColumnValue(
|
||||
table.passwordKeysCipherText,
|
||||
value,
|
||||
);
|
||||
|
||||
_is.ColumnValue<_idt.ByteData, _idt.ByteData> recoveryKeysNonce(
|
||||
_idt.ByteData value,
|
||||
) => _is.ColumnValue(
|
||||
table.recoveryKeysNonce,
|
||||
value,
|
||||
);
|
||||
|
||||
_is.ColumnValue<_idt.ByteData, _idt.ByteData> recoveryKeysCipherText(
|
||||
_idt.ByteData value,
|
||||
) => _is.ColumnValue(
|
||||
table.recoveryKeysCipherText,
|
||||
value,
|
||||
);
|
||||
|
||||
_is.ColumnValue<DateTime, DateTime> createdAt(DateTime value) =>
|
||||
_is.ColumnValue(
|
||||
table.createdAt,
|
||||
value,
|
||||
);
|
||||
|
||||
_is.ColumnValue<DateTime, DateTime> updatedAt(DateTime value) =>
|
||||
_is.ColumnValue(
|
||||
table.updatedAt,
|
||||
value,
|
||||
);
|
||||
}
|
||||
|
||||
class AccountPasswordTable extends _is.Table<int?> {
|
||||
AccountPasswordTable({super.tableRelation})
|
||||
: super(tableName: 'account_password') {
|
||||
updateTable = AccountPasswordUpdateTable(this);
|
||||
authUserId = _is.ColumnUuid(
|
||||
'authUserId',
|
||||
this,
|
||||
);
|
||||
salt = _is.ColumnByteData(
|
||||
'salt',
|
||||
this,
|
||||
);
|
||||
kdfAlgorithm = _is.ColumnString(
|
||||
'kdfAlgorithm',
|
||||
this,
|
||||
);
|
||||
kdfOpsLimit = _is.ColumnInt(
|
||||
'kdfOpsLimit',
|
||||
this,
|
||||
);
|
||||
kdfMemLimit = _is.ColumnInt(
|
||||
'kdfMemLimit',
|
||||
this,
|
||||
);
|
||||
verifier = _is.ColumnByteData(
|
||||
'verifier',
|
||||
this,
|
||||
);
|
||||
encryptionPublicKey = _is.ColumnByteData(
|
||||
'encryptionPublicKey',
|
||||
this,
|
||||
);
|
||||
signingPublicKey = _is.ColumnByteData(
|
||||
'signingPublicKey',
|
||||
this,
|
||||
);
|
||||
passwordKeysNonce = _is.ColumnByteData(
|
||||
'passwordKeysNonce',
|
||||
this,
|
||||
);
|
||||
passwordKeysCipherText = _is.ColumnByteData(
|
||||
'passwordKeysCipherText',
|
||||
this,
|
||||
);
|
||||
recoveryKeysNonce = _is.ColumnByteData(
|
||||
'recoveryKeysNonce',
|
||||
this,
|
||||
);
|
||||
recoveryKeysCipherText = _is.ColumnByteData(
|
||||
'recoveryKeysCipherText',
|
||||
this,
|
||||
);
|
||||
createdAt = _is.ColumnDateTime(
|
||||
'createdAt',
|
||||
this,
|
||||
);
|
||||
updatedAt = _is.ColumnDateTime(
|
||||
'updatedAt',
|
||||
this,
|
||||
);
|
||||
}
|
||||
|
||||
late final AccountPasswordUpdateTable updateTable;
|
||||
|
||||
/// The account: its person's ID (deleted with them, `deletePerson`).
|
||||
late final _is.ColumnUuid authUserId;
|
||||
|
||||
/// Argon2id's salt (16 bytes).
|
||||
late final _is.ColumnByteData salt;
|
||||
|
||||
/// Argon2id's version as libsodium names it (`argon2id13`).
|
||||
late final _is.ColumnString kdfAlgorithm;
|
||||
|
||||
/// Argon2id's passes.
|
||||
late final _is.ColumnInt kdfOpsLimit;
|
||||
|
||||
/// Argon2id's memory, in bytes.
|
||||
late final _is.ColumnInt kdfMemLimit;
|
||||
|
||||
/// SRP's verifier, big-endian, as long as N.
|
||||
late final _is.ColumnByteData verifier;
|
||||
|
||||
/// The account's X25519 public key, which data keys are sealed to.
|
||||
late final _is.ColumnByteData encryptionPublicKey;
|
||||
|
||||
/// The account's Ed25519 public key, which its signatures check against.
|
||||
late final _is.ColumnByteData signingPublicKey;
|
||||
|
||||
/// The private keys under the password's key: XChaCha20-Poly1305's nonce.
|
||||
late final _is.ColumnByteData passwordKeysNonce;
|
||||
|
||||
/// …and what it sealed.
|
||||
late final _is.ColumnByteData passwordKeysCipherText;
|
||||
|
||||
/// The private keys under the recovery phrase's key: the nonce.
|
||||
late final _is.ColumnByteData recoveryKeysNonce;
|
||||
|
||||
/// …and what it sealed.
|
||||
late final _is.ColumnByteData recoveryKeysCipherText;
|
||||
|
||||
late final _is.ColumnDateTime createdAt;
|
||||
|
||||
late final _is.ColumnDateTime updatedAt;
|
||||
|
||||
@override
|
||||
List<_is.Column> get columns => [
|
||||
id,
|
||||
authUserId,
|
||||
salt,
|
||||
kdfAlgorithm,
|
||||
kdfOpsLimit,
|
||||
kdfMemLimit,
|
||||
verifier,
|
||||
encryptionPublicKey,
|
||||
signingPublicKey,
|
||||
passwordKeysNonce,
|
||||
passwordKeysCipherText,
|
||||
recoveryKeysNonce,
|
||||
recoveryKeysCipherText,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
];
|
||||
}
|
||||
|
||||
class AccountPasswordInclude extends _is.IncludeObject {
|
||||
AccountPasswordInclude._();
|
||||
|
||||
@override
|
||||
Map<String, _is.Include?> get includes => {};
|
||||
|
||||
@override
|
||||
_is.Table<int?> get table => AccountPassword.t;
|
||||
}
|
||||
|
||||
class AccountPasswordIncludeList extends _is.IncludeList {
|
||||
AccountPasswordIncludeList._({
|
||||
_is.WhereExpressionBuilder<AccountPasswordTable>? where,
|
||||
super.limit,
|
||||
super.offset,
|
||||
super.orderBy,
|
||||
super.orderByList,
|
||||
super.include,
|
||||
}) {
|
||||
super.where = where?.call(AccountPassword.t);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, _is.Include?> get includes => include?.includes ?? {};
|
||||
|
||||
@override
|
||||
_is.Table<int?> get table => AccountPassword.t;
|
||||
}
|
||||
|
||||
class AccountPasswordRepository {
|
||||
const AccountPasswordRepository._();
|
||||
|
||||
/// Returns a list of [AccountPassword]s matching the given query parameters.
|
||||
///
|
||||
/// Use [where] to specify which items to include in the return value.
|
||||
/// If none is specified, all items will be returned.
|
||||
///
|
||||
/// To specify the order of the items use [orderBy] or [orderByList]
|
||||
/// when sorting by multiple columns.
|
||||
///
|
||||
/// The maximum number of items can be set by [limit]. If no limit is set,
|
||||
/// all items matching the query will be returned.
|
||||
///
|
||||
/// [offset] defines how many items to skip, after which [limit] (or all)
|
||||
/// items are read from the database.
|
||||
///
|
||||
/// ```dart
|
||||
/// var persons = await Persons.db.find(
|
||||
/// session,
|
||||
/// where: (t) => t.lastName.equals('Jones'),
|
||||
/// orderBy: (t) => t.firstName,
|
||||
/// limit: 100,
|
||||
/// );
|
||||
/// ```
|
||||
Future<List<AccountPassword>> find(
|
||||
_is.DatabaseSession session, {
|
||||
_is.WhereExpressionBuilder<AccountPasswordTable>? where,
|
||||
int? limit,
|
||||
int? offset,
|
||||
_is.OrderByBuilder<AccountPasswordTable>? orderBy,
|
||||
_is.OrderByListBuilder<AccountPasswordTable>? orderByList,
|
||||
_is.Transaction? transaction,
|
||||
_is.LockMode? lockMode,
|
||||
_is.LockBehavior? lockBehavior,
|
||||
}) async {
|
||||
return session.db.find<AccountPassword>(
|
||||
where: where?.call(AccountPassword.t),
|
||||
orderBy: orderBy?.call(AccountPassword.t),
|
||||
orderByList: orderByList?.call(AccountPassword.t),
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
transaction: transaction,
|
||||
lockMode: lockMode,
|
||||
lockBehavior: lockBehavior,
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns the first matching [AccountPassword] matching the given query parameters.
|
||||
///
|
||||
/// Use [where] to specify which items to include in the return value.
|
||||
/// If none is specified, all items will be returned.
|
||||
///
|
||||
/// To specify the order use [orderBy] or [orderByList]
|
||||
/// when sorting by multiple columns.
|
||||
///
|
||||
/// [offset] defines how many items to skip, after which the next one will be picked.
|
||||
///
|
||||
/// ```dart
|
||||
/// var youngestPerson = await Persons.db.findFirstRow(
|
||||
/// session,
|
||||
/// where: (t) => t.lastName.equals('Jones'),
|
||||
/// orderBy: (t) => t.age,
|
||||
/// );
|
||||
/// ```
|
||||
Future<AccountPassword?> findFirstRow(
|
||||
_is.DatabaseSession session, {
|
||||
_is.WhereExpressionBuilder<AccountPasswordTable>? where,
|
||||
int? offset,
|
||||
_is.OrderByBuilder<AccountPasswordTable>? orderBy,
|
||||
_is.OrderByListBuilder<AccountPasswordTable>? orderByList,
|
||||
_is.Transaction? transaction,
|
||||
_is.LockMode? lockMode,
|
||||
_is.LockBehavior? lockBehavior,
|
||||
}) async {
|
||||
return session.db.findFirstRow<AccountPassword>(
|
||||
where: where?.call(AccountPassword.t),
|
||||
orderBy: orderBy?.call(AccountPassword.t),
|
||||
orderByList: orderByList?.call(AccountPassword.t),
|
||||
offset: offset,
|
||||
transaction: transaction,
|
||||
lockMode: lockMode,
|
||||
lockBehavior: lockBehavior,
|
||||
);
|
||||
}
|
||||
|
||||
/// Finds a single [AccountPassword] by its [id] or null if no such row exists.
|
||||
Future<AccountPassword?> findById(
|
||||
_is.DatabaseSession session,
|
||||
int id, {
|
||||
_is.Transaction? transaction,
|
||||
_is.LockMode? lockMode,
|
||||
_is.LockBehavior? lockBehavior,
|
||||
}) async {
|
||||
return session.db.findById<AccountPassword>(
|
||||
id,
|
||||
transaction: transaction,
|
||||
lockMode: lockMode,
|
||||
lockBehavior: lockBehavior,
|
||||
);
|
||||
}
|
||||
|
||||
/// Inserts all [AccountPassword]s in the list and returns the inserted rows.
|
||||
///
|
||||
/// The returned [AccountPassword]s will have their `id` fields set.
|
||||
///
|
||||
/// This is an atomic operation, meaning that if one of the rows fails to
|
||||
/// insert, none of the rows will be inserted.
|
||||
///
|
||||
/// If [ignoreConflicts] is set to `true`, rows that conflict with existing
|
||||
/// rows are silently skipped, and only the successfully inserted rows are
|
||||
/// returned.
|
||||
///
|
||||
/// If [noReturn] is set to `true`, the inserted rows are not read back from
|
||||
/// the database and an empty list is returned. This avoids the overhead of
|
||||
/// transferring and deserializing the rows when the result is not needed.
|
||||
Future<List<AccountPassword>> insert(
|
||||
_is.DatabaseSession session,
|
||||
List<AccountPassword> rows, {
|
||||
_is.Transaction? transaction,
|
||||
bool ignoreConflicts = false,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.insert<AccountPassword>(
|
||||
rows,
|
||||
transaction: transaction,
|
||||
ignoreConflicts: ignoreConflicts,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Inserts a single [AccountPassword] and returns the inserted row.
|
||||
///
|
||||
/// The returned [AccountPassword] will have its `id` field set.
|
||||
Future<AccountPassword> insertRow(
|
||||
_is.DatabaseSession session,
|
||||
AccountPassword row, {
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.insertRow<AccountPassword>(
|
||||
row,
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Upserts all [AccountPassword]s in the list and returns the resulting rows.
|
||||
///
|
||||
/// If a row conflicts on the given [conflictColumns], the existing row is
|
||||
/// updated with the new values. Otherwise, a new row is inserted.
|
||||
///
|
||||
/// If [updateColumns] is provided, only those columns will be updated on
|
||||
/// conflict. If null, all non-conflict, non-id columns are updated.
|
||||
///
|
||||
/// If [updateWhere] is provided, the update only applies to rows matching the
|
||||
/// given expression. Conflicting rows that don't match are skipped and not
|
||||
/// returned, so the resulting list may be shorter than [rows].
|
||||
///
|
||||
/// The returned [AccountPassword]s will have their `id` fields set.
|
||||
///
|
||||
/// This is an atomic operation, meaning that if one of the rows fails,
|
||||
/// none of the rows will be affected.
|
||||
///
|
||||
/// If [noReturn] is set to `true`, the resulting rows are not read back from
|
||||
/// the database and an empty list is returned. This avoids the overhead of
|
||||
/// transferring and deserializing the rows when the result is not needed.
|
||||
Future<List<AccountPassword>> upsert(
|
||||
_is.DatabaseSession session,
|
||||
List<AccountPassword> rows, {
|
||||
required _is.ColumnSelections<AccountPasswordTable> conflictColumns,
|
||||
_is.ColumnSelections<AccountPasswordTable>? updateColumns,
|
||||
_is.WhereExpressionBuilder<AccountPasswordTable>? updateWhere,
|
||||
_is.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.upsert<AccountPassword>(
|
||||
rows,
|
||||
conflictColumns: conflictColumns(AccountPassword.t),
|
||||
updateColumns: updateColumns?.call(AccountPassword.t),
|
||||
updateWhere: updateWhere?.call(AccountPassword.t),
|
||||
transaction: transaction,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Upserts a single [AccountPassword] and returns the resulting row.
|
||||
///
|
||||
/// If the row conflicts on the given [conflictColumns], the existing row is
|
||||
/// updated. Otherwise, a new row is inserted.
|
||||
///
|
||||
/// If [updateColumns] is provided, only those columns will be updated on
|
||||
/// conflict. If null, all non-conflict, non-id columns are updated.
|
||||
///
|
||||
/// If [updateWhere] is provided, the update only applies when the existing
|
||||
/// row matches the expression. Returns `null` if no row was affected — for
|
||||
/// example when [updateWhere] does not match the conflicting row.
|
||||
///
|
||||
/// The returned [AccountPassword] will have its `id` field set.
|
||||
Future<AccountPassword?> upsertRow(
|
||||
_is.DatabaseSession session,
|
||||
AccountPassword row, {
|
||||
required _is.ColumnSelections<AccountPasswordTable> conflictColumns,
|
||||
_is.ColumnSelections<AccountPasswordTable>? updateColumns,
|
||||
_is.WhereExpressionBuilder<AccountPasswordTable>? updateWhere,
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.upsertRow<AccountPassword>(
|
||||
row,
|
||||
conflictColumns: conflictColumns(AccountPassword.t),
|
||||
updateColumns: updateColumns?.call(AccountPassword.t),
|
||||
updateWhere: updateWhere?.call(AccountPassword.t),
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates all [AccountPassword]s in the list and returns the updated rows. If
|
||||
/// [columns] is provided, only those columns will be updated. Defaults to
|
||||
/// all columns.
|
||||
/// This is an atomic operation, meaning that if one of the rows fails to
|
||||
/// update, none of the rows will be updated.
|
||||
///
|
||||
/// If [noReturn] is set to `true`, the updated rows are not read back from
|
||||
/// the database and an empty list is returned. This avoids the overhead of
|
||||
/// transferring and deserializing the rows when the result is not needed.
|
||||
Future<List<AccountPassword>> update(
|
||||
_is.DatabaseSession session,
|
||||
List<AccountPassword> rows, {
|
||||
_is.ColumnSelections<AccountPasswordTable>? columns,
|
||||
_is.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.update<AccountPassword>(
|
||||
rows,
|
||||
columns: columns?.call(AccountPassword.t),
|
||||
transaction: transaction,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates a single [AccountPassword]. The row needs to have its id set.
|
||||
/// Optionally, a list of [columns] can be provided to only update those
|
||||
/// columns. Defaults to all columns.
|
||||
Future<AccountPassword> updateRow(
|
||||
_is.DatabaseSession session,
|
||||
AccountPassword row, {
|
||||
_is.ColumnSelections<AccountPasswordTable>? columns,
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.updateRow<AccountPassword>(
|
||||
row,
|
||||
columns: columns?.call(AccountPassword.t),
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates a single [AccountPassword] by its [id] with the specified [columnValues].
|
||||
/// Returns the updated row or null if no row with the given id exists.
|
||||
Future<AccountPassword?> updateById(
|
||||
_is.DatabaseSession session,
|
||||
int id, {
|
||||
required _is.ColumnValueListBuilder<AccountPasswordUpdateTable>
|
||||
columnValues,
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.updateById<AccountPassword>(
|
||||
id,
|
||||
columnValues: columnValues(AccountPassword.t.updateTable),
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates all [AccountPassword]s matching the [where] expression with the specified [columnValues].
|
||||
/// Returns the list of updated rows.
|
||||
///
|
||||
/// If [noReturn] is set to `true`, the updated rows are not read back from
|
||||
/// the database and an empty list is returned. This avoids the overhead of
|
||||
/// transferring and deserializing the rows when the result is not needed.
|
||||
Future<List<AccountPassword>> updateWhere(
|
||||
_is.DatabaseSession session, {
|
||||
required _is.ColumnValueListBuilder<AccountPasswordUpdateTable>
|
||||
columnValues,
|
||||
required _is.WhereExpressionBuilder<AccountPasswordTable> where,
|
||||
int? limit,
|
||||
int? offset,
|
||||
_is.OrderByBuilder<AccountPasswordTable>? orderBy,
|
||||
_is.OrderByListBuilder<AccountPasswordTable>? orderByList,
|
||||
_is.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.updateWhere<AccountPassword>(
|
||||
columnValues: columnValues(AccountPassword.t.updateTable),
|
||||
where: where(AccountPassword.t),
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
orderBy: orderBy?.call(AccountPassword.t),
|
||||
orderByList: orderByList?.call(AccountPassword.t),
|
||||
transaction: transaction,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Deletes all [AccountPassword]s in the list and returns the deleted rows.
|
||||
///
|
||||
/// To specify the order of the returned rows use [orderBy] or [orderByList]
|
||||
/// when sorting by multiple columns.
|
||||
///
|
||||
/// This is an atomic operation, meaning that if one of the rows fail to
|
||||
/// be deleted, none of the rows will be deleted.
|
||||
///
|
||||
/// If [noReturn] is set to `true`, the deleted rows are not read back from
|
||||
/// the database and an empty list is returned. This avoids the overhead of
|
||||
/// transferring and deserializing the rows when the result is not needed.
|
||||
Future<List<AccountPassword>> delete(
|
||||
_is.DatabaseSession session,
|
||||
List<AccountPassword> rows, {
|
||||
_is.OrderByBuilder<AccountPasswordTable>? orderBy,
|
||||
_is.OrderByListBuilder<AccountPasswordTable>? orderByList,
|
||||
_is.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.delete<AccountPassword>(
|
||||
rows,
|
||||
orderBy: orderBy?.call(AccountPassword.t),
|
||||
orderByList: orderByList?.call(AccountPassword.t),
|
||||
transaction: transaction,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Deletes a single [AccountPassword].
|
||||
Future<AccountPassword> deleteRow(
|
||||
_is.DatabaseSession session,
|
||||
AccountPassword row, {
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.deleteRow<AccountPassword>(
|
||||
row,
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Deletes all rows matching the [where] expression.
|
||||
///
|
||||
/// To specify the order of the returned rows use [orderBy] or [orderByList]
|
||||
/// when sorting by multiple columns.
|
||||
///
|
||||
/// If [noReturn] is set to `true`, the deleted rows are not read back from
|
||||
/// the database and an empty list is returned. This avoids the overhead of
|
||||
/// transferring and deserializing the rows when the result is not needed.
|
||||
Future<List<AccountPassword>> deleteWhere(
|
||||
_is.DatabaseSession session, {
|
||||
required _is.WhereExpressionBuilder<AccountPasswordTable> where,
|
||||
_is.OrderByBuilder<AccountPasswordTable>? orderBy,
|
||||
_is.OrderByListBuilder<AccountPasswordTable>? orderByList,
|
||||
_is.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.deleteWhere<AccountPassword>(
|
||||
where: where(AccountPassword.t),
|
||||
orderBy: orderBy?.call(AccountPassword.t),
|
||||
orderByList: orderByList?.call(AccountPassword.t),
|
||||
transaction: transaction,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Counts the number of rows matching the [where] expression. If omitted,
|
||||
/// will return the count of all rows in the table.
|
||||
Future<int> count(
|
||||
_is.DatabaseSession session, {
|
||||
_is.WhereExpressionBuilder<AccountPasswordTable>? where,
|
||||
int? limit,
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.count<AccountPassword>(
|
||||
where: where?.call(AccountPassword.t),
|
||||
limit: limit,
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Acquires row-level locks on [AccountPassword] rows matching the [where] expression.
|
||||
Future<void> lockRows(
|
||||
_is.DatabaseSession session, {
|
||||
required _is.WhereExpressionBuilder<AccountPasswordTable> where,
|
||||
required _is.LockMode lockMode,
|
||||
required _is.Transaction transaction,
|
||||
_is.LockBehavior lockBehavior = _is.LockBehavior.wait,
|
||||
}) async {
|
||||
return session.db.lockRows<AccountPassword>(
|
||||
where: where(AccountPassword.t),
|
||||
lockMode: lockMode,
|
||||
lockBehavior: lockBehavior,
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,666 @@
|
|||
/* 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 '../password/account_reset_kind.dart' as _itrk9fro;
|
||||
|
||||
/// A reset asked for that takes effect only later (ADR 0017), so the
|
||||
/// account's owner, told by mail, can call it off by signing in meanwhile:
|
||||
/// the second factor's, or the whole account's (new keys).
|
||||
abstract class AccountReset
|
||||
implements _is.TableRow<int?>, _is.ProtocolSerialization {
|
||||
AccountReset._({
|
||||
this.id,
|
||||
required this.authUserId,
|
||||
required this.kind,
|
||||
required this.takesEffectAt,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
factory AccountReset({
|
||||
int? id,
|
||||
required _is.UuidValue authUserId,
|
||||
required _itrk9fro.AccountResetKind kind,
|
||||
required DateTime takesEffectAt,
|
||||
required DateTime createdAt,
|
||||
}) = _AccountResetImpl;
|
||||
|
||||
factory AccountReset.fromJson(Map<String, dynamic> jsonSerialization) {
|
||||
return AccountReset(
|
||||
id: jsonSerialization['id'] as int?,
|
||||
authUserId: _is.UuidValueJsonExtension.fromJson(
|
||||
jsonSerialization['authUserId'],
|
||||
),
|
||||
kind: _itrk9fro.AccountResetKind.fromJson(
|
||||
(jsonSerialization['kind'] as String),
|
||||
),
|
||||
takesEffectAt: _is.DateTimeJsonExtension.fromJson(
|
||||
jsonSerialization['takesEffectAt'],
|
||||
),
|
||||
createdAt: _is.DateTimeJsonExtension.fromJson(
|
||||
jsonSerialization['createdAt'],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static final t = AccountResetTable();
|
||||
|
||||
static const db = AccountResetRepository._();
|
||||
|
||||
@override
|
||||
int? id;
|
||||
|
||||
/// The account: its person's ID (deleted with them, `deletePerson`).
|
||||
_is.UuidValue authUserId;
|
||||
|
||||
/// Which.
|
||||
_itrk9fro.AccountResetKind kind;
|
||||
|
||||
/// When it may be carried out.
|
||||
DateTime takesEffectAt;
|
||||
|
||||
DateTime createdAt;
|
||||
|
||||
@override
|
||||
_is.Table<int?> get table => t;
|
||||
|
||||
/// Returns a shallow copy of this [AccountReset]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
AccountReset copyWith({
|
||||
int? id,
|
||||
_is.UuidValue? authUserId,
|
||||
_itrk9fro.AccountResetKind? kind,
|
||||
DateTime? takesEffectAt,
|
||||
DateTime? createdAt,
|
||||
});
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'__className__': 'halkyon_base.AccountReset',
|
||||
if (id != null) 'id': id,
|
||||
'authUserId': authUserId.toJson(),
|
||||
'kind': kind.toJson(),
|
||||
'takesEffectAt': takesEffectAt.toJson(),
|
||||
'createdAt': createdAt.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJsonForProtocol() {
|
||||
return {};
|
||||
}
|
||||
|
||||
static AccountResetInclude include() {
|
||||
return AccountResetInclude._();
|
||||
}
|
||||
|
||||
static AccountResetIncludeList includeList({
|
||||
_is.WhereExpressionBuilder<AccountResetTable>? where,
|
||||
int? limit,
|
||||
int? offset,
|
||||
_is.OrderByBuilder<AccountResetTable>? orderBy,
|
||||
_is.OrderByListBuilder<AccountResetTable>? orderByList,
|
||||
AccountResetInclude? include,
|
||||
}) {
|
||||
return AccountResetIncludeList._(
|
||||
where: where,
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
orderBy: orderBy?.call(AccountReset.t),
|
||||
orderByList: orderByList?.call(AccountReset.t),
|
||||
include: include,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return _is.SerializationManager.encode(this);
|
||||
}
|
||||
}
|
||||
|
||||
class _Undefined {}
|
||||
|
||||
class _AccountResetImpl extends AccountReset {
|
||||
_AccountResetImpl({
|
||||
int? id,
|
||||
required _is.UuidValue authUserId,
|
||||
required _itrk9fro.AccountResetKind kind,
|
||||
required DateTime takesEffectAt,
|
||||
required DateTime createdAt,
|
||||
}) : super._(
|
||||
id: id,
|
||||
authUserId: authUserId,
|
||||
kind: kind,
|
||||
takesEffectAt: takesEffectAt,
|
||||
createdAt: createdAt,
|
||||
);
|
||||
|
||||
/// Returns a shallow copy of this [AccountReset]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
@override
|
||||
AccountReset copyWith({
|
||||
Object? id = _Undefined,
|
||||
_is.UuidValue? authUserId,
|
||||
_itrk9fro.AccountResetKind? kind,
|
||||
DateTime? takesEffectAt,
|
||||
DateTime? createdAt,
|
||||
}) {
|
||||
return AccountReset(
|
||||
id: id is int? ? id : this.id,
|
||||
authUserId: authUserId ?? this.authUserId,
|
||||
kind: kind ?? this.kind,
|
||||
takesEffectAt: takesEffectAt ?? this.takesEffectAt,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AccountResetUpdateTable extends _is.UpdateTable<AccountResetTable> {
|
||||
AccountResetUpdateTable(super.table);
|
||||
|
||||
_is.ColumnValue<_is.UuidValue, _is.UuidValue> authUserId(
|
||||
_is.UuidValue value,
|
||||
) => _is.ColumnValue(
|
||||
table.authUserId,
|
||||
value,
|
||||
);
|
||||
|
||||
_is.ColumnValue<_itrk9fro.AccountResetKind, _itrk9fro.AccountResetKind> kind(
|
||||
_itrk9fro.AccountResetKind value,
|
||||
) => _is.ColumnValue(
|
||||
table.kind,
|
||||
value,
|
||||
);
|
||||
|
||||
_is.ColumnValue<DateTime, DateTime> takesEffectAt(DateTime value) =>
|
||||
_is.ColumnValue(
|
||||
table.takesEffectAt,
|
||||
value,
|
||||
);
|
||||
|
||||
_is.ColumnValue<DateTime, DateTime> createdAt(DateTime value) =>
|
||||
_is.ColumnValue(
|
||||
table.createdAt,
|
||||
value,
|
||||
);
|
||||
}
|
||||
|
||||
class AccountResetTable extends _is.Table<int?> {
|
||||
AccountResetTable({super.tableRelation}) : super(tableName: 'account_reset') {
|
||||
updateTable = AccountResetUpdateTable(this);
|
||||
authUserId = _is.ColumnUuid(
|
||||
'authUserId',
|
||||
this,
|
||||
);
|
||||
kind = _is.ColumnEnum(
|
||||
'kind',
|
||||
this,
|
||||
_is.EnumSerialization.byName,
|
||||
);
|
||||
takesEffectAt = _is.ColumnDateTime(
|
||||
'takesEffectAt',
|
||||
this,
|
||||
);
|
||||
createdAt = _is.ColumnDateTime(
|
||||
'createdAt',
|
||||
this,
|
||||
);
|
||||
}
|
||||
|
||||
late final AccountResetUpdateTable updateTable;
|
||||
|
||||
/// The account: its person's ID (deleted with them, `deletePerson`).
|
||||
late final _is.ColumnUuid authUserId;
|
||||
|
||||
/// Which.
|
||||
late final _is.ColumnEnum<_itrk9fro.AccountResetKind> kind;
|
||||
|
||||
/// When it may be carried out.
|
||||
late final _is.ColumnDateTime takesEffectAt;
|
||||
|
||||
late final _is.ColumnDateTime createdAt;
|
||||
|
||||
@override
|
||||
List<_is.Column> get columns => [
|
||||
id,
|
||||
authUserId,
|
||||
kind,
|
||||
takesEffectAt,
|
||||
createdAt,
|
||||
];
|
||||
}
|
||||
|
||||
class AccountResetInclude extends _is.IncludeObject {
|
||||
AccountResetInclude._();
|
||||
|
||||
@override
|
||||
Map<String, _is.Include?> get includes => {};
|
||||
|
||||
@override
|
||||
_is.Table<int?> get table => AccountReset.t;
|
||||
}
|
||||
|
||||
class AccountResetIncludeList extends _is.IncludeList {
|
||||
AccountResetIncludeList._({
|
||||
_is.WhereExpressionBuilder<AccountResetTable>? where,
|
||||
super.limit,
|
||||
super.offset,
|
||||
super.orderBy,
|
||||
super.orderByList,
|
||||
super.include,
|
||||
}) {
|
||||
super.where = where?.call(AccountReset.t);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, _is.Include?> get includes => include?.includes ?? {};
|
||||
|
||||
@override
|
||||
_is.Table<int?> get table => AccountReset.t;
|
||||
}
|
||||
|
||||
class AccountResetRepository {
|
||||
const AccountResetRepository._();
|
||||
|
||||
/// Returns a list of [AccountReset]s matching the given query parameters.
|
||||
///
|
||||
/// Use [where] to specify which items to include in the return value.
|
||||
/// If none is specified, all items will be returned.
|
||||
///
|
||||
/// To specify the order of the items use [orderBy] or [orderByList]
|
||||
/// when sorting by multiple columns.
|
||||
///
|
||||
/// The maximum number of items can be set by [limit]. If no limit is set,
|
||||
/// all items matching the query will be returned.
|
||||
///
|
||||
/// [offset] defines how many items to skip, after which [limit] (or all)
|
||||
/// items are read from the database.
|
||||
///
|
||||
/// ```dart
|
||||
/// var persons = await Persons.db.find(
|
||||
/// session,
|
||||
/// where: (t) => t.lastName.equals('Jones'),
|
||||
/// orderBy: (t) => t.firstName,
|
||||
/// limit: 100,
|
||||
/// );
|
||||
/// ```
|
||||
Future<List<AccountReset>> find(
|
||||
_is.DatabaseSession session, {
|
||||
_is.WhereExpressionBuilder<AccountResetTable>? where,
|
||||
int? limit,
|
||||
int? offset,
|
||||
_is.OrderByBuilder<AccountResetTable>? orderBy,
|
||||
_is.OrderByListBuilder<AccountResetTable>? orderByList,
|
||||
_is.Transaction? transaction,
|
||||
_is.LockMode? lockMode,
|
||||
_is.LockBehavior? lockBehavior,
|
||||
}) async {
|
||||
return session.db.find<AccountReset>(
|
||||
where: where?.call(AccountReset.t),
|
||||
orderBy: orderBy?.call(AccountReset.t),
|
||||
orderByList: orderByList?.call(AccountReset.t),
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
transaction: transaction,
|
||||
lockMode: lockMode,
|
||||
lockBehavior: lockBehavior,
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns the first matching [AccountReset] matching the given query parameters.
|
||||
///
|
||||
/// Use [where] to specify which items to include in the return value.
|
||||
/// If none is specified, all items will be returned.
|
||||
///
|
||||
/// To specify the order use [orderBy] or [orderByList]
|
||||
/// when sorting by multiple columns.
|
||||
///
|
||||
/// [offset] defines how many items to skip, after which the next one will be picked.
|
||||
///
|
||||
/// ```dart
|
||||
/// var youngestPerson = await Persons.db.findFirstRow(
|
||||
/// session,
|
||||
/// where: (t) => t.lastName.equals('Jones'),
|
||||
/// orderBy: (t) => t.age,
|
||||
/// );
|
||||
/// ```
|
||||
Future<AccountReset?> findFirstRow(
|
||||
_is.DatabaseSession session, {
|
||||
_is.WhereExpressionBuilder<AccountResetTable>? where,
|
||||
int? offset,
|
||||
_is.OrderByBuilder<AccountResetTable>? orderBy,
|
||||
_is.OrderByListBuilder<AccountResetTable>? orderByList,
|
||||
_is.Transaction? transaction,
|
||||
_is.LockMode? lockMode,
|
||||
_is.LockBehavior? lockBehavior,
|
||||
}) async {
|
||||
return session.db.findFirstRow<AccountReset>(
|
||||
where: where?.call(AccountReset.t),
|
||||
orderBy: orderBy?.call(AccountReset.t),
|
||||
orderByList: orderByList?.call(AccountReset.t),
|
||||
offset: offset,
|
||||
transaction: transaction,
|
||||
lockMode: lockMode,
|
||||
lockBehavior: lockBehavior,
|
||||
);
|
||||
}
|
||||
|
||||
/// Finds a single [AccountReset] by its [id] or null if no such row exists.
|
||||
Future<AccountReset?> findById(
|
||||
_is.DatabaseSession session,
|
||||
int id, {
|
||||
_is.Transaction? transaction,
|
||||
_is.LockMode? lockMode,
|
||||
_is.LockBehavior? lockBehavior,
|
||||
}) async {
|
||||
return session.db.findById<AccountReset>(
|
||||
id,
|
||||
transaction: transaction,
|
||||
lockMode: lockMode,
|
||||
lockBehavior: lockBehavior,
|
||||
);
|
||||
}
|
||||
|
||||
/// Inserts all [AccountReset]s in the list and returns the inserted rows.
|
||||
///
|
||||
/// The returned [AccountReset]s will have their `id` fields set.
|
||||
///
|
||||
/// This is an atomic operation, meaning that if one of the rows fails to
|
||||
/// insert, none of the rows will be inserted.
|
||||
///
|
||||
/// If [ignoreConflicts] is set to `true`, rows that conflict with existing
|
||||
/// rows are silently skipped, and only the successfully inserted rows are
|
||||
/// returned.
|
||||
///
|
||||
/// If [noReturn] is set to `true`, the inserted rows are not read back from
|
||||
/// the database and an empty list is returned. This avoids the overhead of
|
||||
/// transferring and deserializing the rows when the result is not needed.
|
||||
Future<List<AccountReset>> insert(
|
||||
_is.DatabaseSession session,
|
||||
List<AccountReset> rows, {
|
||||
_is.Transaction? transaction,
|
||||
bool ignoreConflicts = false,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.insert<AccountReset>(
|
||||
rows,
|
||||
transaction: transaction,
|
||||
ignoreConflicts: ignoreConflicts,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Inserts a single [AccountReset] and returns the inserted row.
|
||||
///
|
||||
/// The returned [AccountReset] will have its `id` field set.
|
||||
Future<AccountReset> insertRow(
|
||||
_is.DatabaseSession session,
|
||||
AccountReset row, {
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.insertRow<AccountReset>(
|
||||
row,
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Upserts all [AccountReset]s in the list and returns the resulting rows.
|
||||
///
|
||||
/// If a row conflicts on the given [conflictColumns], the existing row is
|
||||
/// updated with the new values. Otherwise, a new row is inserted.
|
||||
///
|
||||
/// If [updateColumns] is provided, only those columns will be updated on
|
||||
/// conflict. If null, all non-conflict, non-id columns are updated.
|
||||
///
|
||||
/// If [updateWhere] is provided, the update only applies to rows matching the
|
||||
/// given expression. Conflicting rows that don't match are skipped and not
|
||||
/// returned, so the resulting list may be shorter than [rows].
|
||||
///
|
||||
/// The returned [AccountReset]s will have their `id` fields set.
|
||||
///
|
||||
/// This is an atomic operation, meaning that if one of the rows fails,
|
||||
/// none of the rows will be affected.
|
||||
///
|
||||
/// If [noReturn] is set to `true`, the resulting rows are not read back from
|
||||
/// the database and an empty list is returned. This avoids the overhead of
|
||||
/// transferring and deserializing the rows when the result is not needed.
|
||||
Future<List<AccountReset>> upsert(
|
||||
_is.DatabaseSession session,
|
||||
List<AccountReset> rows, {
|
||||
required _is.ColumnSelections<AccountResetTable> conflictColumns,
|
||||
_is.ColumnSelections<AccountResetTable>? updateColumns,
|
||||
_is.WhereExpressionBuilder<AccountResetTable>? updateWhere,
|
||||
_is.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.upsert<AccountReset>(
|
||||
rows,
|
||||
conflictColumns: conflictColumns(AccountReset.t),
|
||||
updateColumns: updateColumns?.call(AccountReset.t),
|
||||
updateWhere: updateWhere?.call(AccountReset.t),
|
||||
transaction: transaction,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Upserts a single [AccountReset] and returns the resulting row.
|
||||
///
|
||||
/// If the row conflicts on the given [conflictColumns], the existing row is
|
||||
/// updated. Otherwise, a new row is inserted.
|
||||
///
|
||||
/// If [updateColumns] is provided, only those columns will be updated on
|
||||
/// conflict. If null, all non-conflict, non-id columns are updated.
|
||||
///
|
||||
/// If [updateWhere] is provided, the update only applies when the existing
|
||||
/// row matches the expression. Returns `null` if no row was affected — for
|
||||
/// example when [updateWhere] does not match the conflicting row.
|
||||
///
|
||||
/// The returned [AccountReset] will have its `id` field set.
|
||||
Future<AccountReset?> upsertRow(
|
||||
_is.DatabaseSession session,
|
||||
AccountReset row, {
|
||||
required _is.ColumnSelections<AccountResetTable> conflictColumns,
|
||||
_is.ColumnSelections<AccountResetTable>? updateColumns,
|
||||
_is.WhereExpressionBuilder<AccountResetTable>? updateWhere,
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.upsertRow<AccountReset>(
|
||||
row,
|
||||
conflictColumns: conflictColumns(AccountReset.t),
|
||||
updateColumns: updateColumns?.call(AccountReset.t),
|
||||
updateWhere: updateWhere?.call(AccountReset.t),
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates all [AccountReset]s in the list and returns the updated rows. If
|
||||
/// [columns] is provided, only those columns will be updated. Defaults to
|
||||
/// all columns.
|
||||
/// This is an atomic operation, meaning that if one of the rows fails to
|
||||
/// update, none of the rows will be updated.
|
||||
///
|
||||
/// If [noReturn] is set to `true`, the updated rows are not read back from
|
||||
/// the database and an empty list is returned. This avoids the overhead of
|
||||
/// transferring and deserializing the rows when the result is not needed.
|
||||
Future<List<AccountReset>> update(
|
||||
_is.DatabaseSession session,
|
||||
List<AccountReset> rows, {
|
||||
_is.ColumnSelections<AccountResetTable>? columns,
|
||||
_is.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.update<AccountReset>(
|
||||
rows,
|
||||
columns: columns?.call(AccountReset.t),
|
||||
transaction: transaction,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates a single [AccountReset]. The row needs to have its id set.
|
||||
/// Optionally, a list of [columns] can be provided to only update those
|
||||
/// columns. Defaults to all columns.
|
||||
Future<AccountReset> updateRow(
|
||||
_is.DatabaseSession session,
|
||||
AccountReset row, {
|
||||
_is.ColumnSelections<AccountResetTable>? columns,
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.updateRow<AccountReset>(
|
||||
row,
|
||||
columns: columns?.call(AccountReset.t),
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates a single [AccountReset] by its [id] with the specified [columnValues].
|
||||
/// Returns the updated row or null if no row with the given id exists.
|
||||
Future<AccountReset?> updateById(
|
||||
_is.DatabaseSession session,
|
||||
int id, {
|
||||
required _is.ColumnValueListBuilder<AccountResetUpdateTable> columnValues,
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.updateById<AccountReset>(
|
||||
id,
|
||||
columnValues: columnValues(AccountReset.t.updateTable),
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates all [AccountReset]s matching the [where] expression with the specified [columnValues].
|
||||
/// Returns the list of updated rows.
|
||||
///
|
||||
/// If [noReturn] is set to `true`, the updated rows are not read back from
|
||||
/// the database and an empty list is returned. This avoids the overhead of
|
||||
/// transferring and deserializing the rows when the result is not needed.
|
||||
Future<List<AccountReset>> updateWhere(
|
||||
_is.DatabaseSession session, {
|
||||
required _is.ColumnValueListBuilder<AccountResetUpdateTable> columnValues,
|
||||
required _is.WhereExpressionBuilder<AccountResetTable> where,
|
||||
int? limit,
|
||||
int? offset,
|
||||
_is.OrderByBuilder<AccountResetTable>? orderBy,
|
||||
_is.OrderByListBuilder<AccountResetTable>? orderByList,
|
||||
_is.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.updateWhere<AccountReset>(
|
||||
columnValues: columnValues(AccountReset.t.updateTable),
|
||||
where: where(AccountReset.t),
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
orderBy: orderBy?.call(AccountReset.t),
|
||||
orderByList: orderByList?.call(AccountReset.t),
|
||||
transaction: transaction,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Deletes all [AccountReset]s in the list and returns the deleted rows.
|
||||
///
|
||||
/// To specify the order of the returned rows use [orderBy] or [orderByList]
|
||||
/// when sorting by multiple columns.
|
||||
///
|
||||
/// This is an atomic operation, meaning that if one of the rows fail to
|
||||
/// be deleted, none of the rows will be deleted.
|
||||
///
|
||||
/// If [noReturn] is set to `true`, the deleted rows are not read back from
|
||||
/// the database and an empty list is returned. This avoids the overhead of
|
||||
/// transferring and deserializing the rows when the result is not needed.
|
||||
Future<List<AccountReset>> delete(
|
||||
_is.DatabaseSession session,
|
||||
List<AccountReset> rows, {
|
||||
_is.OrderByBuilder<AccountResetTable>? orderBy,
|
||||
_is.OrderByListBuilder<AccountResetTable>? orderByList,
|
||||
_is.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.delete<AccountReset>(
|
||||
rows,
|
||||
orderBy: orderBy?.call(AccountReset.t),
|
||||
orderByList: orderByList?.call(AccountReset.t),
|
||||
transaction: transaction,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Deletes a single [AccountReset].
|
||||
Future<AccountReset> deleteRow(
|
||||
_is.DatabaseSession session,
|
||||
AccountReset row, {
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.deleteRow<AccountReset>(
|
||||
row,
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Deletes all rows matching the [where] expression.
|
||||
///
|
||||
/// To specify the order of the returned rows use [orderBy] or [orderByList]
|
||||
/// when sorting by multiple columns.
|
||||
///
|
||||
/// If [noReturn] is set to `true`, the deleted rows are not read back from
|
||||
/// the database and an empty list is returned. This avoids the overhead of
|
||||
/// transferring and deserializing the rows when the result is not needed.
|
||||
Future<List<AccountReset>> deleteWhere(
|
||||
_is.DatabaseSession session, {
|
||||
required _is.WhereExpressionBuilder<AccountResetTable> where,
|
||||
_is.OrderByBuilder<AccountResetTable>? orderBy,
|
||||
_is.OrderByListBuilder<AccountResetTable>? orderByList,
|
||||
_is.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.deleteWhere<AccountReset>(
|
||||
where: where(AccountReset.t),
|
||||
orderBy: orderBy?.call(AccountReset.t),
|
||||
orderByList: orderByList?.call(AccountReset.t),
|
||||
transaction: transaction,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Counts the number of rows matching the [where] expression. If omitted,
|
||||
/// will return the count of all rows in the table.
|
||||
Future<int> count(
|
||||
_is.DatabaseSession session, {
|
||||
_is.WhereExpressionBuilder<AccountResetTable>? where,
|
||||
int? limit,
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.count<AccountReset>(
|
||||
where: where?.call(AccountReset.t),
|
||||
limit: limit,
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Acquires row-level locks on [AccountReset] rows matching the [where] expression.
|
||||
Future<void> lockRows(
|
||||
_is.DatabaseSession session, {
|
||||
required _is.WhereExpressionBuilder<AccountResetTable> where,
|
||||
required _is.LockMode lockMode,
|
||||
required _is.Transaction transaction,
|
||||
_is.LockBehavior lockBehavior = _is.LockBehavior.wait,
|
||||
}) async {
|
||||
return session.db.lockRows<AccountReset>(
|
||||
where: where(AccountReset.t),
|
||||
lockMode: lockMode,
|
||||
lockBehavior: lockBehavior,
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
/* 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;
|
||||
|
||||
/// Which reset an account waits for.
|
||||
enum AccountResetKind implements _is.SerializableModel {
|
||||
/// Its authenticators and recovery codes go: a lost second factor.
|
||||
authenticator,
|
||||
|
||||
/// Its password and keys are made anew: a lost password without the
|
||||
/// recovery phrase, which loses what was encrypted under the old keys.
|
||||
account;
|
||||
|
||||
static AccountResetKind fromJson(String name) {
|
||||
switch (name) {
|
||||
case 'authenticator':
|
||||
return AccountResetKind.authenticator;
|
||||
case 'account':
|
||||
return AccountResetKind.account;
|
||||
default:
|
||||
throw ArgumentError(
|
||||
'Value "$name" cannot be converted to "AccountResetKind"',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String toJson() => name;
|
||||
|
||||
@override
|
||||
String toString() => name;
|
||||
}
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
/* 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;
|
||||
|
||||
/// One of the account's authenticators, as its owner sees them: never its
|
||||
/// secret.
|
||||
abstract class Authenticator
|
||||
implements _is.SerializableModel, _is.ProtocolSerialization {
|
||||
Authenticator._({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.confirmed,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
factory Authenticator({
|
||||
required _is.UuidValue id,
|
||||
required String name,
|
||||
required bool confirmed,
|
||||
required DateTime createdAt,
|
||||
}) = _AuthenticatorImpl;
|
||||
|
||||
factory Authenticator.fromJson(Map<String, dynamic> jsonSerialization) {
|
||||
return Authenticator(
|
||||
id: _is.UuidValueJsonExtension.fromJson(jsonSerialization['id']),
|
||||
name: jsonSerialization['name'] as String,
|
||||
confirmed: _is.BoolJsonExtension.fromJson(jsonSerialization['confirmed']),
|
||||
createdAt: _is.DateTimeJsonExtension.fromJson(
|
||||
jsonSerialization['createdAt'],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_is.UuidValue id;
|
||||
|
||||
String name;
|
||||
|
||||
/// Whether a code from it was accepted; one that wasn't counts for nothing.
|
||||
bool confirmed;
|
||||
|
||||
DateTime createdAt;
|
||||
|
||||
/// Returns a shallow copy of this [Authenticator]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
Authenticator copyWith({
|
||||
_is.UuidValue? id,
|
||||
String? name,
|
||||
bool? confirmed,
|
||||
DateTime? createdAt,
|
||||
});
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'__className__': 'halkyon_base.Authenticator',
|
||||
'id': id.toJson(),
|
||||
'name': name,
|
||||
'confirmed': confirmed,
|
||||
'createdAt': createdAt.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJsonForProtocol() {
|
||||
return {
|
||||
'__className__': 'halkyon_base.Authenticator',
|
||||
'id': id.toJson(),
|
||||
'name': name,
|
||||
'confirmed': confirmed,
|
||||
'createdAt': createdAt.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return _is.SerializationManager.encode(this);
|
||||
}
|
||||
}
|
||||
|
||||
class _AuthenticatorImpl extends Authenticator {
|
||||
_AuthenticatorImpl({
|
||||
required _is.UuidValue id,
|
||||
required String name,
|
||||
required bool confirmed,
|
||||
required DateTime createdAt,
|
||||
}) : super._(
|
||||
id: id,
|
||||
name: name,
|
||||
confirmed: confirmed,
|
||||
createdAt: createdAt,
|
||||
);
|
||||
|
||||
/// Returns a shallow copy of this [Authenticator]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
@override
|
||||
Authenticator copyWith({
|
||||
_is.UuidValue? id,
|
||||
String? name,
|
||||
bool? confirmed,
|
||||
DateTime? createdAt,
|
||||
}) {
|
||||
return Authenticator(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
confirmed: confirmed ?? this.confirmed,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
/* 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;
|
||||
|
||||
/// A new authenticator (ADR 0017): the `otpauth://` URI its app reads from a
|
||||
/// QR code, shown this once, and the ID a code from it confirms.
|
||||
abstract class AuthenticatorEnrolment
|
||||
implements _is.SerializableModel, _is.ProtocolSerialization {
|
||||
AuthenticatorEnrolment._({
|
||||
required this.authenticator,
|
||||
required this.uri,
|
||||
});
|
||||
|
||||
factory AuthenticatorEnrolment({
|
||||
required _is.UuidValue authenticator,
|
||||
required String uri,
|
||||
}) = _AuthenticatorEnrolmentImpl;
|
||||
|
||||
factory AuthenticatorEnrolment.fromJson(
|
||||
Map<String, dynamic> jsonSerialization,
|
||||
) {
|
||||
return AuthenticatorEnrolment(
|
||||
authenticator: _is.UuidValueJsonExtension.fromJson(
|
||||
jsonSerialization['authenticator'],
|
||||
),
|
||||
uri: jsonSerialization['uri'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
_is.UuidValue authenticator;
|
||||
|
||||
String uri;
|
||||
|
||||
/// Returns a shallow copy of this [AuthenticatorEnrolment]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
AuthenticatorEnrolment copyWith({
|
||||
_is.UuidValue? authenticator,
|
||||
String? uri,
|
||||
});
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'__className__': 'halkyon_base.AuthenticatorEnrolment',
|
||||
'authenticator': authenticator.toJson(),
|
||||
'uri': uri,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJsonForProtocol() {
|
||||
return {
|
||||
'__className__': 'halkyon_base.AuthenticatorEnrolment',
|
||||
'authenticator': authenticator.toJson(),
|
||||
'uri': uri,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return _is.SerializationManager.encode(this);
|
||||
}
|
||||
}
|
||||
|
||||
class _AuthenticatorEnrolmentImpl extends AuthenticatorEnrolment {
|
||||
_AuthenticatorEnrolmentImpl({
|
||||
required _is.UuidValue authenticator,
|
||||
required String uri,
|
||||
}) : super._(
|
||||
authenticator: authenticator,
|
||||
uri: uri,
|
||||
);
|
||||
|
||||
/// Returns a shallow copy of this [AuthenticatorEnrolment]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
@override
|
||||
AuthenticatorEnrolment copyWith({
|
||||
_is.UuidValue? authenticator,
|
||||
String? uri,
|
||||
}) {
|
||||
return AuthenticatorEnrolment(
|
||||
authenticator: authenticator ?? this.authenticator,
|
||||
uri: uri ?? this.uri,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
/* 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 'dart:typed_data' as _idt;
|
||||
import 'package:halkyon_base_server/src/generated/protocol.dart' as _ibewtqgw;
|
||||
import 'package:serverpod/serverpod.dart' as _is;
|
||||
import '../password/wrapped_keys.dart' as _i2rxumte;
|
||||
|
||||
/// What a device needs to prove the recovery phrase (ADR 0017): the
|
||||
/// account's private keys wrapped under the phrase's key and its public
|
||||
/// keys, which the device opens with the phrase, and a nonce it signs with
|
||||
/// the signing key so opened, within two minutes, once. Alike for an address
|
||||
/// nobody has.
|
||||
abstract class KeyProofChallenge
|
||||
implements _is.SerializableModel, _is.ProtocolSerialization {
|
||||
KeyProofChallenge._({
|
||||
required this.challenge,
|
||||
required this.nonce,
|
||||
required this.encryptionPublicKey,
|
||||
required this.signingPublicKey,
|
||||
required this.recoveryKeys,
|
||||
});
|
||||
|
||||
factory KeyProofChallenge({
|
||||
required _is.UuidValue challenge,
|
||||
required _idt.ByteData nonce,
|
||||
required _idt.ByteData encryptionPublicKey,
|
||||
required _idt.ByteData signingPublicKey,
|
||||
required _i2rxumte.WrappedKeys recoveryKeys,
|
||||
}) = _KeyProofChallengeImpl;
|
||||
|
||||
factory KeyProofChallenge.fromJson(Map<String, dynamic> jsonSerialization) {
|
||||
return KeyProofChallenge(
|
||||
challenge: _is.UuidValueJsonExtension.fromJson(
|
||||
jsonSerialization['challenge'],
|
||||
),
|
||||
nonce: _is.ByteDataJsonExtension.fromJson(jsonSerialization['nonce']),
|
||||
encryptionPublicKey: _is.ByteDataJsonExtension.fromJson(
|
||||
jsonSerialization['encryptionPublicKey'],
|
||||
),
|
||||
signingPublicKey: _is.ByteDataJsonExtension.fromJson(
|
||||
jsonSerialization['signingPublicKey'],
|
||||
),
|
||||
recoveryKeys: _ibewtqgw.Protocol().deserialize<_i2rxumte.WrappedKeys>(
|
||||
jsonSerialization['recoveryKeys'],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_is.UuidValue challenge;
|
||||
|
||||
_idt.ByteData nonce;
|
||||
|
||||
_idt.ByteData encryptionPublicKey;
|
||||
|
||||
_idt.ByteData signingPublicKey;
|
||||
|
||||
_i2rxumte.WrappedKeys recoveryKeys;
|
||||
|
||||
/// Returns a shallow copy of this [KeyProofChallenge]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
KeyProofChallenge copyWith({
|
||||
_is.UuidValue? challenge,
|
||||
_idt.ByteData? nonce,
|
||||
_idt.ByteData? encryptionPublicKey,
|
||||
_idt.ByteData? signingPublicKey,
|
||||
_i2rxumte.WrappedKeys? recoveryKeys,
|
||||
});
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'__className__': 'halkyon_base.KeyProofChallenge',
|
||||
'challenge': challenge.toJson(),
|
||||
'nonce': nonce.toJson(),
|
||||
'encryptionPublicKey': encryptionPublicKey.toJson(),
|
||||
'signingPublicKey': signingPublicKey.toJson(),
|
||||
'recoveryKeys': recoveryKeys.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJsonForProtocol() {
|
||||
return {
|
||||
'__className__': 'halkyon_base.KeyProofChallenge',
|
||||
'challenge': challenge.toJson(),
|
||||
'nonce': nonce.toJson(),
|
||||
'encryptionPublicKey': encryptionPublicKey.toJson(),
|
||||
'signingPublicKey': signingPublicKey.toJson(),
|
||||
'recoveryKeys': recoveryKeys.toJsonForProtocol(),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return _is.SerializationManager.encode(this);
|
||||
}
|
||||
}
|
||||
|
||||
class _KeyProofChallengeImpl extends KeyProofChallenge {
|
||||
_KeyProofChallengeImpl({
|
||||
required _is.UuidValue challenge,
|
||||
required _idt.ByteData nonce,
|
||||
required _idt.ByteData encryptionPublicKey,
|
||||
required _idt.ByteData signingPublicKey,
|
||||
required _i2rxumte.WrappedKeys recoveryKeys,
|
||||
}) : super._(
|
||||
challenge: challenge,
|
||||
nonce: nonce,
|
||||
encryptionPublicKey: encryptionPublicKey,
|
||||
signingPublicKey: signingPublicKey,
|
||||
recoveryKeys: recoveryKeys,
|
||||
);
|
||||
|
||||
/// Returns a shallow copy of this [KeyProofChallenge]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
@override
|
||||
KeyProofChallenge copyWith({
|
||||
_is.UuidValue? challenge,
|
||||
_idt.ByteData? nonce,
|
||||
_idt.ByteData? encryptionPublicKey,
|
||||
_idt.ByteData? signingPublicKey,
|
||||
_i2rxumte.WrappedKeys? recoveryKeys,
|
||||
}) {
|
||||
return KeyProofChallenge(
|
||||
challenge: challenge ?? this.challenge,
|
||||
nonce: nonce ?? this.nonce.clone(),
|
||||
encryptionPublicKey:
|
||||
encryptionPublicKey ?? this.encryptionPublicKey.clone(),
|
||||
signingPublicKey: signingPublicKey ?? this.signingPublicKey.clone(),
|
||||
recoveryKeys: recoveryKeys ?? this.recoveryKeys.copyWith(),
|
||||
);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue