feat(base)!: a handle at password sign-up (ADR 0020, D26, D28–D30)
Every new account gets a handle, and an account without one gets its first as it sets a password. The handle is private until its person goes public (D5). - `passwordSignIn.signUp` takes a handle. `passwordSignIn.checkHandle` says whether a sign-up could take one, only while that sign-up's mailed code waits (`codeExpired` otherwise), counted per code request (30 an hour) and per network (60) (D29). Members ask the register. - `password.set` takes the first handle of an account without one (D28); `SignInWays.handle` tells the app whether there is one. - Each checks the handle before the code is used, so a refusal leaves the code for another try. An existing account at the address keeps its own handle where it has one, and the typed handle is still checked, so the answer says nothing about the address. - `first_handle.dart` holds what SimpleLogin, the keys and passwords share: `refusalOfNewHandle`, `insertFirstProfile`, `hasHandle`. - halkyon_app: one `HandleField` with its `HandleCheckCubit` (asked 300 ms after typing pauses, only about the handle typed last; the instance's suggestion is free without asking) (D30). The password setup asks for the handle between the phrase check and the code, and a refusal at the code sends back to it. SimpleLogin's sign-up and keys steps use the same field and now check as it is typed. - The module's README and ADR 0020 (D29, D30, the built state) amended. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
87671208df
commit
630965d920
9 changed files with 2305 additions and 395 deletions
|
|
@ -184,7 +184,11 @@ void main() {
|
|||
usePasswordKeys(pod, testPasswordKeys);
|
||||
now = DateTime.utc(2026, 10, 20, 12);
|
||||
device = await PasswordDevice.make();
|
||||
anna = await newPersonWithAddress(session, 'anna@example.org');
|
||||
anna = await newPersonWithAddress(
|
||||
session,
|
||||
'anna@example.org',
|
||||
handle: 'anna',
|
||||
);
|
||||
annasPhone = await signInNewDevice(session, anna);
|
||||
annasLaptop = await signInNewDevice(session, anna);
|
||||
final code = await mailedCode(
|
||||
|
|
@ -975,7 +979,11 @@ void main() {
|
|||
'code by mail asked for deleting, nothing less, nor one asked for '
|
||||
'anything else',
|
||||
() async {
|
||||
final ben = await newPersonWithAddress(session, 'ben@example.org');
|
||||
final ben = await newPersonWithAddress(
|
||||
session,
|
||||
'ben@example.org',
|
||||
handle: 'ben',
|
||||
);
|
||||
final bensLaptop = await signInNewDevice(session, ben);
|
||||
final bens = await PasswordDevice.make(seed: 7);
|
||||
TestSessionBuilder asBen() => sessionBuilder.copyWith(
|
||||
|
|
|
|||
|
|
@ -48,7 +48,11 @@ void main() {
|
|||
EmailCodes.of(pod).mailer = mailbox;
|
||||
usePasswordKeys(pod, testPasswordKeys);
|
||||
now = DateTime.utc(2026, 10, 20, 12);
|
||||
anna = await newPersonWithAddress(session, 'anna@example.org');
|
||||
anna = await newPersonWithAddress(
|
||||
session,
|
||||
'anna@example.org',
|
||||
handle: 'anna',
|
||||
);
|
||||
annasPhone = await signInNewDevice(
|
||||
session,
|
||||
anna,
|
||||
|
|
@ -353,10 +357,139 @@ void main() {
|
|||
request: code.request,
|
||||
code: code.code,
|
||||
setup: device.setup(),
|
||||
handle: 'ben',
|
||||
);
|
||||
expect(signedUp.authUserId, isNot(ben));
|
||||
});
|
||||
|
||||
group('for an account without a handle (ADR 0020, D28)', () {
|
||||
late UuidValue ben;
|
||||
late UuidValue bensPhone;
|
||||
|
||||
setUp(() async {
|
||||
ben = await newPersonWithAddress(session, 'ben@example.org');
|
||||
bensPhone = await signInNewDevice(
|
||||
session,
|
||||
ben,
|
||||
scopes: {identified},
|
||||
);
|
||||
});
|
||||
|
||||
TestSessionBuilder asBen() => sessionBuilder.copyWith(
|
||||
authentication: AuthenticationOverride.authenticationInfo(
|
||||
ben.uuid,
|
||||
{identified},
|
||||
authId: bensPhone.uuid,
|
||||
),
|
||||
);
|
||||
|
||||
Future<({UuidValue request, String code})> bensCode() => mailedCode(
|
||||
'ben@example.org',
|
||||
purpose: EmailCodePurpose.firstPassword,
|
||||
);
|
||||
|
||||
Matcher refusedAsProfile(ProfileRefusal reason) => throwsA(
|
||||
isA<ProfileRefusedException>().having(
|
||||
(error) => error.reason,
|
||||
'reason',
|
||||
reason,
|
||||
),
|
||||
);
|
||||
|
||||
test('gives it the handle with its first password, its profile '
|
||||
'private', () async {
|
||||
expect(
|
||||
(await endpoints.account.signInWays(asBen())).handle,
|
||||
isFalse,
|
||||
);
|
||||
final code = await bensCode();
|
||||
|
||||
await endpoints.password.set(
|
||||
asBen(),
|
||||
setup: device.setup(),
|
||||
request: code.request,
|
||||
code: code.code,
|
||||
handle: 'Ben',
|
||||
);
|
||||
|
||||
final profile = (await PublicProfile.db.findFirstRow(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(ben),
|
||||
))!;
|
||||
expect(profile.handle, 'Ben');
|
||||
expect(profile.displayName, isEmpty);
|
||||
expect(
|
||||
(await endpoints.account.signInWays(asBen())).handle,
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
test('is refused without a handle, and for one refused as saving a '
|
||||
'profile refuses it, before the code is used', () async {
|
||||
final code = await bensCode();
|
||||
Future<void> set(String? handle) => endpoints.password.set(
|
||||
asBen(),
|
||||
setup: device.setup(),
|
||||
request: code.request,
|
||||
code: code.code,
|
||||
handle: handle,
|
||||
);
|
||||
|
||||
for (final (handle, reason) in [
|
||||
(null, ProfileRefusal.handleInvalid),
|
||||
('ANNA', ProfileRefusal.handleTaken),
|
||||
('b-e-n', ProfileRefusal.handleInvalid),
|
||||
('admin', ProfileRefusal.handleReserved),
|
||||
]) {
|
||||
await expectLater(
|
||||
set(handle),
|
||||
refusedAsProfile(reason),
|
||||
reason: '$handle',
|
||||
);
|
||||
}
|
||||
expect(await AccountPassword.db.count(session), 0);
|
||||
expect(await PublicProfile.db.count(session), 1);
|
||||
|
||||
await set('ben');
|
||||
expect(
|
||||
await AccountPassword.db.count(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(ben),
|
||||
),
|
||||
1,
|
||||
);
|
||||
});
|
||||
|
||||
test('is refused a handle for an account that has one, before the '
|
||||
'code is used', () async {
|
||||
final code = await ownAddressCode();
|
||||
|
||||
await expectLater(
|
||||
endpoints.password.set(
|
||||
asAnna(),
|
||||
setup: device.setup(),
|
||||
request: code.request,
|
||||
code: code.code,
|
||||
handle: 'anna_two',
|
||||
),
|
||||
refusedAsProfile(ProfileRefusal.handleInvalid),
|
||||
);
|
||||
await endpoints.password.set(
|
||||
asAnna(),
|
||||
setup: device.setup(),
|
||||
request: code.request,
|
||||
code: code.code,
|
||||
);
|
||||
expect(
|
||||
(await PublicProfile.db.findFirstRow(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(anna),
|
||||
))!.handle,
|
||||
'anna',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('for someone who signs in with GitHub alone', () {
|
||||
setUp(() async {
|
||||
// Anna has no address here: GitHub is her only way in.
|
||||
|
|
@ -1460,6 +1593,7 @@ void main() {
|
|||
setup: bensDevice.setup(),
|
||||
request: bensCode.request,
|
||||
code: bensCode.code,
|
||||
handle: 'ben',
|
||||
);
|
||||
|
||||
await expectLater(
|
||||
|
|
@ -1831,6 +1965,7 @@ void main() {
|
|||
setup: (await PasswordDevice.make(seed: 7)).setup(),
|
||||
request: code.request,
|
||||
code: code.code,
|
||||
handle: 'ben',
|
||||
);
|
||||
final bens = await endpoints.authenticator.enrol(
|
||||
asBen,
|
||||
|
|
|
|||
|
|
@ -50,7 +50,11 @@ void main() {
|
|||
EmailCodes.of(pod).mailer = mailbox;
|
||||
usePasswordKeys(pod, testPasswordKeys);
|
||||
now = DateTime.utc(2026, 10, 20, 12);
|
||||
anna = await newPersonWithAddress(session, 'anna@example.org');
|
||||
anna = await newPersonWithAddress(
|
||||
session,
|
||||
'anna@example.org',
|
||||
handle: 'anna',
|
||||
);
|
||||
annasPhone = await signInNewDevice(
|
||||
session,
|
||||
anna,
|
||||
|
|
|
|||
|
|
@ -74,16 +74,22 @@ void main() {
|
|||
return (request: request, code: mailbox.codeFor(address));
|
||||
}
|
||||
|
||||
Future<AuthSuccess> signUp(String address, {PasswordDevice? with_}) =>
|
||||
atNow(() async {
|
||||
final code = await mailedCode(address);
|
||||
return await endpoints.passwordSignIn.signUp(
|
||||
notSignedIn(),
|
||||
request: code.request,
|
||||
code: code.code,
|
||||
setup: (with_ ?? device).setup(),
|
||||
);
|
||||
});
|
||||
/// Signs up with [address], a code mailed there and [handle] (the
|
||||
/// address's part before the @ unless given).
|
||||
Future<AuthSuccess> signUp(
|
||||
String address, {
|
||||
PasswordDevice? with_,
|
||||
String? handle,
|
||||
}) => atNow(() async {
|
||||
final code = await mailedCode(address);
|
||||
return await endpoints.passwordSignIn.signUp(
|
||||
notSignedIn(),
|
||||
request: code.request,
|
||||
code: code.code,
|
||||
setup: (with_ ?? device).setup(),
|
||||
handle: handle ?? address.split('@').first,
|
||||
);
|
||||
});
|
||||
|
||||
/// Signs in as a device would, both steps, with a code by mail where
|
||||
/// [mailedTo] names its address.
|
||||
|
|
@ -196,7 +202,7 @@ void main() {
|
|||
final other = await PasswordDevice.make(seed: 99);
|
||||
|
||||
await expectLater(
|
||||
signUp('anna@example.org', with_: other),
|
||||
signUp('anna@example.org', with_: other, handle: 'anna_again'),
|
||||
refused(PasswordRefusal.passwordExists),
|
||||
);
|
||||
final after = (await AccountPassword.db.findFirstRow(
|
||||
|
|
@ -219,6 +225,7 @@ void main() {
|
|||
request: code.request,
|
||||
code: 'WRONGCOD',
|
||||
setup: device.setup(),
|
||||
handle: 'anna',
|
||||
),
|
||||
refusedAsEmail(EmailSignInRefusal.wrongCode),
|
||||
);
|
||||
|
|
@ -231,6 +238,7 @@ void main() {
|
|||
request: code.request,
|
||||
code: code.code,
|
||||
setup: short,
|
||||
handle: 'anna',
|
||||
),
|
||||
refused(PasswordRefusal.invalidSetup),
|
||||
);
|
||||
|
|
@ -241,6 +249,7 @@ void main() {
|
|||
request: code.request,
|
||||
code: code.code,
|
||||
setup: device.setup(),
|
||||
handle: 'anna',
|
||||
),
|
||||
refused(PasswordRefusal.unavailable),
|
||||
);
|
||||
|
|
@ -249,6 +258,185 @@ void main() {
|
|||
});
|
||||
});
|
||||
|
||||
group('a handle at sign-up (ADR 0020, D26, D29)', () {
|
||||
Matcher refusedAsProfile(ProfileRefusal reason) => throwsA(
|
||||
isA<ProfileRefusedException>().having(
|
||||
(error) => error.reason,
|
||||
'reason',
|
||||
reason,
|
||||
),
|
||||
);
|
||||
|
||||
Future<PublicProfile?> profileOf(UuidValue person) =>
|
||||
PublicProfile.db.findFirstRow(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(person),
|
||||
);
|
||||
|
||||
Future<ProfileRefusal?> check(UuidValue request, String handle) =>
|
||||
endpoints.passwordSignIn.checkHandle(
|
||||
notSignedIn(),
|
||||
request: request,
|
||||
handle: handle,
|
||||
);
|
||||
|
||||
test('gives a new account the handle, its profile private until its '
|
||||
'person goes public', () async {
|
||||
final anna = (await signUp(
|
||||
'anna@example.org',
|
||||
handle: 'Anna_M',
|
||||
)).authUserId;
|
||||
|
||||
final profile = (await profileOf(anna))!;
|
||||
expect(profile.handle, 'Anna_M');
|
||||
expect(profile.handleKey, 'anna_m');
|
||||
expect(profile.displayName, isEmpty);
|
||||
expect((await endpoints.account.signInWays(as(anna))).handle, isTrue);
|
||||
await expectLater(
|
||||
endpoints.profiles.byHandle(as(anna), 'anna_m'),
|
||||
throwsA(isA<ProfileNotFoundException>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('is checked for the code request while the code travels, as '
|
||||
'saving a profile checks it', () async {
|
||||
await signUp('anna@example.org', handle: 'anna');
|
||||
final ben = await mailedCode('ben@example.org');
|
||||
|
||||
expect(await check(ben.request, 'ben'), isNull);
|
||||
expect(await check(ben.request, 'ANNA'), ProfileRefusal.handleTaken);
|
||||
expect(await check(ben.request, 'a-b'), ProfileRefusal.handleInvalid);
|
||||
expect(
|
||||
await check(ben.request, 'admin'),
|
||||
ProfileRefusal.handleReserved,
|
||||
);
|
||||
// Checking used nothing: the code still signs Ben up.
|
||||
await endpoints.passwordSignIn.signUp(
|
||||
notSignedIn(),
|
||||
request: ben.request,
|
||||
code: ben.code,
|
||||
setup: device.setup(),
|
||||
handle: 'ben',
|
||||
);
|
||||
});
|
||||
|
||||
test('is checked for no request but one for a sign-up waiting: '
|
||||
'none unknown, used, past its time or for anything else, nor '
|
||||
'where the instance has no password sign-in', () async {
|
||||
final unknown = const Uuid().v7obj();
|
||||
final used = await mailedCode('anna@example.org');
|
||||
await endpoints.passwordSignIn.signUp(
|
||||
notSignedIn(),
|
||||
request: used.request,
|
||||
code: used.code,
|
||||
setup: device.setup(),
|
||||
handle: 'anna',
|
||||
);
|
||||
final other = await mailedCode(
|
||||
'ben@example.org',
|
||||
purpose: EmailCodePurpose.finishSetup,
|
||||
);
|
||||
for (final request in [unknown, used.request, other.request]) {
|
||||
await expectLater(
|
||||
check(request, 'carla'),
|
||||
refusedAsEmail(EmailSignInRefusal.codeExpired),
|
||||
);
|
||||
}
|
||||
final late_ = await atNow(() => mailedCode('carla@example.org'));
|
||||
now = now.add(emailCodeLifetime + const Duration(seconds: 1));
|
||||
await expectLater(
|
||||
atNow(() => check(late_.request, 'carla')),
|
||||
refusedAsEmail(EmailSignInRefusal.codeExpired),
|
||||
);
|
||||
final waiting = await mailedCode('dora@example.org');
|
||||
usePasswordKeys(pod, null);
|
||||
await expectLater(
|
||||
check(waiting.request, 'dora'),
|
||||
refused(PasswordRefusal.unavailable),
|
||||
);
|
||||
});
|
||||
|
||||
test('answers at most 30 an hour for a request, signing up '
|
||||
'counted with them', () async {
|
||||
final code = await mailedCode('anna@example.org');
|
||||
for (var i = 0; i < 29; i++) {
|
||||
await check(code.request, 'anna$i');
|
||||
}
|
||||
await check(code.request, 'anna');
|
||||
|
||||
await expectLater(
|
||||
check(code.request, 'anna'),
|
||||
refusedAsProfile(ProfileRefusal.tooManyHandleChecks),
|
||||
);
|
||||
await expectLater(
|
||||
endpoints.passwordSignIn.signUp(
|
||||
notSignedIn(),
|
||||
request: code.request,
|
||||
code: code.code,
|
||||
setup: device.setup(),
|
||||
handle: 'anna',
|
||||
),
|
||||
refusedAsProfile(ProfileRefusal.tooManyHandleChecks),
|
||||
);
|
||||
expect(await AuthUser.db.count(session), 0);
|
||||
// Another request asks on.
|
||||
final ben = await mailedCode('ben@example.org');
|
||||
expect(await check(ben.request, 'ben'), isNull);
|
||||
});
|
||||
|
||||
test('refused as saving a profile refuses it, makes nobody and leaves '
|
||||
'the code unused', () async {
|
||||
await signUp('anna@example.org', handle: 'anna');
|
||||
final ben = await mailedCode('ben@example.org');
|
||||
for (final (handle, reason) in [
|
||||
('Anna', ProfileRefusal.handleTaken),
|
||||
('b e n', ProfileRefusal.handleInvalid),
|
||||
('', ProfileRefusal.handleInvalid),
|
||||
('admin', ProfileRefusal.handleReserved),
|
||||
]) {
|
||||
await expectLater(
|
||||
endpoints.passwordSignIn.signUp(
|
||||
notSignedIn(),
|
||||
request: ben.request,
|
||||
code: ben.code,
|
||||
setup: device.setup(),
|
||||
handle: handle,
|
||||
),
|
||||
refusedAsProfile(reason),
|
||||
reason: handle,
|
||||
);
|
||||
}
|
||||
expect(await AuthUser.db.count(session), 1);
|
||||
expect(await EmailIdentity.db.count(session), 1);
|
||||
|
||||
final signedUp = await endpoints.passwordSignIn.signUp(
|
||||
notSignedIn(),
|
||||
request: ben.request,
|
||||
code: ben.code,
|
||||
setup: device.setup(),
|
||||
handle: 'ben',
|
||||
);
|
||||
expect((await profileOf(signedUp.authUserId))!.handle, 'ben');
|
||||
});
|
||||
|
||||
test('gives an account with the address and no password its first '
|
||||
'handle, and leaves the handle of one that has one', () async {
|
||||
final ben = await newPersonWithAddress(session, 'ben@example.org');
|
||||
final carl = await newPersonWithAddress(
|
||||
session,
|
||||
'carl@example.org',
|
||||
handle: 'carl',
|
||||
);
|
||||
|
||||
await signUp('ben@example.org', handle: 'ben');
|
||||
await signUp('carl@example.org', handle: 'carl_new');
|
||||
|
||||
expect((await profileOf(ben))!.handle, 'ben');
|
||||
expect((await profileOf(carl))!.handle, 'carl');
|
||||
expect(await PublicProfile.db.count(session), 2);
|
||||
});
|
||||
});
|
||||
|
||||
group('a setup never finished', () {
|
||||
late UuidValue anna;
|
||||
|
||||
|
|
|
|||
|
|
@ -463,9 +463,15 @@ final class _Register implements AccountRegister {
|
|||
required PasswordSetup setup,
|
||||
UuidValue? request,
|
||||
String? code,
|
||||
String? handle,
|
||||
required String network,
|
||||
bool freshGitHubSignIn = false,
|
||||
UuidValue? keepSession,
|
||||
}) => _answer('setPassword', () {
|
||||
networks.add(network);
|
||||
if (handle != null) {
|
||||
handlesChecked.add(handle);
|
||||
}
|
||||
add(person);
|
||||
passwords.add(person);
|
||||
if (freshGitHubSignIn) {
|
||||
|
|
@ -512,13 +518,29 @@ final class _Register implements AccountRegister {
|
|||
return _answer('completePasswordSignIn', _proven);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ProfileRefusal?> checkSignUpHandle(
|
||||
Session session, {
|
||||
required UuidValue request,
|
||||
required String handle,
|
||||
required String network,
|
||||
}) => _answer('checkSignUpHandle', () {
|
||||
networks.add(network);
|
||||
handlesChecked.add(handle);
|
||||
return handleRefusal;
|
||||
});
|
||||
|
||||
@override
|
||||
Future<UuidValue> signUpWithCode(
|
||||
Session session, {
|
||||
required UuidValue request,
|
||||
required String code,
|
||||
required PasswordSetup setup,
|
||||
required String handle,
|
||||
required String network,
|
||||
}) => _answer('signUpWithCode', () {
|
||||
networks.add(network);
|
||||
handlesChecked.add(handle);
|
||||
final account = const Uuid().v7obj();
|
||||
add(account);
|
||||
passwords.add(account);
|
||||
|
|
@ -1020,6 +1042,23 @@ void main() {
|
|||
);
|
||||
});
|
||||
|
||||
test("checks a sign-up's handle at the register while the code "
|
||||
'travels (ADR 0020, D29), the device network passed on', () async {
|
||||
register.handleRefusal = ProfileRefusal.handleTaken;
|
||||
|
||||
expect(
|
||||
await endpoints.passwordSignIn.checkHandle(
|
||||
notSignedIn(),
|
||||
request: const Uuid().v7obj(),
|
||||
handle: 'carla',
|
||||
),
|
||||
ProfileRefusal.handleTaken,
|
||||
);
|
||||
expect(register.asked, ['checkSignUpHandle']);
|
||||
expect(register.handlesChecked, ['carla']);
|
||||
expect(register.networks, hasLength(1));
|
||||
});
|
||||
|
||||
test('sign up at the register, the new account signed in here, '
|
||||
'identified', () async {
|
||||
final device = await PasswordDevice.make();
|
||||
|
|
@ -1029,9 +1068,12 @@ void main() {
|
|||
request: const Uuid().v7obj(),
|
||||
code: 'K7QX-M2PD',
|
||||
setup: device.setup(),
|
||||
handle: 'anna',
|
||||
);
|
||||
|
||||
expect(register.asked, ['signUpWithCode']);
|
||||
expect(register.handlesChecked, ['anna']);
|
||||
expect(register.networks, hasLength(1));
|
||||
expect(register.passwords, {signedUp.authUserId});
|
||||
expect(await scopesOf(signedUp.authUserId), {identified.name});
|
||||
expect(
|
||||
|
|
|
|||
|
|
@ -444,6 +444,7 @@ void main() {
|
|||
final annaOnTodo = await newPersonWithAddress(
|
||||
session,
|
||||
'anna@example.org',
|
||||
handle: 'anna',
|
||||
);
|
||||
final request = await remote.sendSignInCode(
|
||||
session,
|
||||
|
|
@ -460,6 +461,7 @@ void main() {
|
|||
await expectLater(
|
||||
remote.setPassword(
|
||||
session,
|
||||
network: '203.0.113.7',
|
||||
person: annaOnTodo,
|
||||
setup: device.setup(),
|
||||
request: request,
|
||||
|
|
@ -475,6 +477,7 @@ void main() {
|
|||
await expectLater(
|
||||
remote.setPassword(
|
||||
session,
|
||||
network: '203.0.113.7',
|
||||
person: carlOnTodo,
|
||||
setup: device.setup(),
|
||||
request: request,
|
||||
|
|
@ -485,6 +488,7 @@ void main() {
|
|||
expect(await AuthUser.db.findById(session, carlOnTodo), isNull);
|
||||
await remote.setPassword(
|
||||
session,
|
||||
network: '203.0.113.7',
|
||||
person: annaOnTodo,
|
||||
setup: device.setup(),
|
||||
request: request,
|
||||
|
|
@ -1088,6 +1092,7 @@ void main() {
|
|||
final annaOnTodo = await newPersonWithAddress(
|
||||
session,
|
||||
'anna@example.org',
|
||||
handle: 'anna',
|
||||
);
|
||||
final request = await remote.sendSignInCode(
|
||||
session,
|
||||
|
|
@ -1098,6 +1103,7 @@ void main() {
|
|||
);
|
||||
await remote.setPassword(
|
||||
session,
|
||||
network: '203.0.113.7',
|
||||
person: annaOnTodo,
|
||||
setup: device.setup(),
|
||||
request: request,
|
||||
|
|
@ -1110,6 +1116,7 @@ void main() {
|
|||
await expectLater(
|
||||
remote.setPassword(
|
||||
session,
|
||||
network: '203.0.113.7',
|
||||
person: ben,
|
||||
setup: device.setup().copyWith(salt: ByteData(3)),
|
||||
),
|
||||
|
|
@ -1390,6 +1397,7 @@ void main() {
|
|||
final annaOnTodo = await newPersonWithAddress(
|
||||
session,
|
||||
'anna@example.org',
|
||||
handle: 'anna',
|
||||
);
|
||||
final annasPhone = const Uuid().v7obj();
|
||||
final request = await remote.sendSignInCode(
|
||||
|
|
@ -1401,6 +1409,7 @@ void main() {
|
|||
);
|
||||
await remote.setPassword(
|
||||
session,
|
||||
network: '203.0.113.7',
|
||||
person: annaOnTodo,
|
||||
setup: device.setup(),
|
||||
request: request,
|
||||
|
|
@ -1584,6 +1593,8 @@ void main() {
|
|||
request: await code(),
|
||||
code: mailbox.codeFor('carla@example.org'),
|
||||
setup: device.setup(),
|
||||
handle: 'carla',
|
||||
network: '203.0.113.7',
|
||||
);
|
||||
expect(
|
||||
(await AuthUser.db.findById(session, carla))!.scopeNames,
|
||||
|
|
@ -1595,6 +1606,8 @@ void main() {
|
|||
request: await code(),
|
||||
code: mailbox.codeFor('carla@example.org'),
|
||||
setup: device.setup(),
|
||||
handle: 'carla_again',
|
||||
network: '203.0.113.7',
|
||||
),
|
||||
refused(PasswordRefusal.passwordExists),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -104,8 +104,13 @@ Future<UuidValue> signInNewDevice(
|
|||
}
|
||||
|
||||
/// A person as codes by mail left them before passwords (ADR 0017):
|
||||
/// identified, with [address] as their only way in, and no password.
|
||||
Future<UuidValue> newPersonWithAddress(Session session, String address) async {
|
||||
/// identified, with [address] as their only way in, and no password; with
|
||||
/// [handle] where given (ADR 0020, D5), their profile private.
|
||||
Future<UuidValue> newPersonWithAddress(
|
||||
Session session,
|
||||
String address, {
|
||||
String? handle,
|
||||
}) async {
|
||||
final person = (await AuthServices.instance.authUsers.create(
|
||||
session,
|
||||
scopes: {identified},
|
||||
|
|
@ -120,6 +125,22 @@ Future<UuidValue> newPersonWithAddress(Session session, String address) async {
|
|||
createdAt: clock.now(),
|
||||
),
|
||||
);
|
||||
if (handle != null) {
|
||||
// INSERT INTO "public_profile" (…) VALUES (…)
|
||||
await PublicProfile.db.insertRow(
|
||||
session,
|
||||
PublicProfile(
|
||||
authUserId: person,
|
||||
handle: handle,
|
||||
handleKey: handle.toLowerCase(),
|
||||
displayName: '',
|
||||
bio: '',
|
||||
links: const [],
|
||||
verifiedLinks: const [],
|
||||
updatedAt: clock.now(),
|
||||
),
|
||||
);
|
||||
}
|
||||
return person;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -52,5 +52,10 @@ Future<void> deleteAll(Session session) async {
|
|||
session,
|
||||
where: (t) => t.id.notEquals(null),
|
||||
);
|
||||
// Handles, which signing up and first passwords give (ADR 0020, D26, D28).
|
||||
await PublicProfile.db.deleteWhere(
|
||||
session,
|
||||
where: (t) => t.id.notEquals(null),
|
||||
);
|
||||
await AuthUser.db.deleteWhere(session, where: (t) => t.id.notEquals(null));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1154,6 +1154,7 @@ class _PasswordEndpoint {
|
|||
required _ikhyee47.PasswordSetup setup,
|
||||
_is.UuidValue? request,
|
||||
String? code,
|
||||
String? handle,
|
||||
}) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
|
|
@ -1170,6 +1171,7 @@ class _PasswordEndpoint {
|
|||
'setup': setup,
|
||||
'request': request,
|
||||
'code': code,
|
||||
'handle': handle,
|
||||
}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
|
|
@ -1442,11 +1444,47 @@ class _PasswordSignInEndpoint {
|
|||
});
|
||||
}
|
||||
|
||||
_ida.Future<_idszjcpz.ProfileRefusal?> checkHandle(
|
||||
_ist.TestSessionBuilder sessionBuilder, {
|
||||
required _is.UuidValue request,
|
||||
required String handle,
|
||||
}) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
|
||||
endpoint: 'passwordSignIn',
|
||||
method: 'checkHandle',
|
||||
);
|
||||
try {
|
||||
var _localCallContext = await _endpointDispatch.getMethodCallContext(
|
||||
createSessionCallback: (_) => _localUniqueSession,
|
||||
endpointPath: 'passwordSignIn',
|
||||
methodName: 'checkHandle',
|
||||
parameters: _ist.testObjectToJson({
|
||||
'request': request,
|
||||
'handle': handle,
|
||||
}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<_idszjcpz.ProfileRefusal?>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_ida.Future<_iacs.AuthSuccess> signUp(
|
||||
_ist.TestSessionBuilder sessionBuilder, {
|
||||
required _is.UuidValue request,
|
||||
required String code,
|
||||
required _ikhyee47.PasswordSetup setup,
|
||||
required String handle,
|
||||
}) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
|
|
@ -1463,6 +1501,7 @@ class _PasswordSignInEndpoint {
|
|||
'request': request,
|
||||
'code': code,
|
||||
'setup': setup,
|
||||
'handle': handle,
|
||||
}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue