fix(auth): a reset waits while its owner is held out, and devices show it
The full reading's H1: an owner could be kept from calling a reset off, and the reset then went through. Whoever knew the password and had the mailbox asked an authenticators' reset and kept the codes waiting, so the owner's sign-ins were refused until it took effect; whoever had the mailbox filled the address's proofs ceiling from three networks and completed an account reset. Now every hold-out of the owner after a reset was asked (a wait that begins, an attempt a wait refuses, a sign-in or proof refused by the address's ceiling) moves the account's resets not carried out to take effect no sooner than 72 hours after it ends (pushOutResets). A reset carried out (the password alone signing in until an authenticator is confirmed) is never pushed, nor a lapsed one brought back. Signed-in devices are told too: SignInWays carries when a waiting reset of each kind takes effect, the account section shows them with Call it off, which takes the owner's proof (password and code; password. callOffResets, forwarded by members) and calls every reset off, as no wait holds a signed-in owner's proof back. Texts in English and German. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
4d1cd2045c
commit
9d0d037c86
34 changed files with 1022 additions and 18 deletions
|
|
@ -225,5 +225,11 @@
|
||||||
"passwordWarningTitle": "Jemand hat dein Passwort ausprobiert",
|
"passwordWarningTitle": "Jemand hat dein Passwort ausprobiert",
|
||||||
"passwordWarningBody": "Am {date} hat ein bei deinem Konto angemeldetes Gerät zehnmal hintereinander ein falsches Passwort oder einen falschen Code gesendet und wurde abgemeldet. Warst du das nicht, hat vielleicht jemand anderes dieses Gerät oder seine Sitzung: Ändere dein Passwort.",
|
"passwordWarningBody": "Am {date} hat ein bei deinem Konto angemeldetes Gerät zehnmal hintereinander ein falsches Passwort oder einen falschen Code gesendet und wurde abgemeldet. Warst du das nicht, hat vielleicht jemand anderes dieses Gerät oder seine Sitzung: Ändere dein Passwort.",
|
||||||
"passwordWarningDismiss": "Ausblenden",
|
"passwordWarningDismiss": "Ausblenden",
|
||||||
"passwordWarningLater": "Später erinnern"
|
"passwordWarningLater": "Später erinnern",
|
||||||
|
"passwordResetsTitle": "Ein Zurücksetzen steht an",
|
||||||
|
"passwordResetsAuthenticator": "Am {date} werden deine Authenticator-Apps entfernt: Jemand hat es mit deinem Passwort angefordert. Warst du das nicht, sag es ab und ändere dein Passwort.",
|
||||||
|
"passwordResetsAccount": "Am {date} bekommt dein Konto ein neues Passwort und neue Schlüssel, und was mit den alten verschlüsselt ist, geht verloren: Jemand hat es mit einem Code per Mail angefordert. Warst du das nicht, sag es ab.",
|
||||||
|
"passwordResetsCallOff": "Absagen",
|
||||||
|
"passwordCallOffTitle": "Zurücksetzen absagen",
|
||||||
|
"passwordCallOffIntro": "Dein Passwort und ein Code sagen jedes anstehende Zurücksetzen ab."
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -479,5 +479,25 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"passwordWarningDismiss": "Dismiss",
|
"passwordWarningDismiss": "Dismiss",
|
||||||
"passwordWarningLater": "Remind me later"
|
"passwordWarningLater": "Remind me later",
|
||||||
|
"passwordResetsTitle": "A reset is waiting",
|
||||||
|
"passwordResetsAuthenticator": "Your authenticators will be removed on {date}: someone asked with your password. If that wasn't you, call it off and change your password.",
|
||||||
|
"@passwordResetsAuthenticator": {
|
||||||
|
"placeholders": {
|
||||||
|
"date": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"passwordResetsAccount": "Your account will get a new password and new keys on {date}, and what is encrypted under the old ones will be lost: someone asked with a code by mail. If that wasn't you, call it off.",
|
||||||
|
"@passwordResetsAccount": {
|
||||||
|
"placeholders": {
|
||||||
|
"date": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"passwordResetsCallOff": "Call it off",
|
||||||
|
"passwordCallOffTitle": "Call off the reset",
|
||||||
|
"passwordCallOffIntro": "Your password and a code call off every reset waiting."
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1458,6 +1458,42 @@ abstract class HalkyonAppLocalizations {
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Remind me later'**
|
/// **'Remind me later'**
|
||||||
String get passwordWarningLater;
|
String get passwordWarningLater;
|
||||||
|
|
||||||
|
/// No description provided for @passwordResetsTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'A reset is waiting'**
|
||||||
|
String get passwordResetsTitle;
|
||||||
|
|
||||||
|
/// No description provided for @passwordResetsAuthenticator.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Your authenticators will be removed on {date}: someone asked with your password. If that wasn\'t you, call it off and change your password.'**
|
||||||
|
String passwordResetsAuthenticator(String date);
|
||||||
|
|
||||||
|
/// No description provided for @passwordResetsAccount.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Your account will get a new password and new keys on {date}, and what is encrypted under the old ones will be lost: someone asked with a code by mail. If that wasn\'t you, call it off.'**
|
||||||
|
String passwordResetsAccount(String date);
|
||||||
|
|
||||||
|
/// No description provided for @passwordResetsCallOff.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Call it off'**
|
||||||
|
String get passwordResetsCallOff;
|
||||||
|
|
||||||
|
/// No description provided for @passwordCallOffTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Call off the reset'**
|
||||||
|
String get passwordCallOffTitle;
|
||||||
|
|
||||||
|
/// No description provided for @passwordCallOffIntro.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Your password and a code call off every reset waiting.'**
|
||||||
|
String get passwordCallOffIntro;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _HalkyonAppLocalizationsDelegate
|
class _HalkyonAppLocalizationsDelegate
|
||||||
|
|
|
||||||
|
|
@ -846,4 +846,27 @@ class HalkyonAppLocalizationsDe extends HalkyonAppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get passwordWarningLater => 'Später erinnern';
|
String get passwordWarningLater => 'Später erinnern';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get passwordResetsTitle => 'Ein Zurücksetzen steht an';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String passwordResetsAuthenticator(String date) {
|
||||||
|
return 'Am $date werden deine Authenticator-Apps entfernt: Jemand hat es mit deinem Passwort angefordert. Warst du das nicht, sag es ab und ändere dein Passwort.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String passwordResetsAccount(String date) {
|
||||||
|
return 'Am $date bekommt dein Konto ein neues Passwort und neue Schlüssel, und was mit den alten verschlüsselt ist, geht verloren: Jemand hat es mit einem Code per Mail angefordert. Warst du das nicht, sag es ab.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get passwordResetsCallOff => 'Absagen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get passwordCallOffTitle => 'Zurücksetzen absagen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get passwordCallOffIntro =>
|
||||||
|
'Dein Passwort und ein Code sagen jedes anstehende Zurücksetzen ab.';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -829,6 +829,29 @@ class HalkyonAppLocalizationsEn extends HalkyonAppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get passwordWarningLater => 'Remind me later';
|
String get passwordWarningLater => 'Remind me later';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get passwordResetsTitle => 'A reset is waiting';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String passwordResetsAuthenticator(String date) {
|
||||||
|
return 'Your authenticators will be removed on $date: someone asked with your password. If that wasn\'t you, call it off and change your password.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String passwordResetsAccount(String date) {
|
||||||
|
return 'Your account will get a new password and new keys on $date, and what is encrypted under the old ones will be lost: someone asked with a code by mail. If that wasn\'t you, call it off.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get passwordResetsCallOff => 'Call it off';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get passwordCallOffTitle => 'Call off the reset';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get passwordCallOffIntro =>
|
||||||
|
'Your password and a code call off every reset waiting.';
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The translations for English, as used in the United Kingdom (`en_GB`).
|
/// The translations for English, as used in the United Kingdom (`en_GB`).
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,11 @@ typedef PasswordWays = ({
|
||||||
/// When a session of the account was last ended for ten wrong proofs in
|
/// When a session of the account was last ended for ten wrong proofs in
|
||||||
/// a row, until the warning is dismissed; null for none.
|
/// a row, until the warning is dismissed; null for none.
|
||||||
DateTime? sessionEndedForProofsAt,
|
DateTime? sessionEndedForProofsAt,
|
||||||
|
|
||||||
|
/// When a reset of the authenticators, and of the account, waiting takes
|
||||||
|
/// effect; null for none (ADR 0017, the full reading's H1).
|
||||||
|
DateTime? authenticatorResetAt,
|
||||||
|
DateTime? accountResetAt,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// What a signed-in owner proves to change how their account is secured
|
/// What a signed-in owner proves to change how their account is secured
|
||||||
|
|
@ -121,6 +126,10 @@ abstract interface class PasswordGateway() {
|
||||||
/// New recovery codes for the [owner]'s proof.
|
/// New recovery codes for the [owner]'s proof.
|
||||||
Future<List<String>> newRecoveryCodes(OwnerAnswer owner);
|
Future<List<String>> newRecoveryCodes(OwnerAnswer owner);
|
||||||
|
|
||||||
|
/// Calls off every reset of the signed-in person's account waiting, for
|
||||||
|
/// the [owner]'s proof.
|
||||||
|
Future<void> callOffResets(OwnerAnswer owner);
|
||||||
|
|
||||||
/// How the signed-in person signs in.
|
/// How the signed-in person signs in.
|
||||||
Future<PasswordWays> ways();
|
Future<PasswordWays> ways();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -330,6 +330,17 @@ class PasswordRepository(
|
||||||
await _ownerAnswer(password, totpCode: code),
|
await _ownerAnswer(password, totpCode: code),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// Calls off every reset of the signed-in person's account waiting, for
|
||||||
|
/// their [password] and a [code] (the full reading's H1: the section shows
|
||||||
|
/// them, as no wait holds back a signed-in owner's proof).
|
||||||
|
Future<void> callOffResets({
|
||||||
|
required String password,
|
||||||
|
required String code,
|
||||||
|
}) async {
|
||||||
|
await _gateway.callOffResets(await _ownerAnswer(password, totpCode: code));
|
||||||
|
_changed();
|
||||||
|
}
|
||||||
|
|
||||||
/// The owner's proof (`OwnerProof`): [password] (or [unlocked], unlocked
|
/// The owner's proof (`OwnerProof`): [password] (or [unlocked], unlocked
|
||||||
/// at a sign-in already) answering a change's challenge, of the signed-in
|
/// at a sign-in already) answering a change's challenge, of the signed-in
|
||||||
/// session or [as], with a [totpCode] or [recoveryCode] where given.
|
/// session or [as], with a [totpCode] or [recoveryCode] where given.
|
||||||
|
|
@ -377,6 +388,8 @@ class PasswordRepository(
|
||||||
hasPassword: ways.password,
|
hasPassword: ways.password,
|
||||||
secondFactorWorks: ways.secondFactorWorks,
|
secondFactorWorks: ways.secondFactorWorks,
|
||||||
sessionEndedAt: ways.sessionEndedForProofsAt,
|
sessionEndedAt: ways.sessionEndedForProofsAt,
|
||||||
|
authenticatorResetAt: ways.authenticatorResetAt,
|
||||||
|
accountResetAt: ways.accountResetAt,
|
||||||
authenticators: ways.password
|
authenticators: ways.password
|
||||||
? await _gateway.authenticators()
|
? await _gateway.authenticators()
|
||||||
: const <AccountAuthenticator>[],
|
: const <AccountAuthenticator>[],
|
||||||
|
|
|
||||||
|
|
@ -182,6 +182,11 @@ class ServerpodPasswordGateway(
|
||||||
() => _base.authenticator.newRecoveryCodes(proof: _proofOf(owner)!),
|
() => _base.authenticator.newRecoveryCodes(proof: _proofOf(owner)!),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> callOffResets(OwnerAnswer owner) => _failing(
|
||||||
|
() => _base.password.callOffResets(proof: _proofOf(owner)!),
|
||||||
|
);
|
||||||
|
|
||||||
/// [owner] as the instance takes it, bound to the [host] addressed.
|
/// [owner] as the instance takes it, bound to the [host] addressed.
|
||||||
protocol.OwnerProof? _proofOf(OwnerAnswer? owner) => switch (owner) {
|
protocol.OwnerProof? _proofOf(OwnerAnswer? owner) => switch (owner) {
|
||||||
null => null,
|
null => null,
|
||||||
|
|
@ -206,6 +211,8 @@ class ServerpodPasswordGateway(
|
||||||
authenticator: ways.authenticator,
|
authenticator: ways.authenticator,
|
||||||
secondFactorWorks: ways.secondFactorWorks,
|
secondFactorWorks: ways.secondFactorWorks,
|
||||||
sessionEndedForProofsAt: ways.sessionEndedForProofsAt,
|
sessionEndedForProofsAt: ways.sessionEndedForProofsAt,
|
||||||
|
authenticatorResetAt: ways.authenticatorResetAt,
|
||||||
|
accountResetAt: ways.accountResetAt,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,12 @@ final class const AccountSecurity({
|
||||||
/// owner's warning until dismissed or a new password; null for none.
|
/// owner's warning until dismissed or a new password; null for none.
|
||||||
final DateTime? sessionEndedAt,
|
final DateTime? sessionEndedAt,
|
||||||
|
|
||||||
|
/// When a reset of the authenticators, and of the account, waiting takes
|
||||||
|
/// effect (the full reading's H1): the section shows them, with a way to
|
||||||
|
/// call them off; null for none.
|
||||||
|
final DateTime? authenticatorResetAt,
|
||||||
|
final DateTime? accountResetAt,
|
||||||
|
|
||||||
/// Their authenticators, the oldest first; empty without a password.
|
/// Their authenticators, the oldest first; empty without a password.
|
||||||
final List<AccountAuthenticator> authenticators = const [],
|
final List<AccountAuthenticator> authenticators = const [],
|
||||||
|
|
||||||
|
|
@ -48,6 +54,8 @@ final class const AccountSecurity({
|
||||||
hasPassword,
|
hasPassword,
|
||||||
secondFactorWorks,
|
secondFactorWorks,
|
||||||
sessionEndedAt,
|
sessionEndedAt,
|
||||||
|
authenticatorResetAt,
|
||||||
|
accountResetAt,
|
||||||
authenticators,
|
authenticators,
|
||||||
keysHere,
|
keysHere,
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,14 @@ class const _AccountSecurity() extends StatelessWidget {
|
||||||
Text(l10n.passwordSectionTitle, style: theme.textTheme.titleSmall),
|
Text(l10n.passwordSectionTitle, style: theme.textTheme.titleSmall),
|
||||||
if (security.sessionEndedAt case final ended?)
|
if (security.sessionEndedAt case final ended?)
|
||||||
_SessionEndedWarning(ended),
|
_SessionEndedWarning(ended),
|
||||||
|
if (security.authenticatorResetAt != null ||
|
||||||
|
security.accountResetAt != null)
|
||||||
|
_ResetsWaiting(
|
||||||
|
authenticatorAt: security.authenticatorResetAt,
|
||||||
|
accountAt: security.accountResetAt,
|
||||||
|
onCallOff: () =>
|
||||||
|
_show(context, (context) => const _CallOffResets()),
|
||||||
|
),
|
||||||
if (security.gitHub && security.gitHubEnds != null)
|
if (security.gitHub && security.gitHubEnds != null)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 8),
|
padding: const EdgeInsets.only(top: 8),
|
||||||
|
|
@ -605,6 +613,151 @@ class _AddAuthenticatorState() extends State<_AddAuthenticator> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The resets of the account waiting, each with when it takes effect, and
|
||||||
|
/// a way to call them off (the full reading's H1: an owner held out of
|
||||||
|
/// signing in by codes made to wait still calls them off here).
|
||||||
|
class const _ResetsWaiting({
|
||||||
|
required final DateTime? authenticatorAt,
|
||||||
|
required final DateTime? accountAt,
|
||||||
|
required final VoidCallback onCallOff,
|
||||||
|
}) extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = HalkyonAppLocalizations.of(context);
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final colors = theme.colorScheme;
|
||||||
|
final body = theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: colors.onErrorContainer,
|
||||||
|
);
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8),
|
||||||
|
child: DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colors.errorContainer,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 12, 8, 4),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(LucideIcons.shieldAlert, color: colors.onErrorContainer),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
l10n.passwordResetsTitle,
|
||||||
|
style: theme.textTheme.titleSmall?.copyWith(
|
||||||
|
color: colors.onErrorContainer,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (authenticatorAt case final at?)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8, right: 8),
|
||||||
|
child: Text(
|
||||||
|
l10n.passwordResetsAuthenticator(
|
||||||
|
passwordMoment(context, at),
|
||||||
|
),
|
||||||
|
style: body,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (accountAt case final at?)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8, right: 8),
|
||||||
|
child: Text(
|
||||||
|
l10n.passwordResetsAccount(passwordMoment(context, at)),
|
||||||
|
style: body,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Align(
|
||||||
|
alignment: AlignmentDirectional.centerEnd,
|
||||||
|
child: TextButton(
|
||||||
|
onPressed: onCallOff,
|
||||||
|
child: Text(l10n.passwordResetsCallOff),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calls off every reset waiting, for the password and a code.
|
||||||
|
class const _CallOffResets() extends StatefulWidget {
|
||||||
|
@override
|
||||||
|
State<_CallOffResets> createState() => _CallOffResetsState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CallOffResetsState() extends State<_CallOffResets> {
|
||||||
|
final _password = TextEditingController();
|
||||||
|
final _code = TextEditingController();
|
||||||
|
late final _step = SecurityStepCubit<bool>();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_password.dispose();
|
||||||
|
_code.dispose();
|
||||||
|
_step.close();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _submit() async {
|
||||||
|
final passwords = _passwordsOf(context);
|
||||||
|
final navigator = Navigator.of(context);
|
||||||
|
if (await _step.run(() async {
|
||||||
|
await passwords.callOffResets(
|
||||||
|
password: _password.text,
|
||||||
|
code: _code.text,
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
})) {
|
||||||
|
navigator.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = HalkyonAppLocalizations.of(context);
|
||||||
|
return BlocBuilder<SecurityStepCubit<bool>, SecurityStepState<bool>>(
|
||||||
|
bloc: _step,
|
||||||
|
builder: (context, state) => SheetScaffold(
|
||||||
|
title: l10n.passwordCallOffTitle,
|
||||||
|
busy: state.busy,
|
||||||
|
body: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
SheetIntro(l10n.passwordCallOffIntro),
|
||||||
|
PasswordField(
|
||||||
|
controller: _password,
|
||||||
|
label: l10n.passwordCurrent,
|
||||||
|
autofocus: true,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
CodeField(
|
||||||
|
controller: _code,
|
||||||
|
kind: CodeKind.totp,
|
||||||
|
errorText: _problemOf(context, state),
|
||||||
|
onSubmitted: (_) => _submit(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
footer: FilledButton(
|
||||||
|
onPressed: state.busy ? null : _submit,
|
||||||
|
child: state.busy
|
||||||
|
? const ButtonLoadingIndicator()
|
||||||
|
: Text(l10n.passwordResetsCallOff),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// New recovery codes for the password and a code: shown once.
|
/// New recovery codes for the password and a code: shown once.
|
||||||
class const _NewRecoveryCodes() extends StatefulWidget {
|
class const _NewRecoveryCodes() extends StatefulWidget {
|
||||||
@override
|
@override
|
||||||
|
|
|
||||||
|
|
@ -559,6 +559,10 @@ class FakePasswordGateway(final AuthGateway _auth) implements PasswordGateway {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> callOffResets(OwnerAnswer owner) =>
|
||||||
|
_call('callOffResets', () => _ownerProved(owner));
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<PasswordWays> ways() => _call('ways', () {
|
Future<PasswordWays> ways() => _call('ways', () {
|
||||||
final person = _signedIn();
|
final person = _signedIn();
|
||||||
|
|
@ -572,6 +576,11 @@ class FakePasswordGateway(final AuthGateway _auth) implements PasswordGateway {
|
||||||
(account?.finished ?? false) &&
|
(account?.finished ?? false) &&
|
||||||
(account?.authenticatorReset?.isAfter(clock.now()) ?? true),
|
(account?.authenticatorReset?.isAfter(clock.now()) ?? true),
|
||||||
sessionEndedForProofsAt: account?.sessionEndedForProofsAt,
|
sessionEndedForProofsAt: account?.sessionEndedForProofsAt,
|
||||||
|
// Not carried out: one of its authenticators still confirmed.
|
||||||
|
authenticatorResetAt: (account?.finished ?? false)
|
||||||
|
? account?.authenticatorReset
|
||||||
|
: null,
|
||||||
|
accountResetAt: account?.accountReset,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,36 @@ void main() {
|
||||||
expect(anna.sessionEndedForProofsAt, isNull);
|
expect(anna.sessionEndedForProofsAt, isNull);
|
||||||
expect(cubit.state.security?.sessionEndedAt, isNull);
|
expect(cubit.state.security?.sessionEndedAt, isNull);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('carries the resets waiting, each with when it takes effect, '
|
||||||
|
'until called off with the password and a code', () async {
|
||||||
|
anna
|
||||||
|
..authenticatorReset = DateTime.utc(2026, 10, 24, 12)
|
||||||
|
..accountReset = DateTime.utc(2026, 10, 25, 12);
|
||||||
|
final cubit = AccountSecurityCubit(passwords);
|
||||||
|
addTearDown(cubit.close);
|
||||||
|
await cubit.load();
|
||||||
|
expect(
|
||||||
|
cubit.state.security?.authenticatorResetAt,
|
||||||
|
DateTime.utc(2026, 10, 24, 12),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
cubit.state.security?.accountResetAt,
|
||||||
|
DateTime.utc(2026, 10, 25, 12),
|
||||||
|
);
|
||||||
|
|
||||||
|
await expectLater(
|
||||||
|
passwords.callOffResets(password: 'correct horse', code: '000000'),
|
||||||
|
throwsA(isA<PasswordFailed>()),
|
||||||
|
);
|
||||||
|
await passwords.callOffResets(
|
||||||
|
password: 'correct horse',
|
||||||
|
code: fakeTotpCode,
|
||||||
|
);
|
||||||
|
await pumpEventQueue();
|
||||||
|
expect(cubit.state.security?.authenticatorResetAt, isNull);
|
||||||
|
expect(cubit.state.security?.accountResetAt, isNull);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
group('a step of the account section', () {
|
group('a step of the account section', () {
|
||||||
|
|
|
||||||
|
|
@ -689,6 +689,48 @@ void main() {
|
||||||
return anna;
|
return anna;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
testWidgets('shows the resets waiting, called off only with the '
|
||||||
|
'password and a code', (tester) async {
|
||||||
|
final anna = await annaSignedIn();
|
||||||
|
anna
|
||||||
|
..authenticatorReset = DateTime.utc(2026, 10, 24, 12)
|
||||||
|
..accountReset = DateTime.utc(2026, 10, 25, 12);
|
||||||
|
await show(tester);
|
||||||
|
|
||||||
|
expect(find.text('A reset is waiting'), findsOneWidget);
|
||||||
|
expect(
|
||||||
|
find.textContaining('Your authenticators will be removed on'),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
find.textContaining('Your account will get a new password'),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.tap(find.widgetWithText(TextButton, 'Call it off'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.enterText(field('Current password'), 'correct horse');
|
||||||
|
await tester.enterText(
|
||||||
|
field('Code from your authenticator app'),
|
||||||
|
'000000',
|
||||||
|
);
|
||||||
|
await tapButton(tester, 'Call it off');
|
||||||
|
expect(
|
||||||
|
find.text("The address, the password or the code isn't right."),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
expect(anna.accountReset, isNotNull);
|
||||||
|
|
||||||
|
await tester.enterText(
|
||||||
|
field('Code from your authenticator app'),
|
||||||
|
fakeTotpCode,
|
||||||
|
);
|
||||||
|
await tapButton(tester, 'Call it off');
|
||||||
|
expect(find.text('A reset is waiting'), findsNothing);
|
||||||
|
expect(anna.authenticatorReset, isNull);
|
||||||
|
expect(anna.accountReset, isNull);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('removes an authenticator only with the password and a code, '
|
testWidgets('removes an authenticator only with the password and a code, '
|
||||||
'and never the last', (tester) async {
|
'and never the last', (tester) async {
|
||||||
final anna = await annaSignedIn();
|
final anna = await annaSignedIn();
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@ abstract class SignInWays
|
||||||
bool? passwordOnly,
|
bool? passwordOnly,
|
||||||
bool? secondFactorWorks,
|
bool? secondFactorWorks,
|
||||||
this.sessionEndedForProofsAt,
|
this.sessionEndedForProofsAt,
|
||||||
|
this.authenticatorResetAt,
|
||||||
|
this.accountResetAt,
|
||||||
}) : password = password ?? false,
|
}) : password = password ?? false,
|
||||||
authenticator = authenticator ?? false,
|
authenticator = authenticator ?? false,
|
||||||
passwordOnly = passwordOnly ?? false,
|
passwordOnly = passwordOnly ?? false,
|
||||||
|
|
@ -36,6 +38,8 @@ abstract class SignInWays
|
||||||
bool? passwordOnly,
|
bool? passwordOnly,
|
||||||
bool? secondFactorWorks,
|
bool? secondFactorWorks,
|
||||||
DateTime? sessionEndedForProofsAt,
|
DateTime? sessionEndedForProofsAt,
|
||||||
|
DateTime? authenticatorResetAt,
|
||||||
|
DateTime? accountResetAt,
|
||||||
}) = _SignInWaysImpl;
|
}) = _SignInWaysImpl;
|
||||||
|
|
||||||
factory SignInWays.fromJson(Map<String, dynamic> jsonSerialization) {
|
factory SignInWays.fromJson(Map<String, dynamic> jsonSerialization) {
|
||||||
|
|
@ -62,6 +66,16 @@ abstract class SignInWays
|
||||||
: _isc.DateTimeJsonExtension.fromJson(
|
: _isc.DateTimeJsonExtension.fromJson(
|
||||||
jsonSerialization['sessionEndedForProofsAt'],
|
jsonSerialization['sessionEndedForProofsAt'],
|
||||||
),
|
),
|
||||||
|
authenticatorResetAt: jsonSerialization['authenticatorResetAt'] == null
|
||||||
|
? null
|
||||||
|
: _isc.DateTimeJsonExtension.fromJson(
|
||||||
|
jsonSerialization['authenticatorResetAt'],
|
||||||
|
),
|
||||||
|
accountResetAt: jsonSerialization['accountResetAt'] == null
|
||||||
|
? null
|
||||||
|
: _isc.DateTimeJsonExtension.fromJson(
|
||||||
|
jsonSerialization['accountResetAt'],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -90,6 +104,14 @@ abstract class SignInWays
|
||||||
/// password (ADR 0017, the fourth reading); null for none.
|
/// password (ADR 0017, the fourth reading); null for none.
|
||||||
DateTime? sessionEndedForProofsAt;
|
DateTime? sessionEndedForProofsAt;
|
||||||
|
|
||||||
|
/// When a reset of the authenticators waiting (or due, not carried out)
|
||||||
|
/// takes effect, so a signed-in device shows it and its owner calls it
|
||||||
|
/// off with their proof (ADR 0017, the full reading's H1); null for none.
|
||||||
|
DateTime? authenticatorResetAt;
|
||||||
|
|
||||||
|
/// When a reset of the account waiting (or due) takes effect, alike.
|
||||||
|
DateTime? accountResetAt;
|
||||||
|
|
||||||
/// Returns a shallow copy of this [SignInWays]
|
/// Returns a shallow copy of this [SignInWays]
|
||||||
/// with some or all fields replaced by the given arguments.
|
/// with some or all fields replaced by the given arguments.
|
||||||
@_isc.useResult
|
@_isc.useResult
|
||||||
|
|
@ -101,6 +123,8 @@ abstract class SignInWays
|
||||||
bool? passwordOnly,
|
bool? passwordOnly,
|
||||||
bool? secondFactorWorks,
|
bool? secondFactorWorks,
|
||||||
DateTime? sessionEndedForProofsAt,
|
DateTime? sessionEndedForProofsAt,
|
||||||
|
DateTime? authenticatorResetAt,
|
||||||
|
DateTime? accountResetAt,
|
||||||
});
|
});
|
||||||
@override
|
@override
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
|
|
@ -114,6 +138,9 @@ abstract class SignInWays
|
||||||
'secondFactorWorks': secondFactorWorks,
|
'secondFactorWorks': secondFactorWorks,
|
||||||
if (sessionEndedForProofsAt != null)
|
if (sessionEndedForProofsAt != null)
|
||||||
'sessionEndedForProofsAt': sessionEndedForProofsAt?.toJson(),
|
'sessionEndedForProofsAt': sessionEndedForProofsAt?.toJson(),
|
||||||
|
if (authenticatorResetAt != null)
|
||||||
|
'authenticatorResetAt': authenticatorResetAt?.toJson(),
|
||||||
|
if (accountResetAt != null) 'accountResetAt': accountResetAt?.toJson(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -129,6 +156,9 @@ abstract class SignInWays
|
||||||
'secondFactorWorks': secondFactorWorks,
|
'secondFactorWorks': secondFactorWorks,
|
||||||
if (sessionEndedForProofsAt != null)
|
if (sessionEndedForProofsAt != null)
|
||||||
'sessionEndedForProofsAt': sessionEndedForProofsAt?.toJson(),
|
'sessionEndedForProofsAt': sessionEndedForProofsAt?.toJson(),
|
||||||
|
if (authenticatorResetAt != null)
|
||||||
|
'authenticatorResetAt': authenticatorResetAt?.toJson(),
|
||||||
|
if (accountResetAt != null) 'accountResetAt': accountResetAt?.toJson(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -149,6 +179,8 @@ class _SignInWaysImpl extends SignInWays {
|
||||||
bool? passwordOnly,
|
bool? passwordOnly,
|
||||||
bool? secondFactorWorks,
|
bool? secondFactorWorks,
|
||||||
DateTime? sessionEndedForProofsAt,
|
DateTime? sessionEndedForProofsAt,
|
||||||
|
DateTime? authenticatorResetAt,
|
||||||
|
DateTime? accountResetAt,
|
||||||
}) : super._(
|
}) : super._(
|
||||||
gitHub: gitHub,
|
gitHub: gitHub,
|
||||||
email: email,
|
email: email,
|
||||||
|
|
@ -157,6 +189,8 @@ class _SignInWaysImpl extends SignInWays {
|
||||||
passwordOnly: passwordOnly,
|
passwordOnly: passwordOnly,
|
||||||
secondFactorWorks: secondFactorWorks,
|
secondFactorWorks: secondFactorWorks,
|
||||||
sessionEndedForProofsAt: sessionEndedForProofsAt,
|
sessionEndedForProofsAt: sessionEndedForProofsAt,
|
||||||
|
authenticatorResetAt: authenticatorResetAt,
|
||||||
|
accountResetAt: accountResetAt,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Returns a shallow copy of this [SignInWays]
|
/// Returns a shallow copy of this [SignInWays]
|
||||||
|
|
@ -171,6 +205,8 @@ class _SignInWaysImpl extends SignInWays {
|
||||||
bool? passwordOnly,
|
bool? passwordOnly,
|
||||||
bool? secondFactorWorks,
|
bool? secondFactorWorks,
|
||||||
Object? sessionEndedForProofsAt = _Undefined,
|
Object? sessionEndedForProofsAt = _Undefined,
|
||||||
|
Object? authenticatorResetAt = _Undefined,
|
||||||
|
Object? accountResetAt = _Undefined,
|
||||||
}) {
|
}) {
|
||||||
return SignInWays(
|
return SignInWays(
|
||||||
gitHub: gitHub ?? this.gitHub,
|
gitHub: gitHub ?? this.gitHub,
|
||||||
|
|
@ -182,6 +218,12 @@ class _SignInWaysImpl extends SignInWays {
|
||||||
sessionEndedForProofsAt: sessionEndedForProofsAt is DateTime?
|
sessionEndedForProofsAt: sessionEndedForProofsAt is DateTime?
|
||||||
? sessionEndedForProofsAt
|
? sessionEndedForProofsAt
|
||||||
: this.sessionEndedForProofsAt,
|
: this.sessionEndedForProofsAt,
|
||||||
|
authenticatorResetAt: authenticatorResetAt is DateTime?
|
||||||
|
? authenticatorResetAt
|
||||||
|
: this.authenticatorResetAt,
|
||||||
|
accountResetAt: accountResetAt is DateTime?
|
||||||
|
? accountResetAt
|
||||||
|
: this.accountResetAt,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -489,6 +489,18 @@ class EndpointPassword extends _isc.EndpointRef {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// Calls off every reset of the caller's account waiting, with the
|
||||||
|
/// owner's [proof] (ADR 0017, the full reading's H1: a signed-in device
|
||||||
|
/// shows the resets, `SignInWays`), even while the account's codes wait;
|
||||||
|
/// counted against this session as every owner's proof is. Refusals as
|
||||||
|
/// `callOffResets`' ([PasswordRefusedException]).
|
||||||
|
_ida.Future<void> callOffResets({required _ikq9b2yz.OwnerProof proof}) =>
|
||||||
|
caller.callServerEndpoint<void>(
|
||||||
|
'halkyon_base.password',
|
||||||
|
'callOffResets',
|
||||||
|
{'proof': proof},
|
||||||
|
);
|
||||||
|
|
||||||
/// A new recovery phrase: [recoveryKeys], the caller's keys wrapped under
|
/// A new recovery phrase: [recoveryKeys], the caller's keys wrapped under
|
||||||
/// it on the device, replace the old wrapping, and [recoveryProofKey], the
|
/// it on the device, replace the old wrapping, and [recoveryProofKey], the
|
||||||
/// public half of its proof key, the old one, with the owner's [proof], as
|
/// public half of its proof key, the old one, with the owner's [proof], as
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ export 'src/halkyon_base.dart';
|
||||||
export 'src/instance/data_location.dart';
|
export 'src/instance/data_location.dart';
|
||||||
export 'src/instance/source_code.dart';
|
export 'src/instance/source_code.dart';
|
||||||
export 'src/password/account_resets.dart'
|
export 'src/password/account_resets.dart'
|
||||||
show forgetLapsedResets, resetLapsesAfter;
|
show forgetLapsedResets, resetDelay, resetLapsesAfter;
|
||||||
export 'src/password/authenticators.dart'
|
export 'src/password/authenticators.dart'
|
||||||
show
|
show
|
||||||
forgetUnconfirmedAuthenticators,
|
forgetUnconfirmedAuthenticators,
|
||||||
|
|
@ -36,7 +36,6 @@ export 'src/password/passwords.dart'
|
||||||
newPasswordCosts,
|
newPasswordCosts,
|
||||||
passwordGroup,
|
passwordGroup,
|
||||||
passwordMethod,
|
passwordMethod,
|
||||||
resetDelay,
|
|
||||||
signInChallengeLifetime,
|
signInChallengeLifetime,
|
||||||
signsInWithPassword,
|
signsInWithPassword,
|
||||||
wrappedKeysLength;
|
wrappedKeysLength;
|
||||||
|
|
|
||||||
|
|
@ -19,3 +19,9 @@ fields:
|
||||||
### a row, the owner's warning until they dismiss it or set a new
|
### a row, the owner's warning until they dismiss it or set a new
|
||||||
### password (ADR 0017, the fourth reading); null for none.
|
### password (ADR 0017, the fourth reading); null for none.
|
||||||
sessionEndedForProofsAt: DateTime?
|
sessionEndedForProofsAt: DateTime?
|
||||||
|
### When a reset of the authenticators waiting (or due, not carried out)
|
||||||
|
### takes effect, so a signed-in device shows it and its owner calls it
|
||||||
|
### off with their proof (ADR 0017, the full reading's H1); null for none.
|
||||||
|
authenticatorResetAt: DateTime?
|
||||||
|
### When a reset of the account waiting (or due) takes effect, alike.
|
||||||
|
accountResetAt: DateTime?
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@ abstract class SignInWays
|
||||||
bool? passwordOnly,
|
bool? passwordOnly,
|
||||||
bool? secondFactorWorks,
|
bool? secondFactorWorks,
|
||||||
this.sessionEndedForProofsAt,
|
this.sessionEndedForProofsAt,
|
||||||
|
this.authenticatorResetAt,
|
||||||
|
this.accountResetAt,
|
||||||
}) : password = password ?? false,
|
}) : password = password ?? false,
|
||||||
authenticator = authenticator ?? false,
|
authenticator = authenticator ?? false,
|
||||||
passwordOnly = passwordOnly ?? false,
|
passwordOnly = passwordOnly ?? false,
|
||||||
|
|
@ -36,6 +38,8 @@ abstract class SignInWays
|
||||||
bool? passwordOnly,
|
bool? passwordOnly,
|
||||||
bool? secondFactorWorks,
|
bool? secondFactorWorks,
|
||||||
DateTime? sessionEndedForProofsAt,
|
DateTime? sessionEndedForProofsAt,
|
||||||
|
DateTime? authenticatorResetAt,
|
||||||
|
DateTime? accountResetAt,
|
||||||
}) = _SignInWaysImpl;
|
}) = _SignInWaysImpl;
|
||||||
|
|
||||||
factory SignInWays.fromJson(Map<String, dynamic> jsonSerialization) {
|
factory SignInWays.fromJson(Map<String, dynamic> jsonSerialization) {
|
||||||
|
|
@ -62,6 +66,16 @@ abstract class SignInWays
|
||||||
: _is.DateTimeJsonExtension.fromJson(
|
: _is.DateTimeJsonExtension.fromJson(
|
||||||
jsonSerialization['sessionEndedForProofsAt'],
|
jsonSerialization['sessionEndedForProofsAt'],
|
||||||
),
|
),
|
||||||
|
authenticatorResetAt: jsonSerialization['authenticatorResetAt'] == null
|
||||||
|
? null
|
||||||
|
: _is.DateTimeJsonExtension.fromJson(
|
||||||
|
jsonSerialization['authenticatorResetAt'],
|
||||||
|
),
|
||||||
|
accountResetAt: jsonSerialization['accountResetAt'] == null
|
||||||
|
? null
|
||||||
|
: _is.DateTimeJsonExtension.fromJson(
|
||||||
|
jsonSerialization['accountResetAt'],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -90,6 +104,14 @@ abstract class SignInWays
|
||||||
/// password (ADR 0017, the fourth reading); null for none.
|
/// password (ADR 0017, the fourth reading); null for none.
|
||||||
DateTime? sessionEndedForProofsAt;
|
DateTime? sessionEndedForProofsAt;
|
||||||
|
|
||||||
|
/// When a reset of the authenticators waiting (or due, not carried out)
|
||||||
|
/// takes effect, so a signed-in device shows it and its owner calls it
|
||||||
|
/// off with their proof (ADR 0017, the full reading's H1); null for none.
|
||||||
|
DateTime? authenticatorResetAt;
|
||||||
|
|
||||||
|
/// When a reset of the account waiting (or due) takes effect, alike.
|
||||||
|
DateTime? accountResetAt;
|
||||||
|
|
||||||
/// Returns a shallow copy of this [SignInWays]
|
/// Returns a shallow copy of this [SignInWays]
|
||||||
/// with some or all fields replaced by the given arguments.
|
/// with some or all fields replaced by the given arguments.
|
||||||
@_is.useResult
|
@_is.useResult
|
||||||
|
|
@ -101,6 +123,8 @@ abstract class SignInWays
|
||||||
bool? passwordOnly,
|
bool? passwordOnly,
|
||||||
bool? secondFactorWorks,
|
bool? secondFactorWorks,
|
||||||
DateTime? sessionEndedForProofsAt,
|
DateTime? sessionEndedForProofsAt,
|
||||||
|
DateTime? authenticatorResetAt,
|
||||||
|
DateTime? accountResetAt,
|
||||||
});
|
});
|
||||||
@override
|
@override
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
|
|
@ -114,6 +138,9 @@ abstract class SignInWays
|
||||||
'secondFactorWorks': secondFactorWorks,
|
'secondFactorWorks': secondFactorWorks,
|
||||||
if (sessionEndedForProofsAt != null)
|
if (sessionEndedForProofsAt != null)
|
||||||
'sessionEndedForProofsAt': sessionEndedForProofsAt?.toJson(),
|
'sessionEndedForProofsAt': sessionEndedForProofsAt?.toJson(),
|
||||||
|
if (authenticatorResetAt != null)
|
||||||
|
'authenticatorResetAt': authenticatorResetAt?.toJson(),
|
||||||
|
if (accountResetAt != null) 'accountResetAt': accountResetAt?.toJson(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -129,6 +156,9 @@ abstract class SignInWays
|
||||||
'secondFactorWorks': secondFactorWorks,
|
'secondFactorWorks': secondFactorWorks,
|
||||||
if (sessionEndedForProofsAt != null)
|
if (sessionEndedForProofsAt != null)
|
||||||
'sessionEndedForProofsAt': sessionEndedForProofsAt?.toJson(),
|
'sessionEndedForProofsAt': sessionEndedForProofsAt?.toJson(),
|
||||||
|
if (authenticatorResetAt != null)
|
||||||
|
'authenticatorResetAt': authenticatorResetAt?.toJson(),
|
||||||
|
if (accountResetAt != null) 'accountResetAt': accountResetAt?.toJson(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -149,6 +179,8 @@ class _SignInWaysImpl extends SignInWays {
|
||||||
bool? passwordOnly,
|
bool? passwordOnly,
|
||||||
bool? secondFactorWorks,
|
bool? secondFactorWorks,
|
||||||
DateTime? sessionEndedForProofsAt,
|
DateTime? sessionEndedForProofsAt,
|
||||||
|
DateTime? authenticatorResetAt,
|
||||||
|
DateTime? accountResetAt,
|
||||||
}) : super._(
|
}) : super._(
|
||||||
gitHub: gitHub,
|
gitHub: gitHub,
|
||||||
email: email,
|
email: email,
|
||||||
|
|
@ -157,6 +189,8 @@ class _SignInWaysImpl extends SignInWays {
|
||||||
passwordOnly: passwordOnly,
|
passwordOnly: passwordOnly,
|
||||||
secondFactorWorks: secondFactorWorks,
|
secondFactorWorks: secondFactorWorks,
|
||||||
sessionEndedForProofsAt: sessionEndedForProofsAt,
|
sessionEndedForProofsAt: sessionEndedForProofsAt,
|
||||||
|
authenticatorResetAt: authenticatorResetAt,
|
||||||
|
accountResetAt: accountResetAt,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Returns a shallow copy of this [SignInWays]
|
/// Returns a shallow copy of this [SignInWays]
|
||||||
|
|
@ -171,6 +205,8 @@ class _SignInWaysImpl extends SignInWays {
|
||||||
bool? passwordOnly,
|
bool? passwordOnly,
|
||||||
bool? secondFactorWorks,
|
bool? secondFactorWorks,
|
||||||
Object? sessionEndedForProofsAt = _Undefined,
|
Object? sessionEndedForProofsAt = _Undefined,
|
||||||
|
Object? authenticatorResetAt = _Undefined,
|
||||||
|
Object? accountResetAt = _Undefined,
|
||||||
}) {
|
}) {
|
||||||
return SignInWays(
|
return SignInWays(
|
||||||
gitHub: gitHub ?? this.gitHub,
|
gitHub: gitHub ?? this.gitHub,
|
||||||
|
|
@ -182,6 +218,12 @@ class _SignInWaysImpl extends SignInWays {
|
||||||
sessionEndedForProofsAt: sessionEndedForProofsAt is DateTime?
|
sessionEndedForProofsAt: sessionEndedForProofsAt is DateTime?
|
||||||
? sessionEndedForProofsAt
|
? sessionEndedForProofsAt
|
||||||
: this.sessionEndedForProofsAt,
|
: this.sessionEndedForProofsAt,
|
||||||
|
authenticatorResetAt: authenticatorResetAt is DateTime?
|
||||||
|
? authenticatorResetAt
|
||||||
|
: this.authenticatorResetAt,
|
||||||
|
accountResetAt: accountResetAt is DateTime?
|
||||||
|
? accountResetAt
|
||||||
|
: this.accountResetAt,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -747,6 +747,25 @@ class Endpoints extends _is.EndpointDispatch {
|
||||||
password: params['password'],
|
password: params['password'],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
'callOffResets': _is.MethodConnector(
|
||||||
|
name: 'callOffResets',
|
||||||
|
params: {
|
||||||
|
'proof': _is.ParameterDescription(
|
||||||
|
name: 'proof',
|
||||||
|
type: _is.getType<_ifhod73s.OwnerProof>(),
|
||||||
|
nullable: false,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
call:
|
||||||
|
(
|
||||||
|
_is.Session session,
|
||||||
|
Map<String, dynamic> params,
|
||||||
|
) async => (endpoints['password'] as _ig7fyg1b.PasswordEndpoint)
|
||||||
|
.callOffResets(
|
||||||
|
session,
|
||||||
|
proof: params['proof'],
|
||||||
|
),
|
||||||
|
),
|
||||||
'setRecoveryKeys': _is.MethodConnector(
|
'setRecoveryKeys': _is.MethodConnector(
|
||||||
name: 'setRecoveryKeys',
|
name: 'setRecoveryKeys',
|
||||||
params: {
|
params: {
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ password:
|
||||||
- set:
|
- set:
|
||||||
- beginChange:
|
- beginChange:
|
||||||
- change:
|
- change:
|
||||||
|
- callOffResets:
|
||||||
- setRecoveryKeys:
|
- setRecoveryKeys:
|
||||||
- dismissSessionEndedWarning:
|
- dismissSessionEndedWarning:
|
||||||
passwordSignIn:
|
passwordSignIn:
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue