fix(auth): only a session made since dismisses the owner's warning
The full reading's L1: the warning that ten wrong proofs ended a session is there for the owner to learn of it, and any other session of the account (another app on the same stolen phone) dismissed it. Now only a session made after the warning dismisses it (signed in again, or the sign-in that first shows it); an older one is refused (PasswordRefusal.signedInBefore, its own text in English and German), and a new password clears it as before. A member tells the register when its session asking was made. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
76291e6ab4
commit
003cb4821a
20 changed files with 165 additions and 24 deletions
|
|
@ -232,5 +232,6 @@
|
||||||
"passwordResetsCallOff": "Absagen",
|
"passwordResetsCallOff": "Absagen",
|
||||||
"passwordCallOffTitle": "Zurücksetzen absagen",
|
"passwordCallOffTitle": "Zurücksetzen absagen",
|
||||||
"passwordCallOffIntro": "Dein Passwort und ein Code sagen jedes anstehende Zurücksetzen ab.",
|
"passwordCallOffIntro": "Dein Passwort und ein Code sagen jedes anstehende Zurücksetzen ab.",
|
||||||
"passwordSectionGitHubEndsDeletes": "Die Anmeldung mit GitHub endet am {date}. Dann wird dein Konto mit allem darin gelöscht, es sei denn, du richtest vorher ein Passwort ein."
|
"passwordSectionGitHubEndsDeletes": "Die Anmeldung mit GitHub endet am {date}. Dann wird dein Konto mit allem darin gelöscht, es sei denn, du richtest vorher ein Passwort ein.",
|
||||||
|
"passwordSignedInBefore": "Nur ein seitdem angemeldetes Gerät kann diese Warnung ausblenden: Melde dich auf diesem neu an oder ändere dein Passwort."
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -507,5 +507,6 @@
|
||||||
"type": "String"
|
"type": "String"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"passwordSignedInBefore": "Only a device signed in since then can dismiss this warning: sign in again on this one, or change your password."
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1500,6 +1500,12 @@ abstract class HalkyonAppLocalizations {
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Sign-in with GitHub ends on {date}. Your account, with everything in it, will be deleted then, unless you set up a password before.'**
|
/// **'Sign-in with GitHub ends on {date}. Your account, with everything in it, will be deleted then, unless you set up a password before.'**
|
||||||
String passwordSectionGitHubEndsDeletes(String date);
|
String passwordSectionGitHubEndsDeletes(String date);
|
||||||
|
|
||||||
|
/// No description provided for @passwordSignedInBefore.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Only a device signed in since then can dismiss this warning: sign in again on this one, or change your password.'**
|
||||||
|
String get passwordSignedInBefore;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _HalkyonAppLocalizationsDelegate
|
class _HalkyonAppLocalizationsDelegate
|
||||||
|
|
|
||||||
|
|
@ -874,4 +874,8 @@ class HalkyonAppLocalizationsDe extends HalkyonAppLocalizations {
|
||||||
String passwordSectionGitHubEndsDeletes(String date) {
|
String passwordSectionGitHubEndsDeletes(String date) {
|
||||||
return 'Die Anmeldung mit GitHub endet am $date. Dann wird dein Konto mit allem darin gelöscht, es sei denn, du richtest vorher ein Passwort ein.';
|
return 'Die Anmeldung mit GitHub endet am $date. Dann wird dein Konto mit allem darin gelöscht, es sei denn, du richtest vorher ein Passwort ein.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get passwordSignedInBefore =>
|
||||||
|
'Nur ein seitdem angemeldetes Gerät kann diese Warnung ausblenden: Melde dich auf diesem neu an oder ändere dein Passwort.';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -857,6 +857,10 @@ class HalkyonAppLocalizationsEn extends HalkyonAppLocalizations {
|
||||||
String passwordSectionGitHubEndsDeletes(String date) {
|
String passwordSectionGitHubEndsDeletes(String date) {
|
||||||
return 'Sign-in with GitHub ends on $date. Your account, with everything in it, will be deleted then, unless you set up a password before.';
|
return 'Sign-in with GitHub ends on $date. Your account, with everything in it, will be deleted then, unless you set up a password before.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get passwordSignedInBefore =>
|
||||||
|
'Only a device signed in since then can dismiss this warning: sign in again on this one, or change your password.';
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The translations for English, as used in the United Kingdom (`en_GB`).
|
/// The translations for English, as used in the United Kingdom (`en_GB`).
|
||||||
|
|
|
||||||
|
|
@ -464,6 +464,7 @@ PasswordProblem _problemOf(protocol.PasswordRefusal reason) => switch (reason) {
|
||||||
protocol.PasswordRefusal.sessionEnded => PasswordProblem.sessionEnded,
|
protocol.PasswordRefusal.sessionEnded => PasswordProblem.sessionEnded,
|
||||||
protocol.PasswordRefusal.passwordChanged => PasswordProblem.passwordChanged,
|
protocol.PasswordRefusal.passwordChanged => PasswordProblem.passwordChanged,
|
||||||
protocol.PasswordRefusal.signInAgain => PasswordProblem.signInAgain,
|
protocol.PasswordRefusal.signInAgain => PasswordProblem.signInAgain,
|
||||||
|
protocol.PasswordRefusal.signedInBefore => PasswordProblem.signedInBefore,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Keeps one session in memory, for a client used once.
|
/// Keeps one session in memory, for a client used once.
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,10 @@ enum PasswordProblem() {
|
||||||
/// then go on.
|
/// then go on.
|
||||||
signInAgain,
|
signInAgain,
|
||||||
|
|
||||||
|
/// The warning that a session was ended is dismissed only by a session
|
||||||
|
/// made since: sign in again on this device, or change the password.
|
||||||
|
signedInBefore,
|
||||||
|
|
||||||
/// No reset waits for the address.
|
/// No reset waits for the address.
|
||||||
noReset,
|
noReset,
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ String passwordProblemText(BuildContext context, PasswordFailed failed) {
|
||||||
PasswordProblem.sessionEnded => l10n.passwordSessionEnded,
|
PasswordProblem.sessionEnded => l10n.passwordSessionEnded,
|
||||||
PasswordProblem.passwordChanged => l10n.passwordChangedSignInAgain,
|
PasswordProblem.passwordChanged => l10n.passwordChangedSignInAgain,
|
||||||
PasswordProblem.signInAgain => l10n.passwordSignInAgain,
|
PasswordProblem.signInAgain => l10n.passwordSignInAgain,
|
||||||
|
PasswordProblem.signedInBefore => l10n.passwordSignedInBefore,
|
||||||
PasswordProblem.noReset => l10n.passwordNoReset,
|
PasswordProblem.noReset => l10n.passwordNoReset,
|
||||||
PasswordProblem.passwordExists => l10n.passwordExists,
|
PasswordProblem.passwordExists => l10n.passwordExists,
|
||||||
PasswordProblem.addressTaken => l10n.passwordAddressTaken,
|
PasswordProblem.addressTaken => l10n.passwordAddressTaken,
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,13 @@ enum PasswordRefusal implements _isc.SerializableModel {
|
||||||
/// A first password for an account that signs in with GitHub alone takes
|
/// A first password for an account that signs in with GitHub alone takes
|
||||||
/// a GitHub sign-in of the last 10 minutes (the fourth reading: a session
|
/// a GitHub sign-in of the last 10 minutes (the fourth reading: a session
|
||||||
/// alone sets none): sign in with GitHub again, then set it.
|
/// alone sets none): sign in with GitHub again, then set it.
|
||||||
signInAgain;
|
signInAgain,
|
||||||
|
|
||||||
|
/// The warning that a session of the account was ended for its wrong
|
||||||
|
/// proofs is dismissed only by a session made since (the full reading's
|
||||||
|
/// L1: another session of a stolen device would dismiss its only use):
|
||||||
|
/// sign in again on this device, or change the password.
|
||||||
|
signedInBefore;
|
||||||
|
|
||||||
static PasswordRefusal fromJson(String name) {
|
static PasswordRefusal fromJson(String name) {
|
||||||
switch (name) {
|
switch (name) {
|
||||||
|
|
@ -139,6 +145,8 @@ enum PasswordRefusal implements _isc.SerializableModel {
|
||||||
return PasswordRefusal.passwordChanged;
|
return PasswordRefusal.passwordChanged;
|
||||||
case 'signInAgain':
|
case 'signInAgain':
|
||||||
return PasswordRefusal.signInAgain;
|
return PasswordRefusal.signInAgain;
|
||||||
|
case 'signedInBefore':
|
||||||
|
return PasswordRefusal.signedInBefore;
|
||||||
default:
|
default:
|
||||||
throw ArgumentError(
|
throw ArgumentError(
|
||||||
'Value "$name" cannot be converted to "PasswordRefusal"',
|
'Value "$name" cannot be converted to "PasswordRefusal"',
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,13 @@ enum PasswordRefusal implements _is.SerializableModel {
|
||||||
/// A first password for an account that signs in with GitHub alone takes
|
/// A first password for an account that signs in with GitHub alone takes
|
||||||
/// a GitHub sign-in of the last 10 minutes (the fourth reading: a session
|
/// a GitHub sign-in of the last 10 minutes (the fourth reading: a session
|
||||||
/// alone sets none): sign in with GitHub again, then set it.
|
/// alone sets none): sign in with GitHub again, then set it.
|
||||||
signInAgain;
|
signInAgain,
|
||||||
|
|
||||||
|
/// The warning that a session of the account was ended for its wrong
|
||||||
|
/// proofs is dismissed only by a session made since (the full reading's
|
||||||
|
/// L1: another session of a stolen device would dismiss its only use):
|
||||||
|
/// sign in again on this device, or change the password.
|
||||||
|
signedInBefore;
|
||||||
|
|
||||||
static PasswordRefusal fromJson(String name) {
|
static PasswordRefusal fromJson(String name) {
|
||||||
switch (name) {
|
switch (name) {
|
||||||
|
|
@ -139,6 +145,8 @@ enum PasswordRefusal implements _is.SerializableModel {
|
||||||
return PasswordRefusal.passwordChanged;
|
return PasswordRefusal.passwordChanged;
|
||||||
case 'signInAgain':
|
case 'signInAgain':
|
||||||
return PasswordRefusal.signInAgain;
|
return PasswordRefusal.signInAgain;
|
||||||
|
case 'signedInBefore':
|
||||||
|
return PasswordRefusal.signedInBefore;
|
||||||
default:
|
default:
|
||||||
throw ArgumentError(
|
throw ArgumentError(
|
||||||
'Value "$name" cannot be converted to "PasswordRefusal"',
|
'Value "$name" cannot be converted to "PasswordRefusal"',
|
||||||
|
|
|
||||||
|
|
@ -184,13 +184,30 @@ Future<void> noteSessionEndedForProofs(
|
||||||
/// `AccountRegister.dismissSessionEndedWarning`.
|
/// `AccountRegister.dismissSessionEndedWarning`.
|
||||||
Future<void> dismissSessionEndedWarning(
|
Future<void> dismissSessionEndedWarning(
|
||||||
Session session,
|
Session session,
|
||||||
UuidValue person,
|
UuidValue person, {
|
||||||
) async {
|
required DateTime? sessionMadeAt,
|
||||||
|
}) async {
|
||||||
|
// SELECT * FROM "account_password" WHERE "authUserId" = $1
|
||||||
|
final row = await AccountPassword.db.findFirstRow(
|
||||||
|
session,
|
||||||
|
where: (t) => t.authUserId.equals(person),
|
||||||
|
);
|
||||||
|
final warned = row?.sessionEndedForProofsAt;
|
||||||
|
if (warned == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Only a session made since (the full reading's L1): another session of
|
||||||
|
// the device whose session was ended would dismiss the warning's only
|
||||||
|
// use, its owner learning of it.
|
||||||
|
if (sessionMadeAt == null || !sessionMadeAt.isAfter(warned)) {
|
||||||
|
throw PasswordRefusedException(reason: PasswordRefusal.signedInBefore);
|
||||||
|
}
|
||||||
// UPDATE "account_password" SET "sessionEndedForProofsAt" = NULL
|
// UPDATE "account_password" SET "sessionEndedForProofsAt" = NULL
|
||||||
// WHERE "authUserId" = $1
|
// WHERE "authUserId" = $1 AND "sessionEndedForProofsAt" = $2
|
||||||
await AccountPassword.db.updateWhere(
|
await AccountPassword.db.updateWhere(
|
||||||
session,
|
session,
|
||||||
columnValues: (t) => [t.sessionEndedForProofsAt(null)],
|
columnValues: (t) => [t.sessionEndedForProofsAt(null)],
|
||||||
where: (t) => t.authUserId.equals(person),
|
where: (t) =>
|
||||||
|
t.authUserId.equals(person) & t.sessionEndedForProofsAt.equals(warned),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -161,12 +161,22 @@ class PasswordEndpoint extends Endpoint {
|
||||||
/// ten wrong proofs in a row (`account.signInWays`'
|
/// ten wrong proofs in a row (`account.signInWays`'
|
||||||
/// `sessionEndedForProofsAt`). Their session alone does: it hides a
|
/// `sessionEndedForProofsAt`). Their session alone does: it hides a
|
||||||
/// warning and changes nothing about how the account is secured.
|
/// warning and changes nothing about how the account is secured.
|
||||||
Future<void> dismissSessionEndedWarning(Session session) =>
|
Future<void> dismissSessionEndedWarning(Session session) async {
|
||||||
AccountRegister.of(session.serverpod).dismissSessionEndedWarning(
|
final authenticated = session.authenticated!;
|
||||||
|
// SELECT * FROM "serverpod_auth_core_session" WHERE "id" = $1
|
||||||
|
final asking = Uuid.isValidUUID(fromString: authenticated.authId)
|
||||||
|
? await ServerSideSession.db.findById(
|
||||||
session,
|
session,
|
||||||
session.authenticated!.authUserId,
|
UuidValue.withValidation(authenticated.authId),
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
await AccountRegister.of(session.serverpod).dismissSessionEndedWarning(
|
||||||
|
session,
|
||||||
|
authenticated.authUserId,
|
||||||
|
sessionMadeAt: asking?.createdAt,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Whether [session]'s own session (this server's) was made by a GitHub
|
/// Whether [session]'s own session (this server's) was made by a GitHub
|
||||||
/// sign-in within [gitHubSignInFreshFor] (the fourth reading).
|
/// sign-in within [gitHubSignInFreshFor] (the fourth reading).
|
||||||
|
|
|
||||||
|
|
@ -62,3 +62,8 @@ values:
|
||||||
### a GitHub sign-in of the last 10 minutes (the fourth reading: a session
|
### a GitHub sign-in of the last 10 minutes (the fourth reading: a session
|
||||||
### alone sets none): sign in with GitHub again, then set it.
|
### alone sets none): sign in with GitHub again, then set it.
|
||||||
- signInAgain
|
- signInAgain
|
||||||
|
### The warning that a session of the account was ended for its wrong
|
||||||
|
### proofs is dismissed only by a session made since (the full reading's
|
||||||
|
### L1: another session of a stolen device would dismiss its only use):
|
||||||
|
### sign in again on this device, or change the password.
|
||||||
|
- signedInBefore
|
||||||
|
|
|
||||||
|
|
@ -143,7 +143,11 @@ abstract interface class AccountRegister {
|
||||||
Future<void> noteSessionEndedForProofs(Session session, UuidValue person);
|
Future<void> noteSessionEndedForProofs(Session session, UuidValue person);
|
||||||
|
|
||||||
/// Dismisses [person]'s warning that a session was ended for its proofs.
|
/// Dismisses [person]'s warning that a session was ended for its proofs.
|
||||||
Future<void> dismissSessionEndedWarning(Session session, UuidValue person);
|
Future<void> dismissSessionEndedWarning(
|
||||||
|
Session session,
|
||||||
|
UuidValue person, {
|
||||||
|
required DateTime? sessionMadeAt,
|
||||||
|
});
|
||||||
|
|
||||||
/// Whether people sign in with a password and TOTP (ADR 0017): the
|
/// Whether people sign in with a password and TOTP (ADR 0017): the
|
||||||
/// register holds the keys it needs.
|
/// register holds the keys it needs.
|
||||||
|
|
|
||||||
|
|
@ -205,6 +205,8 @@ Future<Map<String, Object?>?> _answer(
|
||||||
await register.dismissSessionEndedWarning(
|
await register.dismissSessionEndedWarning(
|
||||||
session,
|
session,
|
||||||
arguments.id('person'),
|
arguments.id('person'),
|
||||||
|
// When the member's session asking was made (the full reading's L1).
|
||||||
|
sessionMadeAt: arguments.optionalTime('sessionMadeAt'),
|
||||||
);
|
);
|
||||||
return const {};
|
return const {};
|
||||||
case RegisterOperation.deleteAccount:
|
case RegisterOperation.deleteAccount:
|
||||||
|
|
@ -521,6 +523,12 @@ extension type const _Arguments(Map<String, Object?> _map) {
|
||||||
ByteData? optionalBytes(String name) =>
|
ByteData? optionalBytes(String name) =>
|
||||||
optionalString(name) == null ? null : bytes(name);
|
optionalString(name) == null ? null : bytes(name);
|
||||||
|
|
||||||
|
/// The moment [name] (ISO 8601) in UTC; null where it is missing.
|
||||||
|
DateTime? optionalTime(String name) => switch (optionalString(name)) {
|
||||||
|
final value? => DateTime.parse(value).toUtc(),
|
||||||
|
null => null,
|
||||||
|
};
|
||||||
|
|
||||||
/// Whether the argument [name] holds anything (not null).
|
/// Whether the argument [name] holds anything (not null).
|
||||||
bool has(String name) => _map[name] != null;
|
bool has(String name) => _map[name] != null;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -408,8 +408,13 @@ final class LocalRegister implements AccountRegister {
|
||||||
@override
|
@override
|
||||||
Future<void> dismissSessionEndedWarning(
|
Future<void> dismissSessionEndedWarning(
|
||||||
Session session,
|
Session session,
|
||||||
UuidValue person,
|
UuidValue person, {
|
||||||
) => owner_proofs.dismissSessionEndedWarning(session, person);
|
required DateTime? sessionMadeAt,
|
||||||
|
}) => owner_proofs.dismissSessionEndedWarning(
|
||||||
|
session,
|
||||||
|
person,
|
||||||
|
sessionMadeAt: sessionMadeAt,
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<bool> passwordSignInOffered(Session session) async =>
|
Future<bool> passwordSignInOffered(Session session) async =>
|
||||||
|
|
|
||||||
|
|
@ -285,9 +285,11 @@ final class RemoteRegister implements AccountRegister {
|
||||||
@override
|
@override
|
||||||
Future<void> dismissSessionEndedWarning(
|
Future<void> dismissSessionEndedWarning(
|
||||||
Session session,
|
Session session,
|
||||||
UuidValue person,
|
UuidValue person, {
|
||||||
) => _ask(RegisterOperation.dismissSessionEndedWarning, {
|
required DateTime? sessionMadeAt,
|
||||||
|
}) => _ask(RegisterOperation.dismissSessionEndedWarning, {
|
||||||
'person': person.uuid,
|
'person': person.uuid,
|
||||||
|
'sessionMadeAt': sessionMadeAt?.toUtc().toIso8601String(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
||||||
|
|
@ -459,6 +459,8 @@ void main() {
|
||||||
test('which her sign-in ways and her next sign-in tell, with when, '
|
test('which her sign-in ways and her next sign-in tell, with when, '
|
||||||
'until she dismisses it', () async {
|
'until she dismisses it', () async {
|
||||||
expect(await warningOn(onLaptop()), isNull);
|
expect(await warningOn(onLaptop()), isNull);
|
||||||
|
// Her tablet, signed in before (at the tests' time).
|
||||||
|
final tablet = await atNow(() => signInNewDevice(session, anna));
|
||||||
final ended = await endPhone();
|
final ended = await endPhone();
|
||||||
|
|
||||||
expect(await warningOn(onLaptop()), ended);
|
expect(await warningOn(onLaptop()), ended);
|
||||||
|
|
@ -467,7 +469,34 @@ void main() {
|
||||||
// Told, not dismissed: it stays until she says so.
|
// Told, not dismissed: it stays until she says so.
|
||||||
expect(await warningOn(onLaptop()), ended);
|
expect(await warningOn(onLaptop()), ended);
|
||||||
|
|
||||||
await endpoints.password.dismissSessionEndedWarning(onLaptop());
|
// Her tablet's session is older than the warning: it dismisses
|
||||||
|
// nothing (the full reading's L1: another session of a stolen
|
||||||
|
// device would dismiss it). Signed in again, it does.
|
||||||
|
await expectLater(
|
||||||
|
endpoints.password.dismissSessionEndedWarning(
|
||||||
|
sessionBuilder.copyWith(
|
||||||
|
authentication: AuthenticationOverride.authenticationInfo(
|
||||||
|
anna.uuid,
|
||||||
|
{},
|
||||||
|
authId: tablet.uuid,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
refused(PasswordRefusal.signedInBefore),
|
||||||
|
);
|
||||||
|
expect(await warningOn(onLaptop()), ended);
|
||||||
|
final newLaptop = await atNow(
|
||||||
|
() => signInNewDevice(session, anna),
|
||||||
|
);
|
||||||
|
await endpoints.password.dismissSessionEndedWarning(
|
||||||
|
sessionBuilder.copyWith(
|
||||||
|
authentication: AuthenticationOverride.authenticationInfo(
|
||||||
|
anna.uuid,
|
||||||
|
{},
|
||||||
|
authId: newLaptop.uuid,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
expect(await warningOn(onLaptop()), isNull);
|
expect(await warningOn(onLaptop()), isNull);
|
||||||
nextStep();
|
nextStep();
|
||||||
expect((await signIn()).sessionEndedForProofsAt, isNull);
|
expect((await signIn()).sessionEndedForProofsAt, isNull);
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,9 @@ final class _Register implements AccountRegister {
|
||||||
/// member's made just now.
|
/// member's made just now.
|
||||||
final freshGitHubSignIns = <UuidValue>{};
|
final freshGitHubSignIns = <UuidValue>{};
|
||||||
|
|
||||||
|
/// When the session dismissing a warning was made, by person.
|
||||||
|
final dismissedMadeAt = <UuidValue, DateTime?>{};
|
||||||
|
|
||||||
/// Those [setPassword] was told are anonymous on the member.
|
/// Those [setPassword] was told are anonymous on the member.
|
||||||
final anonymousHere_ = <UuidValue>{};
|
final anonymousHere_ = <UuidValue>{};
|
||||||
|
|
||||||
|
|
@ -226,9 +229,13 @@ final class _Register implements AccountRegister {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> dismissSessionEndedWarning(Session session, UuidValue person) =>
|
Future<void> dismissSessionEndedWarning(
|
||||||
_answer('dismissSessionEndedWarning', () {
|
Session session,
|
||||||
|
UuidValue person, {
|
||||||
|
required DateTime? sessionMadeAt,
|
||||||
|
}) => _answer('dismissSessionEndedWarning', () {
|
||||||
dismissed.add(person);
|
dismissed.add(person);
|
||||||
|
dismissedMadeAt[person] = sessionMadeAt;
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -1337,6 +1344,8 @@ void main() {
|
||||||
"never another's", () async {
|
"never another's", () async {
|
||||||
await endpoints.password.dismissSessionEndedWarning(asBen());
|
await endpoints.password.dismissSessionEndedWarning(asBen());
|
||||||
expect(register.dismissed, {ben});
|
expect(register.dismissed, {ben});
|
||||||
|
// With when Ben's session here was made (the full reading's L1).
|
||||||
|
expect(register.dismissedMadeAt[ben], isNotNull);
|
||||||
await expectLater(
|
await expectLater(
|
||||||
endpoints.password.dismissSessionEndedWarning(
|
endpoints.password.dismissSessionEndedWarning(
|
||||||
sessionBuilder.copyWith(
|
sessionBuilder.copyWith(
|
||||||
|
|
|
||||||
|
|
@ -613,7 +613,21 @@ void main() {
|
||||||
expect(answer.verifiesServer(proof.serverProof), isTrue);
|
expect(answer.verifiesServer(proof.serverProof), isTrue);
|
||||||
expect(proof.passwordKeys.toJson(), device.passwordKeys.toJson());
|
expect(proof.passwordKeys.toJson(), device.passwordKeys.toJson());
|
||||||
expect(proof.sessionEndedForProofsAt, warned);
|
expect(proof.sessionEndedForProofsAt, warned);
|
||||||
await remote.dismissSessionEndedWarning(session, annaOnTodo);
|
// An older session of the member's dismisses nothing (the full
|
||||||
|
// reading's L1); one made since does.
|
||||||
|
await expectLater(
|
||||||
|
remote.dismissSessionEndedWarning(
|
||||||
|
session,
|
||||||
|
annaOnTodo,
|
||||||
|
sessionMadeAt: warned,
|
||||||
|
),
|
||||||
|
refused(PasswordRefusal.signedInBefore),
|
||||||
|
);
|
||||||
|
await remote.dismissSessionEndedWarning(
|
||||||
|
session,
|
||||||
|
annaOnTodo,
|
||||||
|
sessionMadeAt: warned.add(const Duration(seconds: 1)),
|
||||||
|
);
|
||||||
expect(
|
expect(
|
||||||
(await remote.signInWaysOf(
|
(await remote.signInWaysOf(
|
||||||
session,
|
session,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue