Setup and invitations: the first login made from the client, everyone else by a group's invitation
Build / Build (push) Successful in 52s
Deploy / decepub.thepra.dev (push) Successful in 3m3s

PrivaPub's sign-up is closed. While it has no administrator, Login offers /setup, where the first login is made with its
first persona and becomes the administrator (PrivaPub's /clientapi/user/setup; owner decision 2026-10-16: first come,
first served, and setup then closes for good). Everyone else joins at /join?code=: a group's invitation makes the login,
and its first persona joins the group (/clientapi/user/invitation/signup). Login links there, and a group's page shows
the link next to its code. A login without a persona is sent from Login to Settings instead of a dead end. Login, setup
and join share AuthService's StartSession, and all three go without credentials.

e2e (Login/SignUpTests, against the pasture): setup offered on Login, made, the feed reached with Administration, then a
signed-out reader is offered sign-in and invitations only; an invitation link makes a login whose persona joins the
circle. The phone sweep covers /setup and /join: login, setup and join fit every portrait phone, and AccountTests signs
in through the changed Login. The town's signed-in tests were not rerun: today's pasture came up without Mastodon, and
its town seed left stale sessions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
thepraandClaude Opus 5.5 committed 2026-10-16 23:21:48 +02:00
1 parent 4da6a398d7
commit 5796d62406
12 files changed
+630 -35

No files matched your search

+33
View File
@@ -28,6 +28,39 @@ namespace decePubClient.Services
public async Task<WebResult> Login(LoginForm form, CancellationToken cancellationToken = default)
{
using var response = await httpService.PostTotallyAnon(APIs.ClientApi.POST_Login, form, cancellationToken: cancellationToken);
return await StartSession(response, cancellationToken);
}
/// <summary>Whether the server waits for its first login, which becomes its administrator; false when it can't say.</summary>
public async Task<bool> SetupPending(CancellationToken cancellationToken = default)
{
using var response = await httpService.GetTotallyAnon(APIs.ClientApi.GET_Setup, cancellationToken: cancellationToken);
if (!response.IsSuccessStatusCode)
{
await LogFailure(response, nameof(SetupPending), cancellationToken);
return false;
}
return (await response.DefaultReadFromJsonAsync<ViewServerSetup>(cancellationToken))?.IsPending ?? false;
}
/// <summary>Makes the server's first login, its administrator, with its first persona, and signs it in.</summary>
public async Task<WebResult> SetUp(SetupForm form, CancellationToken cancellationToken = default)
{
using var response = await httpService.PostTotallyAnon(APIs.ClientApi.POST_Setup, form, cancellationToken: cancellationToken);
return await StartSession(response, cancellationToken);
}
/// <summary>Makes a login through a group's invitation, its first persona a member of the group, and signs it in.</summary>
public async Task<WebResult> Join(InvitationLoginForm form, CancellationToken cancellationToken = default)
{
using var response = await httpService.PostTotallyAnon(APIs.ClientApi.POST_InvitationSignUp, form, cancellationToken: cancellationToken);
return await StartSession(response, cancellationToken);
}
/// <summary>Keeps the JWT a login, a setup or an invitation answered with.</summary>
async Task<WebResult> StartSession(HttpResponseMessage response, CancellationToken cancellationToken)
{
if (!response.IsSuccessStatusCode)
return await response.Content.ReadWebResult(response.StatusCode, localizer, cancellationToken);