Completion program R2, with PrivaPub's routes of the same release.
- Pages/PasswordRecovery.razor (/password-recovery): asks for a login or an address and shows the server's one
sentence; with ?rc= it takes the code out of the address bar at once, checks it and sets a new password. Login links
there ("Forgot your password?").
- Settings, the login: its email address with a verified badge, changed with the password and the code emailed to it
(DOtp), removed with the password; "Sign out everywhere" (every device and persona, asked first).
- A password change takes the new JWT PrivaPub answers and exchanges the persona's token again, since every persona
token ended with it.
- The theme kept with the login (PageSettings.SyncWithLogin, Settings > General): taken at sign-in and start-up once a
client saved it, saved 1.5 s after a change here, never posted back when nothing changed, never the language.
- Login, Setup and Join start the session through CascadingState.SignedIn, which takes the login's theme.
- 38 keys in both resx files; CLAUDE.md follows.
Verified: dotnet build decePubClient.csproj -c Release (0 errors, no new warnings).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WpVCoDeM1qUTRp4fXQdfof
138 lines
5.1 KiB
Plaintext
138 lines
5.1 KiB
Plaintext
@page "/setup"
|
|
@inherits LocalizableComponentBase
|
|
<Title>@Localizer["Set up PrivaPub"]</Title>
|
|
|
|
@* The server's first login, made while it has no administrator (PrivaPub's /clientapi/user/setup; owner decision 2026-10-16,
|
|
first come, first served): it becomes the administrator, with its first persona, and setup closes for good. Everyone
|
|
else joins through a group's invitation (Join). *@
|
|
<section class="block relative w-full h-full neo-inset neo-xs rounded-xl">
|
|
|
|
<div class="flex flex-col items-center gap-6 p-4 md:p-5 w-full h-full absolute overflow-y-auto">
|
|
|
|
@if (IsLoading)
|
|
{
|
|
<DLoading Variant="DLoadingVariant.Dots" LabelText="@Localizer["loading"]" />
|
|
}
|
|
else if (CascadingState.IsSignedIn)
|
|
{
|
|
<div class="flex flex-col gap-3 w-full max-w-sm mt-6 md:mt-16 p-5 rounded-xl neo-raised neo-sm">
|
|
<h1 class="text-xl font-bold">@Localizer["You are signed in"]</h1>
|
|
<DButton Href="" Color="DColor.Primary">@Localizer["Home"]</DButton>
|
|
</div>
|
|
}
|
|
else if (!isPending)
|
|
{
|
|
<div class="flex flex-col gap-3 w-full max-w-sm mt-6 md:mt-16 p-5 rounded-xl neo-raised neo-sm" data-setup-done>
|
|
<h1 class="text-xl font-bold">@Localizer["This server is set up"]</h1>
|
|
<p class="text-sm">@Localizer["It has an administrator already: sign in, or join with a group's invitation."]</p>
|
|
<div class="flex flex-wrap gap-2 pointer-coarse:gap-2">
|
|
<DButton Href="login" Color="DColor.Primary">@Localizer["Login"]</DButton>
|
|
<DButton Href="join">@Localizer["Join with an invitation"]</DButton>
|
|
</div>
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
<EditForm Model="form" OnValidSubmit="SetUp" class="flex flex-col gap-4 w-full max-w-sm mt-6 md:mt-16 p-5 rounded-xl neo-raised neo-sm" data-setup>
|
|
<DataAnnotationsValidator />
|
|
<h1 class="text-xl font-bold">@Localizer["Set up PrivaPub"]</h1>
|
|
<p class="text-sm opacity-70">@Localizer["This server has no administrator yet. The first login made here becomes it, with its first persona; after that, joining takes a group's invitation."]</p>
|
|
|
|
<b class="text-sm">@Localizer["Your login"]</b>
|
|
<label class="flex flex-col gap-1">
|
|
<span class="text-sm">@Localizer["Username"]</span>
|
|
<DInput TValue="string" class="w-full" @bind-Value="form.UserName" autocomplete="username" autocapitalize="none" spellcheck="false" data-setup-username>
|
|
<Prefix><i class="ion-md-person"></i></Prefix>
|
|
</DInput>
|
|
</label>
|
|
<label class="flex flex-col gap-1">
|
|
<span class="text-sm">@Localizer["Password"]</span>
|
|
<DInput TValue="string" Type="password" class="w-full" @bind-Value="form.Password" autocomplete="new-password" data-setup-password>
|
|
<Prefix><i class="ion-md-key"></i></Prefix>
|
|
</DInput>
|
|
</label>
|
|
<label class="flex flex-col gap-1">
|
|
<span class="text-sm">@Localizer["Repeat the password"]</span>
|
|
<DInput TValue="string" Type="password" class="w-full" @bind-Value="repeatedPassword" autocomplete="new-password" data-setup-repeat>
|
|
<Prefix><i class="ion-md-key"></i></Prefix>
|
|
</DInput>
|
|
</label>
|
|
|
|
<b class="text-sm">@Localizer["Your first persona"]</b>
|
|
<p class="text-xs opacity-70">@Localizer["Others see your personas, never your login."]</p>
|
|
<DInput TValue="string" class="w-full" @bind-Value="form.AvatarUserName" autocomplete="off" autocapitalize="none" spellcheck="false"
|
|
maxlength="@PrivaPub.ClientModels.Constants.MaxAvatarNameLength" placeholder="@Localizer["user name (a-z, 0-9, _)"]" data-setup-persona-username>
|
|
<Prefix>@@</Prefix>
|
|
</DInput>
|
|
<DInput TValue="string" class="w-full" @bind-Value="form.AvatarName" autocomplete="off" maxlength="64"
|
|
placeholder="@Localizer["Display name"]" data-setup-persona-name />
|
|
|
|
<ErrorViewer AdditionalError="@error" />
|
|
|
|
<DButton Type="submit" Color="DColor.Primary" Disabled="IsDefaultDisabled || isWorking" data-setup-submit>
|
|
@if (isWorking)
|
|
{
|
|
<DLoading Size="DSize.Sm" LabelText="@Localizer["loading"]" />
|
|
}
|
|
<span>@Localizer["Set up PrivaPub"]</span>
|
|
</DButton>
|
|
</EditForm>
|
|
}
|
|
|
|
</div>
|
|
|
|
</section>
|
|
|
|
@code {
|
|
[Inject] AuthService AuthService { get; set; }
|
|
|
|
readonly SetupForm form = new();
|
|
string repeatedPassword;
|
|
string error;
|
|
bool isPending;
|
|
bool isWorking;
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
if (CascadingState.IsOffline)
|
|
{
|
|
WhenOnline(LoadData);
|
|
StartOnlinePolling();
|
|
IsLoading = false;
|
|
return;
|
|
}
|
|
|
|
await LoadData();
|
|
IsLoading = false;
|
|
}
|
|
|
|
async Task LoadData() => isPending = await AuthService.SetupPending();
|
|
|
|
async Task SetUp()
|
|
{
|
|
error = default;
|
|
if (form.Password != repeatedPassword)
|
|
{
|
|
error = Localizer["The two passwords don't match."];
|
|
return;
|
|
}
|
|
|
|
isWorking = true;
|
|
form.AvatarUserName = form.AvatarUserName?.Trim().TrimStart('@').ToLowerInvariant();
|
|
var result = await AuthService.SetUp(form);
|
|
if (result.IsInvalid)
|
|
{
|
|
isWorking = false;
|
|
// set up meanwhile: the page says so
|
|
isPending = result.StatusCode != (int)System.Net.HttpStatusCode.Forbidden;
|
|
error = result.ErrorMessage;
|
|
return;
|
|
}
|
|
|
|
await CascadingState.SignedIn(result.Data as JwtUser);
|
|
isWorking = false;
|
|
Toast.AddResponseSuccess(Localizer["PrivaPub is set up: you are its administrator."]);
|
|
Navigation.NavigateTo("");
|
|
}
|
|
}
|