R2 accounts in decePub: forgotten password, email by code, sessions, the theme kept with the login

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
This commit is contained in:
thepraandClaude Opus 5.5 committed 2026-10-17 03:21:59 +02:00
1 parent fd2f2e7c65
commit 7d4b35f0d7
15 files changed
+1056 -17

No files matched your search

+31 -7
View File
@@ -101,7 +101,14 @@ Ionicons 4.5 (`wwwroot/vendor/ionicons.min.css`, `<i class="ion-md-...">`) is lo
- picking a hue leaves grey.
- `AppStatusService.IsDarkTheme()` tells which variant is showing.
- Settings → General edits every theme setting. NavMenu's toggle and slider are shortcuts to the dark and hue ones.
The other sections of Settings belong to the persona (see **Settings** under PrivaPub wiring).
The other sections of Settings belong to the login or the persona (see **Settings** under PrivaPub wiring).
- **The theme is the login's** while `PageSettings.SyncWithLogin` is on (the default; Settings → General turns it off
for this device). `CascadingState` takes the login's theme (`ViewUserSettings`; never its language, which would reload
the app) at sign-in from the `JwtUser` (`SignedIn`, which Login, Setup and Join call instead of `LoadSession`) and at
start-up from `/clientapi/user/settings`, once `savedAt` says a client saved one; while none did, it gives the login
this device's. `UpdatePageSettings` saves them to `/clientapi/user/update/settings` 1.5 s after the last change, one
save waiting at a time, only when they differ from what the login is known to keep (so taking them never sends them
back), with the language as two letters and the icons' hue at 25; a failure is logged, never shown.
- `Styles/neo.css` holds the neumorphic layer:
- `neo-raised` / `neo-inset` / `neo-flat`, with sizes `neo-2xs` / `neo-xs` / `neo-sm` / `neo-md` (these replace the
old `neomorph is-nxsmall` classes);
@@ -232,8 +239,8 @@ Never redeclare the base's members in a component.
`SUtility.DefaultSerializer` (camelCase, enums by name like PrivaPub, `ClientJsonContext`) for `clientapi/`. A payload
can be an object (JSON), name/value pairs (a form) or a `Func<HttpContent>` (multipart), built once per attempt.
- Auth levels: `Get` / `Post` / `Put` / `Patch` / `Delete` need credentials and end the session when PrivaPub refuses
them; `GetAnon` adds them when there are some; `*TotallyAnon` send none (the login must not carry a JWT:
PrivaPub redirects it).
them; `GetAnon` adds them when there are some; `*TotallyAnon` send none (the login and the password recovery must
not carry a JWT: PrivaPub redirects them).
- `retryOnError` backoff: 1, 2, 4, 8, 16, 29 s, then 60 s, for at most 10 minutes.
- Each request has its own timeout (`Measure`): 100 s, or 10 minutes for a multipart body (an upload); the
`HttpClient`'s own is off.
@@ -250,7 +257,13 @@ Never redeclare the base's members in a component.
- **One sign-in** (`Services/AuthService.cs`, `Pages/Login.razor`): the root signs in on `/clientapi/user/login`, then
each persona the client uses gets its own Mastodon token in exchange for the JWT (PrivaPub's `PersonaExchange`,
RFC 8693 on `/oauth/token`, first-party client id `AppConfiguration:ClientId`). `AuthData` keeps the JWT and the
persona tokens; logout revokes every persona token, then forgets the session.
persona tokens; logout revokes every persona token, ends this device's session (`/clientapi/user/logout`), then
forgets the session.
- **Sessions:** "Sign out everywhere" (`AuthService.LogoutEverywhere`, `/clientapi/user/logout/everywhere`) ends every
session of the login, this one too, and PrivaPub revokes every persona's token with them, so the client only forgets
its own and signs out. A password change ends every other session and revokes every persona token, this device's
too: `AuthService.ChangePassword` keeps the new `JwtUser` PrivaPub answers with, drops the stored persona tokens and
exchanges one for the current persona at once (the others when next used), so the device stays signed in.
- **The ways in** (sign-up is closed on PrivaPub; owner decision 2026-10-16):
- **Setup** (`Pages/Setup.razor` at `/setup`): while PrivaPub has no administrator (`/clientapi/user/setup`), the first
login is made there, first come, first served, with its first persona. It becomes the administrator, and setup
@@ -259,6 +272,13 @@ Never redeclare the base's members in a component.
the group (`/clientapi/user/invitation/signup`). Login links there, and a group's page shows the link with its code.
- Both sign in at once like Login (`AuthService.SetUp` / `Join`, sent with no credentials). A login without a
persona is sent from Login to Settings to make one.
- **A forgotten password** (`Pages/PasswordRecovery.razor` at `/password-recovery`, linked from Login): the login's
name or its address asks for a link (`/clientapi/user/recover/password`), and the page shows PrivaPub's answer, the
same sentence for everyone, so it never tells whether a login or an address exists. PrivaPub mails only a verified
address. The link comes back as `/password-recovery?rc=`: the page keeps the code, takes it out of the address bar
and the history at once (`replace`), checks it (`/recover/valid`, the code as a JSON string), and sets the new
password with it (`/recover/update/password`), which ends every session of the login; then the reader signs in.
All of it goes with no credentials.
- **Services** (`Services/`): `TimelineService` (every feed as `FeedPage`s of `FeedPost`s, paged by `max_id`/`min_id`;
circles are read as ids from `/clientapi` and hydrated through `/api/v1/statuses?id[]=`; located posts come from
`/clientapi/post/nearby` and have no Mastodon actions), `StatusService` (publish, favourite, boost, bookmark, delete,
@@ -338,9 +358,13 @@ Never redeclare the base's members in a component.
- **Interaction policies** (GoToSocial's, and FEP-5624's `canReply` on a PeerTube video whose comments are moderated): a
reply or boost whose target's author must approve it shows so on its card (`privapub.approval`: pending, then nothing
once approved, or rejected); one the policy shuts out is refused by PrivaPub (422) and shown as an error.
- **Settings** (`Pages/Settings.razor`, `Components/Settings/`): the login itself (`AccountEditor`: its password changed,
or the whole of it deleted, every persona with it, through `/clientapi/user/update/password` and `/clientapi/user/delete`;
the client then signs out), the persona's profile (`ProfileEditor`:
- **Settings** (`Pages/Settings.razor`, `Components/Settings/`): the login itself (`AccountEditor`: its email address
(`/clientapi/user/account`), set only with the password and a 6-digit code mailed to the new address (`/user/email`,
then `/email/verify` through `DOtp`; the old `/user/update` is refused), shown verified or not and with an address
waiting for its code, and removed with the password (`/email/remove`); a wrong password is PrivaPub's 403, read as
such; its password changed; every session of it ended ("Sign out everywhere", see **One sign-in**); or the whole of it
deleted, every persona with it, through `/clientapi/user/update/password` and `/clientapi/user/delete`; the client then
signs out), the persona's profile (`ProfileEditor`:
`update_credentials` as multipart, its words as written from `Account.Source`, pictures (streamed from the file,
and removed with `DELETE /api/v1/profile/avatar|header`), fields, locked, bot,
discoverable, indexable and posting defaults; the composer starts a new post at its default visibility), its mutes,