docs: expand Phase 19 to all three AuthBlocks registration paths + reset brief
Cover admin provision-now, public self-service redeem, and admin invite-by-email across CMS + public-site tracks. Add standalone AuthBlocks password-reset team brief.
This commit is contained in:
@@ -380,56 +380,86 @@ opacity + muted-text mixes are tune-on-screen details, not decision gates.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Phase 19 — AuthBlocks User Management in the CMS
|
## Phase 19 — AuthBlocks User Management (CMS admin + public self-registration)
|
||||||
|
|
||||||
Wire the AuthBlocks user-administration surface (create users, manage existing accounts, manage
|
Wire **all three** AuthBlocks account-creation paths into DeepDrft, each on its correct host: the
|
||||||
registration invites, manage role permissions) into the `DeepDrftManager` CMS so an admin runs account
|
CMS-side user-administration surface (provision users, manage accounts, manage registration invites,
|
||||||
management from inside the authenticated CMS. Daniel's framing: *"already part of the AuthBlocks library
|
manage role permissions) on `DeepDrftManager`, **and** the public-facing self-service registration form
|
||||||
so we just wire it up."* Correct — and **further along than it implies.** Full design, the
|
on `DeepDrftPublic`. Daniel's framing: *"already part of the AuthBlocks library so we just wire it up."*
|
||||||
already-done-vs-remaining split, nav-shape alternatives, scope boundaries, and open questions:
|
Correct for the CMS — and **further along than it implies** there; the public-site path is a genuine
|
||||||
|
cold-start integration. Full design, the verified three-path model, the already-done-vs-remaining split,
|
||||||
|
the public-site cold-start analysis, scope boundaries, and open questions:
|
||||||
`product-notes/phase-19-user-management-cms.md`.
|
`product-notes/phase-19-user-management-cms.md`.
|
||||||
|
|
||||||
**Headline finding — most of the wiring already landed by side-effect.** The AuthBlocks startup
|
**The three account-creation paths (verified against AuthBlocks source 2026-06-19):**
|
||||||
separation (`PLAN_authblocks_trackmanager.md`, 2026-05-25) + the login/logout integration already put
|
1. **Admin provisions directly** — `SuperRegister.razor` → `/account/superregister` → `POST
|
||||||
the entire user-admin surface in place: `Cerebellum.AuthBlocks.Web` is referenced
|
api/auth/admin-register` (UserAdmin-gated, **working**). Host: **CMS**. Creates a live account now.
|
||||||
(`DeepDrftManager.csproj`), `ConfigureAuthServices` registers every user-admin client + ViewModel
|
2. **Public self-service** — `Register.razor` → `/account/register` → `POST api/auth/register`
|
||||||
pointed at DeepDrftAPI (`Program.cs`), the Blazor router already discovers the AuthBlocks pages
|
(unauthenticated, **working**). Host: **PUBLIC SITE**. Invited user redeems a code (pre-filled from the
|
||||||
(`Routes.razor` `AdditionalAssemblies`), they already render in `CmsLayout` (`DefaultLayout`), and the
|
invite email's deep link) and self-registers.
|
||||||
DeepDrft `Admin` role **inherits** `UserAdmin` (so the seeded admin already passes the page gate with no
|
3. **Admin provisions a token + triggers the invite email** — `NewRegistration(Form).razor` →
|
||||||
role change). The user-admin pages ship in a published **RCL** (`Cerebellum.AuthBlocks.Web` — an
|
`/useradmin/registrations/new` → `POST api/pendingregistration/create` (UserAdmin-gated). Host:
|
||||||
`Sdk.Razor` project with no `Program.cs`), so the brief's worried-about "extract pages into an RCL" fork
|
**CMS**. **Sends a real email server-side** via Mailtrap (`RegistrationEmailTemplate` +
|
||||||
**does not arise**. The API host (`api/users/*`, `api/auth/admin-register`, etc.) is already mounted on
|
`IGeneralEmailSender`, configured in DeepDrftAPI from `environment/authblocks.json`) — **not stubbed.**
|
||||||
DeepDrftAPI via `MapAuthBlocks`.
|
|
||||||
|
|
||||||
**The genuine remaining work is exposure + verification + polish, not integration.** The surface is
|
**Scope reversal (Daniel, 2026-06-19).** Rev. 1 deferred public registration and treated "create user"
|
||||||
invisible because `CmsLayout` has **no nav menu at all** (just an app bar + Home button), so nothing
|
as one CMS path. Both reversed: all three paths are in scope, and public registration (path 2) is now a
|
||||||
links to `/useradmin/*`. The work: (G1) add navigation; (G2) verify the wired surface end-to-end; (G3) a
|
distinct **public-site track**. The only genuinely stubbed surface is **Reset Password** (`Users.razor`,
|
||||||
legibility-only theming sweep.
|
`// todo integrate with email`; **no backing endpoint** in `AuthRoutes`) — handled separately by Daniel
|
||||||
|
in the AuthBlocks repo (see `product-notes/authblocks-password-reset-brief.md`).
|
||||||
|
|
||||||
**Sequenced as one real wave + verification.** `19.1 → {19.2, 19.3}`.
|
**CMS side — most wiring already landed by side-effect.** The AuthBlocks startup separation
|
||||||
|
(`PLAN_authblocks_trackmanager.md`, 2026-05-25) + login/logout integration already put the entire
|
||||||
|
user-admin surface in place on `DeepDrftManager`: `Cerebellum.AuthBlocks.Web` referenced,
|
||||||
|
`ConfigureAuthServices` registers every client + ViewModel pointed at DeepDrftAPI, the router discovers
|
||||||
|
the pages (`AdditionalAssemblies`), they render in `CmsLayout` (`DefaultLayout`), and the DeepDrft
|
||||||
|
`Admin` role **inherits** `UserAdmin` (the seeded admin passes the gate with no role change). The pages
|
||||||
|
ship in a published **RCL**, so the worried-about "extract pages into an RCL" fork **does not arise**.
|
||||||
|
The CMS remaining work is exposure + verification + polish — the surface is invisible only because
|
||||||
|
`CmsLayout` has **no nav menu** (app bar + Home button), so nothing links to `/useradmin/*` or
|
||||||
|
`/account/superregister`.
|
||||||
|
|
||||||
- **19.1 — CmsLayout navigation (cold-start, the only code wave).** Add a `MudDrawer` + toggle to
|
**Public side — genuine cold start.** `DeepDrftPublic` has **no AuthBlocks footprint at all** (verified:
|
||||||
`CmsLayout.razor`; mount the shipped `UserAdminMenu` fragment (self-gates to `UserAdmin`+) alongside
|
no package ref, no `ConfigureAuthServices`, no page discovery). Path 2 requires real host integration:
|
||||||
the existing CMS destinations (Catalogue / Releases / Upload); wire the canonical create-user link
|
package ref + service wiring + page discovery + layout + CORS verification. The render-mode substrate is
|
||||||
(OQ2). **No service, API, data, or AuthBlocks-source change.** **Recommended nav shape: G1-b** (a real
|
compatible (the public site already has InteractiveServer, which `Register.razor` needs).
|
||||||
drawer reusing AuthBlocks' own `MudNavGroup`) over an app-bar overflow stopgap or a heavier dedicated
|
|
||||||
admin dashboard.
|
**Two parallel tracks.** CMS track `19.1 → {19.2, 19.3}`; public track `19.4` independent and parallel.
|
||||||
- **19.2 — End-to-end verification (after 19.1).** Exercise list/create/deactivate users,
|
|
||||||
registrations, permissions against a running DeepDrftAPI; confirm cross-host token + CORS. Mostly
|
*CMS track:*
|
||||||
test; any break is likely a one-line config fix or an upstream AuthBlocks issue.
|
- **19.1 — CmsLayout navigation (cold-start, the only CMS code wave). DECIDED nav shape: G1-b.** Add a
|
||||||
|
`MudDrawer` + toggle to `CmsLayout.razor`; mount the shipped `UserAdminMenu` fragment (self-gates to
|
||||||
|
`UserAdmin`+) alongside the existing CMS destinations (Catalogue / Releases / Upload); surface **both**
|
||||||
|
admin account paths (path 1 `SuperRegister` + path 3 via the Registrations link); do **not** surface the
|
||||||
|
redundant bare `NewUser` (OQ2 resolved). **No service, API, data, or AuthBlocks-source change.**
|
||||||
|
- **19.2 — End-to-end verification (after 19.1).** Exercise provision-now (path 1), **invite-email send
|
||||||
|
(path 3)**, list/deactivate users, permissions against a running DeepDrftAPI; confirm cross-host token +
|
||||||
|
CORS and that the Mailtrap creds are real. Mostly test; any break is likely a one-line config fix or an
|
||||||
|
upstream AuthBlocks issue.
|
||||||
- **19.3 — Theming legibility sweep (after 19.1, parallel-ok).** Accept the CMS palette for the
|
- **19.3 — Theming legibility sweep (after 19.1, parallel-ok).** Accept the CMS palette for the
|
||||||
MudBlazor-default grids; fix only contrast/legibility breaks. Bespoke restyle deferred.
|
MudBlazor-default grids; fix only contrast/legibility breaks. Bespoke restyle deferred.
|
||||||
|
|
||||||
**Deferred (note, don't build):** an admin dashboard landing (G1-c); working **Reset Password** (the
|
*Public-site track:*
|
||||||
AuthBlocks Users page stubs it — an *upstream AuthBlocks-repo* effort, not a DeepDrft wiring task);
|
- **19.4 — Public self-service registration on DeepDrftPublic (cold-start, parallel to 19.1).** Wire path
|
||||||
bespoke restyle of the AuthBlocks grids; surfacing self-service registration on the public site;
|
2: add `Cerebellum.AuthBlocks.Web` to `DeepDrftPublic`, call `ConfigureAuthServices` in `Program.cs`
|
||||||
bumping `Cerebellum.AuthBlocks.Web` 10.3.33 → 10.3.35 (housekeeping, Daniel's timing).
|
pointed at the existing DeepDrftAPI base URL, add page discovery so `/account/register` is reachable,
|
||||||
|
settle layout (OQ8) + route-exposure posture (OQ7), verify CORS for the public origin. Real
|
||||||
|
host-integration code (unlike the CMS). Acceptance: the full path-3→path-2 loop works — admin provisions
|
||||||
|
in CMS → email arrives → invited user redeems on the public site.
|
||||||
|
|
||||||
**Open questions for Daniel (spec §6):** (1) nav shape — confirm **G1-b**; (2) canonical create-user
|
**Deferred (note, don't build):** admin dashboard landing (G1-c); working **Reset Password** (separate
|
||||||
entry — `SuperRegister` (role multiselect, recommended) vs. `NewUser` (bare form); (3) admin dashboard
|
AuthBlocks-repo effort); bespoke restyle of the AuthBlocks grids; a lean public auth layout (OQ8); a
|
||||||
defer vs. include (recommend defer); (4) package bump now vs. separate (recommend leave); (5) confirm
|
visible public-nav Register link (OQ9 — invite-only, deep-link entry); bumping
|
||||||
Reset Password is accepted **non-functional in v1** so verification doesn't file it as a DeepDrft bug.
|
`Cerebellum.AuthBlocks.Web` 10.3.33 → 10.3.35 (housekeeping; if 19.4 adds the package to the public host,
|
||||||
Items 1, 2, 5 shape the work/acceptance; 3, 4 don't block 19.1.
|
pin both hosts to one version).
|
||||||
|
|
||||||
|
**Open questions for Daniel (spec §6).** *Resolved:* (1) nav shape **G1-b**; (2) surface path 1 +
|
||||||
|
path 3, hide bare `NewUser`; (5) Reset Password non-functional in v1, handled separately. *Still open:*
|
||||||
|
(3) admin dashboard defer (recommend defer); (4) package bump (recommend leave); (6) accept public site
|
||||||
|
becoming auth-aware (recommend accept); (7) public route exposure of admin routes — present-but-gated vs.
|
||||||
|
narrow discovery (recommend accept for v1); (8) public-registration layout — full chrome vs. lean
|
||||||
|
(recommend full for v1); (9) public Register nav link (recommend deep-link-only). Items 6–9 shape 19.4;
|
||||||
|
3, 4 are CMS scope/timing. None block 19.1.
|
||||||
|
|
||||||
**Adjacency to the deferred Identity / accounts backlog item (below).** That item is about *public,
|
**Adjacency to the deferred Identity / accounts backlog item (below).** That item is about *public,
|
||||||
per-user* identity (favourites, listening history, playlists). This phase is *CMS-admin* account
|
per-user* identity (favourites, listening history, playlists). This phase is *CMS-admin* account
|
||||||
|
|||||||
@@ -0,0 +1,240 @@
|
|||||||
|
# Team Brief — Email-Backed Password Reset for AuthBlocks
|
||||||
|
|
||||||
|
**Audience:** an orchestrator (and its implementers) working **only** in the AuthBlocks repository at
|
||||||
|
`C:\Development\AuthBlocks`. You do not need, and should not assume, any knowledge of the products that
|
||||||
|
consume AuthBlocks. Everything you need is in this brief or in that repo.
|
||||||
|
|
||||||
|
**Status:** scoped request, not yet started. Author: product-designer (for a downstream consumer team).
|
||||||
|
Date: 2026-06-19.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. The goal in one sentence
|
||||||
|
|
||||||
|
Replace the non-functional "Reset Password" stub on the AuthBlocks user-administration **Users** page
|
||||||
|
with a real, email-backed password-reset flow — so that triggering "Reset Password" for a user sends
|
||||||
|
that user an email containing a secure, time-limited reset link, and following the link lets them set a
|
||||||
|
new password.
|
||||||
|
|
||||||
|
This is an **upstream library feature**, delivered entirely inside AuthBlocks and published as a normal
|
||||||
|
version bump. Consumers pick it up by referencing the new package version.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Where the stub lives today
|
||||||
|
|
||||||
|
`AuthBlocksWeb/Components/Pages/UserAdmin/Users/Users.razor` — the user grid has a per-row **Reset
|
||||||
|
Password** `MudButton` whose handler is empty:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private async Task ResetPassword(UserInputModel? item)
|
||||||
|
{
|
||||||
|
// todo integrate with email for secure reset
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
There is **no backing API endpoint** for this action. `AuthBlocksLib/Routes/AuthRoutes.cs` maps
|
||||||
|
`login`, `register`, `admin-register`, `refresh`, `logout`, `me`, `roles` — and nothing for password
|
||||||
|
reset. So this is a build-from-scratch flow on both the API side (new endpoints) and the Web side
|
||||||
|
(wire the button + add a public reset page), reusing AuthBlocks' existing email and token machinery.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. What AuthBlocks already has that you should reuse
|
||||||
|
|
||||||
|
**The pending-registration flow is your template.** AuthBlocks already does almost exactly this shape
|
||||||
|
of work for invitations — generate a secure token, email a link, validate the token when the user
|
||||||
|
returns. Read it end-to-end before designing reset; you are building a sibling flow:
|
||||||
|
|
||||||
|
- **Email sending is real and wired.** `AuthBlocksLib/AuthBlocksExtensions.cs` (~line 109) registers
|
||||||
|
`services.AddScoped<IGeneralEmailSender, MailtrapEmailSender>();`. The `IGeneralEmailSender`
|
||||||
|
abstraction and `MailtrapEmailSender` implementation come from the shared NetBlocks library
|
||||||
|
(namespace `API.Common.Email.Mailtrap`). The send signature in use is:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
await emailSender.SendEmailAsync(toAddress, cc: null, subject, htmlBody);
|
||||||
|
```
|
||||||
|
|
||||||
|
See it called for real at `AuthBlocksLib/Routes/PendingRegistrationRoutes.cs:124`.
|
||||||
|
|
||||||
|
- **Email connection config.** The host populates `AuthBlocksOptions.EmailConnection` (a NetBlocks
|
||||||
|
`EmailConnection` with `Host` + `Token`) plus `ApplicationName` and `SupportEmail` when it calls
|
||||||
|
`AddAuthBlocks(options => { ... })`. Those flow into `AuthBlocksExtensions` and are available to your
|
||||||
|
reset endpoint exactly as they are to the registration endpoint. **You do not need to invent any new
|
||||||
|
config or sender** — reuse `IGeneralEmailSender` and `AuthBlocksOptions`.
|
||||||
|
|
||||||
|
- **An HTML email template pattern.** `AuthBlocksLib/Common/RegistrationEmailTemplate.cs` is a static
|
||||||
|
`Create(token, link, applicationName, supportEmail)` returning a styled HTML string. Build a sibling
|
||||||
|
`PasswordResetEmailTemplate.Create(...)` in the same file's neighbourhood and the same house style
|
||||||
|
(the registration template is teal-branded, table-layout, support-line-collapses-when-empty — match
|
||||||
|
it). Do **not** reuse the registration template verbatim; the copy is invitation-specific.
|
||||||
|
|
||||||
|
- **A token service pattern.** `AuthBlocksLib/Services/RegistrationTokenService.cs` generates a random
|
||||||
|
token, SHA-256-hashes `{email}::{token}`, persists the hash with a 7-day expiry, and validates /
|
||||||
|
consumes it. **However — for password reset, prefer ASP.NET Identity's built-in reset token** (see
|
||||||
|
§4) rather than re-implementing this hand-rolled scheme. The registration token service is a *style*
|
||||||
|
reference for endpoint shape and email-link construction, not necessarily the token mechanism.
|
||||||
|
|
||||||
|
- **The deep-link construction idiom.** The registration flow builds its link with
|
||||||
|
`QueryHelpers.AddQueryString(returnHost, { UserEmail, RegistrationToken })` and the public register
|
||||||
|
page reads those query params and pre-fills (`Register.razor`, `[SupplyParameterFromQuery]`). Mirror
|
||||||
|
this for the reset page: link carries `email` + `resetToken`; the reset page reads them.
|
||||||
|
|
||||||
|
- **Identity is fully present.** `UserService` wraps `UserManager<ApplicationUser>` (see
|
||||||
|
`AuthBlocksData/Services/UserService.cs`). `UserManager` gives you the canonical reset primitives —
|
||||||
|
use them.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Recommended mechanism: ASP.NET Identity's built-in reset token
|
||||||
|
|
||||||
|
Password reset is a solved problem in ASP.NET Identity, and rolling your own token store for it is an
|
||||||
|
avoidable security surface. **Strong recommendation:** use `UserManager<ApplicationUser>`'s built-in
|
||||||
|
reset tokens rather than the hand-rolled `RegistrationTokenService` SHA-256 scheme.
|
||||||
|
|
||||||
|
- `var token = await userManager.GeneratePasswordResetTokenAsync(user);` — produces a token bound to
|
||||||
|
the user's security stamp; invalidated when the password changes or the stamp rotates.
|
||||||
|
- `var result = await userManager.ResetPasswordAsync(user, token, newPassword);` — validates and
|
||||||
|
applies in one call; enforces the configured password policy.
|
||||||
|
- Token lifetime is governed by `DataProtectionTokenProviderOptions.TokenLifespan` (default 1 day) —
|
||||||
|
confirm/configure to a sensible reset window (recommend 1–2 hours for reset, tighter than the 7-day
|
||||||
|
registration window).
|
||||||
|
|
||||||
|
This means you likely **do not** need a new DB table or migration for reset (unlike registration,
|
||||||
|
which persists pending rows). Confirm whether the default token providers are registered in the
|
||||||
|
AuthBlocks Identity setup; if `AddDefaultTokenProviders()` (or equivalent) is not already called in the
|
||||||
|
Identity configuration, add it — that is the one wiring prerequisite for `GeneratePasswordResetTokenAsync`
|
||||||
|
to work.
|
||||||
|
|
||||||
|
*Alternative considered (and not recommended):* extend `RegistrationTokenService` / `PendingRegistration`
|
||||||
|
into a generic token table that also serves reset. Rejected — it couples two unrelated flows, re-implements
|
||||||
|
what Identity already does correctly, and adds a migration for no benefit. Use it only if there is a
|
||||||
|
hard reason the Identity token provider cannot be enabled in this setup.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. The surfaces to build
|
||||||
|
|
||||||
|
Three pieces, mirroring the registration flow's API-endpoint + email-template + web-page triad.
|
||||||
|
|
||||||
|
### 5.1 API endpoints (`AuthBlocksLib/Routes/AuthRoutes.cs`)
|
||||||
|
|
||||||
|
Add to the `api/auth` group. Two endpoints, both **unauthenticated** (a user resetting a forgotten
|
||||||
|
password is by definition not logged in — the admin "Reset Password" button triggers the *first* of
|
||||||
|
these on the user's behalf, but the endpoint itself authenticates via the token, not a bearer):
|
||||||
|
|
||||||
|
1. **`POST api/auth/forgot-password`** — body `{ email, returnHost }`. Looks up the user; if found,
|
||||||
|
generates a reset token and emails the reset link (`{returnHost}?email=&resetToken=`). **Always
|
||||||
|
return success** regardless of whether the email exists — do **not** leak account existence (a known
|
||||||
|
reset-flow security requirement; the registration flow's "user already exists" message is acceptable
|
||||||
|
for an *admin-gated* invite but a *public* forgot-password must not reveal it). On email-send failure,
|
||||||
|
log and return a generic failure.
|
||||||
|
|
||||||
|
2. **`POST api/auth/reset-password`** — body `{ email, resetToken, newPassword }`. Resolves the user,
|
||||||
|
calls `ResetPasswordAsync(user, token, newPassword)`, returns the Identity result mapped to the
|
||||||
|
AuthBlocks `Result`/`ApiResult` convention (see how `Register` maps results in `AuthRoutes.cs`).
|
||||||
|
|
||||||
|
Follow the existing `AuthRoutes` conventions exactly: `ApiResult<T>` / `ApiResultDto<T>` wrapping,
|
||||||
|
`ILogger<AuthLogger>` for logging, `Results.Ok` / `Results.BadRequest` / `Results.Json(..., 500)` shapes.
|
||||||
|
|
||||||
|
### 5.2 Email template (`AuthBlocksLib/Common/PasswordResetEmailTemplate.cs`)
|
||||||
|
|
||||||
|
New static `Create(resetLink, applicationName, supportEmail)` in the visual style of
|
||||||
|
`RegistrationEmailTemplate`. Reset copy: a clear "you (or an admin) requested a password reset," the CTA
|
||||||
|
button to the reset link, an expiry notice matching the token lifespan, and "ignore this email if you
|
||||||
|
didn't request it." No registration code box — reset uses an opaque token in the link, not a
|
||||||
|
user-typed code (recommended; do not show the Identity token as a copy-paste code — it is long and
|
||||||
|
URL-encoded).
|
||||||
|
|
||||||
|
### 5.3 Web surfaces (`AuthBlocksWeb`)
|
||||||
|
|
||||||
|
- **Wire the admin button.** In `Users.razor`, replace the empty `ResetPassword` handler with a call to
|
||||||
|
an `IAuthApiClient` (or the appropriate existing client) method that hits `POST api/auth/forgot-password`
|
||||||
|
for `item.Email`, and show a confirmation (a `StatusMessage` / dialog: "Reset email sent to {email}").
|
||||||
|
This is the admin-initiated trigger.
|
||||||
|
- **Add a public reset page.** New `AuthBlocksWeb/Components/Pages/Account/ResetPassword.razor`,
|
||||||
|
`@page "/account/reset-password"`, `@rendermode InteractiveServer`, **no role gate** (a forgotten-password
|
||||||
|
user is unauthenticated). Read `email` + `resetToken` from query params (mirror `Register.razor`'s
|
||||||
|
`[SupplyParameterFromQuery]` pre-fill), present new-password + confirm fields, submit to
|
||||||
|
`POST api/auth/reset-password`, and on success route to `/account/login` with a success message. Match
|
||||||
|
`Register.razor`'s form structure and validation idiom.
|
||||||
|
- **Optional: a public "forgot password?" entry.** Consider a `/account/forgot-password` page (link from
|
||||||
|
`Login.razor`) where a user enters their email to self-initiate reset — same `forgot-password` endpoint.
|
||||||
|
Decide whether this is in scope or whether reset is admin-initiated only (see open questions).
|
||||||
|
|
||||||
|
### 5.4 Client method
|
||||||
|
|
||||||
|
Add the `forgot-password` / `reset-password` calls to whichever API client the Web project uses for auth
|
||||||
|
(the registration/login flows go through `JwtAuthenticationStateProvider` / `IAuthApiClient` — follow the
|
||||||
|
same pattern; do not introduce a new HTTP client).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Constraints
|
||||||
|
|
||||||
|
- **No account-existence leak** on the public `forgot-password` path (§5.1).
|
||||||
|
- **Reuse, don't reinvent:** `IGeneralEmailSender` for sending, `AuthBlocksOptions` for config, Identity's
|
||||||
|
token provider for tokens, the existing `Result`/`ApiResult` conventions for endpoint returns, and the
|
||||||
|
`RegistrationEmailTemplate` house style for the email.
|
||||||
|
- **Match the existing route + result conventions** in `AuthRoutes.cs` precisely — this is a library;
|
||||||
|
consumers rely on the shape staying idiomatic.
|
||||||
|
- **Versioning:** this lands as a normal AuthBlocks version bump (packed/pushed by `pack.ps1` like the
|
||||||
|
other packages). Note the new version so consumers can pin to it.
|
||||||
|
- **Token lifespan** for reset should be short (recommend 1–2 hours), distinct from the 7-day
|
||||||
|
registration token.
|
||||||
|
- **Password policy** is enforced by `ResetPasswordAsync` automatically — do not duplicate validation,
|
||||||
|
but surface the Identity error messages back through the result.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Acceptance criteria
|
||||||
|
|
||||||
|
1. Clicking "Reset Password" for a user on the Users admin page sends that user a styled email with a
|
||||||
|
working reset link, and shows the admin a confirmation. No unhandled exception, no silent no-op.
|
||||||
|
2. Following the reset link lands on `/account/reset-password` with the email pre-filled; setting a new
|
||||||
|
password that meets policy succeeds and the user can immediately log in with the new password.
|
||||||
|
3. An expired or tampered token is rejected with a clear, non-leaky error.
|
||||||
|
4. The public `forgot-password` endpoint returns the same response whether or not the email maps to a
|
||||||
|
real account (no existence leak).
|
||||||
|
5. Email send is exercised through the real `IGeneralEmailSender` (Mailtrap in the configured
|
||||||
|
environment) — verify an email actually arrives.
|
||||||
|
6. No new required config beyond what `AddAuthBlocks` already accepts (reset reuses the existing email
|
||||||
|
connection + application-name + support-email options). If a token-provider registration was missing,
|
||||||
|
it is added and documented.
|
||||||
|
7. Published as a version bump; the new version is recorded.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Open questions for the implementing team / its sponsor
|
||||||
|
|
||||||
|
1. **Admin-initiated only, or also public self-serve?** Is the only entry point the admin "Reset
|
||||||
|
Password" button (§5.3 first bullet), or do you also want a public "forgot password?" link from the
|
||||||
|
login page (§5.3 last bullet)? The endpoints support both; the question is which Web surfaces to build.
|
||||||
|
*Recommendation: build both endpoints, ship the admin button now, and add the public forgot-password
|
||||||
|
page in the same pass since it is nearly free once the endpoint exists.*
|
||||||
|
2. **Token mechanism — confirm Identity's built-in is acceptable** (§4 recommendation) vs. a hard
|
||||||
|
requirement to use the hand-rolled hashed-token scheme. *Recommendation: Identity built-in.*
|
||||||
|
3. **Reset token lifespan** — confirm the window (recommend 1–2 hours).
|
||||||
|
4. **Return host / link base** — the registration flow has the *caller* pass `returnHost`. Confirm the
|
||||||
|
reset flow does the same (the consumer supplies the base URL of its public reset page), vs. AuthBlocks
|
||||||
|
configuring a reset base URL in options. *Recommendation: pass `returnHost` per-call, mirroring
|
||||||
|
registration, so AuthBlocks stays host-agnostic.*
|
||||||
|
5. **Does the public reset page (`/account/reset-password`) need to render in a consumer's own layout?**
|
||||||
|
The page ships in the AuthBlocks RCL with no `@layout`, so it inherits whatever the consuming host sets
|
||||||
|
as default — same as `Register.razor`. Confirm this is acceptable (it should be; it is how registration
|
||||||
|
already behaves).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Suggested reading order in the repo
|
||||||
|
|
||||||
|
1. `AuthBlocksLib/Routes/PendingRegistrationRoutes.cs` — the email-sending endpoint to mirror.
|
||||||
|
2. `AuthBlocksLib/Routes/AuthRoutes.cs` — where your endpoints go; the result/logging conventions.
|
||||||
|
3. `AuthBlocksLib/Common/RegistrationEmailTemplate.cs` — the email house style.
|
||||||
|
4. `AuthBlocksWeb/Components/Pages/Account/Register.razor` — the public-page + query-param-prefill pattern
|
||||||
|
for your reset page.
|
||||||
|
5. `AuthBlocksWeb/Components/Pages/UserAdmin/Users/Users.razor` — the stub to replace.
|
||||||
|
6. `AuthBlocksLib/AuthBlocksExtensions.cs` + `AuthBlocksOptions.cs` — the email sender + options wiring you
|
||||||
|
reuse (and where to add a token-provider registration if one is missing).
|
||||||
|
7. `AuthBlocksData/Services/UserService.cs` — the `UserManager<ApplicationUser>` access point for the
|
||||||
|
Identity reset primitives.
|
||||||
@@ -1,18 +1,71 @@
|
|||||||
# Phase 19 — AuthBlocks User Management in the CMS
|
# Phase 19 — AuthBlocks User Management in the CMS
|
||||||
|
|
||||||
Status: proposed. Author: product-designer. Date: 2026-06-19. Implementer: TBD (separate delegation).
|
Status: proposed (rev. 2 — scope expanded by Daniel 2026-06-19). Author: product-designer.
|
||||||
|
Date: 2026-06-19. Implementer: TBD (separate delegation).
|
||||||
|
|
||||||
Wire the AuthBlocks user-administration surface (create users, manage existing accounts, manage
|
Wire the AuthBlocks user-administration surface (create users, manage existing accounts, manage
|
||||||
registration invites, manage role permissions) into the `DeepDrftManager` CMS so an admin can run
|
registration invites, manage role permissions) into the `DeepDrftManager` CMS so an admin can run
|
||||||
account management from inside the same authenticated CMS they already use.
|
account management from inside the same authenticated CMS they already use — **and** stand up the
|
||||||
|
public-facing **self-service registration** form on the `DeepDrftPublic` site so an invited user can
|
||||||
|
redeem a registration code and create their own account.
|
||||||
|
|
||||||
Daniel's framing: *"this is already part of the AuthBlocks library so we just need to wire it up
|
Daniel's framing: *"this is already part of the AuthBlocks library so we just need to wire it up
|
||||||
properly."* **That framing is correct — and the wiring is further along than it implies.** This note's
|
properly."* **That framing is correct for the CMS surface — and the wiring there is further along than
|
||||||
headline finding is that almost the entire integration already landed as a side-effect of the prior
|
it implies.** Almost the entire CMS-side integration already landed as a side-effect of the prior
|
||||||
AuthBlocks startup separation (`PLAN_authblocks_trackmanager.md`, landed 2026-05-25) and the
|
AuthBlocks startup separation (`PLAN_authblocks_trackmanager.md`, landed 2026-05-25) and the
|
||||||
login/logout integration. What remains is a thin **navigation + verification + polish** slice, not an
|
login/logout integration; what remains there is a thin **navigation + verification + polish** slice,
|
||||||
integration project. The spec below separates *what is already done* from *the genuine remaining work*
|
not an integration project.
|
||||||
so the implementer does not redo settled wiring.
|
|
||||||
|
**Scope expansion (Daniel, 2026-06-19).** The original (rev. 1) spec deferred public self-service
|
||||||
|
registration entirely and treated "create user" as a single CMS path. Daniel reversed both: he wants
|
||||||
|
**all three account-creation paths** wired, with each placed on its correct host. Two of the three are
|
||||||
|
CMS-side (and ride the already-done CMS wiring above); the third is **public-facing** and requires a
|
||||||
|
genuine cold-start AuthBlocks integration on `DeepDrftPublic`, which today has **no AuthBlocks
|
||||||
|
reference at all**. The public-registration work is therefore a **distinct track** with its own host,
|
||||||
|
routing, and layout considerations — not part of the CMS nav slice.
|
||||||
|
|
||||||
|
The spec below separates *what is already done* from *the genuine remaining work*, and separates the
|
||||||
|
**CMS track** (waves 19.1–19.3, the original slice) from the new **public-site track** (wave 19.4).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. The three account-creation paths (verified against AuthBlocks source)
|
||||||
|
|
||||||
|
Daniel asked for the registration model to be double-checked against `C:\Development\AuthBlocks`.
|
||||||
|
Verified — his three-path understanding is **correct and complete**. The model:
|
||||||
|
|
||||||
|
| # | Path | Component(s) | Route | Host | Backed by | Email? |
|
||||||
|
|---|------|--------------|-------|------|-----------|--------|
|
||||||
|
| 1 | **Admin provisions a user directly** (bypasses email/code loop) | `SuperRegister.razor` | `/account/superregister` | **CMS** | `POST api/auth/admin-register` (UserAdmin-gated) — **working** | No |
|
||||||
|
| 2 | **Public self-service** — invited user redeems a code and self-registers | `Register.razor` | `/account/register` | **PUBLIC SITE** | `POST api/auth/register` (unauthenticated) — **working** | No (consumes code) |
|
||||||
|
| 3 | **Admin provisions a registration token + triggers the invite email** | `NewRegistration.razor` → `NewRegistrationForm.razor` | `/useradmin/registrations/new` | **CMS** | `POST api/pendingregistration/create` (UserAdmin-gated) — **working, sends email server-side** | **Yes — real, not stubbed** |
|
||||||
|
|
||||||
|
**Path 3's email is real.** This is the headline correction to rev. 1, which worried the
|
||||||
|
token-provisioning path might be stubbed like Reset Password. It is not. `PendingRegistrationRoutes.Create`
|
||||||
|
(`AuthBlocksLib/Routes/PendingRegistrationRoutes.cs:62`) generates a token, persists the pending
|
||||||
|
registration, builds the invite link (`{ReturnHost}?UserEmail=&RegistrationToken=`), renders
|
||||||
|
`RegistrationEmailTemplate.Create(...)`, and **sends it via `IGeneralEmailSender.SendEmailAsync`** —
|
||||||
|
a Mailtrap-backed `MailtrapEmailSender` registered in `AuthBlocksExtensions` (line 109) and configured
|
||||||
|
in **DeepDrftAPI** from `environment/authblocks.json` (`AuthBlocks:Email:Host` / `:Token`,
|
||||||
|
`Program.cs:106–109`; `ApplicationName="DeepDrft"`, `SupportEmail` from config). On email-send failure
|
||||||
|
the route **rolls back** the pending-registration row and returns 500. So the full invite→email→redeem
|
||||||
|
loop is functional end-to-end across paths 2 and 3: an admin provisions (path 3) → the prospective user
|
||||||
|
receives an email with a code + link → they land on the public `/account/register` form (path 2) with
|
||||||
|
email + token pre-filled from the query string → they set a password and the account is created.
|
||||||
|
|
||||||
|
**The one genuinely stubbed surface is Reset Password** — `Users.razor:55` (`// todo integrate with
|
||||||
|
email for secure reset`) has an empty handler and **no backing API endpoint exists** (`AuthRoutes`
|
||||||
|
maps login/register/admin-register/refresh/logout/me/roles — no reset route). That is the subject of
|
||||||
|
the separate `authblocks-password-reset-brief.md`; it must **not** be filed as a DeepDrft bug.
|
||||||
|
|
||||||
|
**Two distinct admin "create" verbs — both stay, they are not duplicates.** `SuperRegister` (path 1)
|
||||||
|
creates a *live account immediately* with a password the admin sets. The registration-token form (path
|
||||||
|
3) creates a *pending invite* — no account yet — and lets the user set their own password via email. They
|
||||||
|
serve different needs (provision-now vs. invite-by-email); both belong in the CMS nav. (Note the older
|
||||||
|
rev. 1 "canonical create-user" question conflated `SuperRegister` with `NewUser` at
|
||||||
|
`/useradmin/users/new` — that `NewUser` `ModelView` create form still exists as a third bare admin
|
||||||
|
create path, but it is **not** one of Daniel's three; treat it as redundant with `SuperRegister` and
|
||||||
|
do not surface it in nav. See OQ2.)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -36,15 +89,26 @@ RCL.
|
|||||||
|
|
||||||
Components under `AuthBlocksWeb/Components/`:
|
Components under `AuthBlocksWeb/Components/`:
|
||||||
|
|
||||||
- **Account pages** (`Pages/Account/`): `Login`, `Logout`, `Register` (self-service via invite code),
|
- **Account pages** (`Pages/Account/`):
|
||||||
`SuperRegister` (admin-creates-account, route `/account/superregister`), `AccessDenied`.
|
- `Login`, `Logout`, `AccessDenied`.
|
||||||
|
- `Register.razor` → `/account/register` (**path 2** — public self-service via invite code; `@rendermode
|
||||||
|
InteractiveServer`; reads `UserEmail` + `RegistrationToken` from the query string and pre-fills, so a
|
||||||
|
deep link from the invite email lands ready to submit; calls `AuthStateProvider.RegisterAsync` →
|
||||||
|
`POST api/auth/register`; **no role gate** — it is meant to be reachable by an unauthenticated visitor).
|
||||||
|
- `SuperRegister.razor` → `/account/superregister` (**path 1** — admin creates a live account
|
||||||
|
immediately, with a role multiselect; gated `[HierarchicalRoleAuthorize(UserAdmin)]`; calls
|
||||||
|
`IAuthApiClient.AdminRegisterAsync` → `POST api/auth/admin-register`).
|
||||||
- **User admin pages** (`Pages/UserAdmin/`), each `@page`-routed and gated
|
- **User admin pages** (`Pages/UserAdmin/`), each `@page`-routed and gated
|
||||||
`[HierarchicalRoleAuthorize(SystemRoleConstants.UserAdmin)]`:
|
`[HierarchicalRoleAuthorize(SystemRoleConstants.UserAdmin)]`:
|
||||||
- `Users/Users.razor` → `/useradmin/users` — searchable user grid; per-row Reset Password
|
- `Users/Users.razor` → `/useradmin/users` — searchable user grid; per-row Reset Password
|
||||||
(stubbed — `// todo integrate with email`), Deactivate/Reactivate, edit modal.
|
(**stubbed — `// todo integrate with email`, no backing endpoint**), Deactivate/Reactivate, edit modal.
|
||||||
- `Users/NewUser.razor` → `/useradmin/users/new` — create-user form.
|
- `Users/NewUser.razor` → `/useradmin/users/new` — bare create-user form (redundant with `SuperRegister`;
|
||||||
|
not one of Daniel's three paths — do not surface in nav).
|
||||||
- `Registrations/Registrations.razor` → `/useradmin/registrations` — pending-invite grid
|
- `Registrations/Registrations.razor` → `/useradmin/registrations` — pending-invite grid
|
||||||
(email, consumed?, dates), new-registration + edit-registration modals.
|
(email, consumed?, dates), with `NewRegistration.razor` → `/useradmin/registrations/new` (**path 3** —
|
||||||
|
`NewRegistrationForm` posts to `PendingRegistrationClient.CreatePendingRegistration` →
|
||||||
|
`POST api/pendingregistration/create`, which mints the token **and sends the invite email**) and the
|
||||||
|
edit-registration modal.
|
||||||
- `Permissions/Permissions.razor` → `/useradmin/permissions` — user↔role assignment.
|
- `Permissions/Permissions.razor` → `/useradmin/permissions` — user↔role assignment.
|
||||||
- **Menu fragments** (`Components/Layout/`): `AccountNavMenu`, `UserAdminMenu` (a `MudNavGroup`
|
- **Menu fragments** (`Components/Layout/`): `AccountNavMenu`, `UserAdminMenu` (a `MudNavGroup`
|
||||||
with the three user-admin `MudNavLink`s, itself wrapped in a `HierarchicalRoleAuthorizeView` so it
|
with the three user-admin `MudNavLink`s, itself wrapped in a `HierarchicalRoleAuthorizeView` so it
|
||||||
@@ -98,7 +162,65 @@ should render and call DeepDrftAPI. The reason it *feels* unbuilt is that **noth
|
|||||||
to these pages** — `CmsLayout` has no nav drawer at all (just an app bar with a Home button), so the
|
to these pages** — `CmsLayout` has no nav drawer at all (just an app bar with a Home button), so the
|
||||||
surface is invisible and unverified.
|
surface is invisible and unverified.
|
||||||
|
|
||||||
This is the crux: the work is not *integration*, it is *exposure + verification + fit-and-finish*.
|
This is the crux: the CMS-side work is not *integration*, it is *exposure + verification + fit-and-finish*.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2b. What is NOT wired on DeepDrftPublic (the public-registration track — genuine cold start)
|
||||||
|
|
||||||
|
The public self-service registration form (path 2) lives in the **same RCL** (`Cerebellum.AuthBlocks.Web`)
|
||||||
|
as the CMS pages. But unlike the CMS, **DeepDrftPublic has no AuthBlocks footprint at all** — verified:
|
||||||
|
|
||||||
|
- **No package reference.** Neither `DeepDrftPublic.csproj` nor `DeepDrftPublic.Client.csproj` references
|
||||||
|
`Cerebellum.AuthBlocks.Web` (or any AuthBlocks package).
|
||||||
|
- **No service wiring.** `DeepDrftPublic/Program.cs` never calls `ConfigureAuthServices` — the
|
||||||
|
`AuthStateProvider` / JWT client stack that `Register.razor` depends on is absent from the container.
|
||||||
|
- **No page discovery.** No `AdditionalAssemblies` entry for the AuthBlocks RCL, so the router cannot
|
||||||
|
reach `/account/register` even if the package were referenced.
|
||||||
|
|
||||||
|
So path 2 is a **from-cold integration on the public site**, not a "flip it on" task. The render-mode
|
||||||
|
substrate, at least, is compatible: DeepDrftPublic is already a Blazor Web App with **both**
|
||||||
|
`AddInteractiveServerComponents` + `AddInteractiveWebAssemblyComponents` and the matching render modes
|
||||||
|
(`Program.cs:33–34, 147–148`), so `Register.razor`'s `@rendermode InteractiveServer` is satisfiable
|
||||||
|
without a render-mode change.
|
||||||
|
|
||||||
|
The public-track integration steps (mirror of §2 items 1–4, but on the public host):
|
||||||
|
|
||||||
|
1. **Package reference** — add `Cerebellum.AuthBlocks.Web` to `DeepDrftPublic.csproj` (the host owns the
|
||||||
|
page discovery + DI; the client assembly need not reference it unless a client-rendered surface is
|
||||||
|
wanted — `Register` is `InteractiveServer`, so server-host wiring suffices).
|
||||||
|
2. **Service wiring** — call `AuthBlocksWeb.Startup.ConfigureAuthServices(builder.Services, contentApiUrl)`
|
||||||
|
in `DeepDrftPublic/Program.cs`, pointed at the same DeepDrftAPI base URL the public site already uses
|
||||||
|
for `api/track/*` (it resolves from `environment/api.json` `Api:ContentApiUrl`). This registers the
|
||||||
|
`AuthStateProvider` and JWT client stack `Register.razor` needs. **Open scope question (OQ6):** this
|
||||||
|
also pulls in the *entire* AuthBlocks client surface (all user-admin clients/VMs) and the cascading
|
||||||
|
auth state — heavier than the public site needs. Acceptable for v1 (it is inert without the gated
|
||||||
|
pages mounted), but worth a conscious "is the public site now auth-aware?" decision (it gains a
|
||||||
|
logged-in concept it did not have).
|
||||||
|
3. **Page discovery** — add the AuthBlocks `_Imports` assembly to `AdditionalAssemblies` on the public
|
||||||
|
site's router (and mirror for endpoint mapping) so `/account/register` is route-reachable. **This also
|
||||||
|
exposes `/account/login`, `/account/superregister`, and the `/useradmin/*` pages on the public host.**
|
||||||
|
The `/useradmin/*` and `/account/superregister` pages self-gate to `UserAdmin` (a public visitor fails
|
||||||
|
the gate → RedirectToLogin/AccessDenied), so they are not a data-exposure risk, but surfacing admin
|
||||||
|
routes on the public origin at all is a posture choice. **Recommendation:** if the framework supports
|
||||||
|
it cleanly, register only the `Register` page (or scope discovery), or accept the gated routes as
|
||||||
|
present-but-unreachable-in-practice. Flag as OQ7.
|
||||||
|
4. **Layout** — `Register.razor` declares no `@layout`, so it inherits the public site's `DefaultLayout`
|
||||||
|
(the full public chrome: player bar, nav, footer). Decide whether self-registration should render in
|
||||||
|
the full public layout or a lean auth layout (mirroring the CMS's `CmsHomeLayout` splash idiom). For
|
||||||
|
v1, the public layout is acceptable; a lean layout is polish. Flag as OQ8.
|
||||||
|
5. **CORS** — DeepDrftAPI's `ContentApiPolicy` must allow the **public site origin** for the `api/auth/*`
|
||||||
|
calls `Register` makes. The public origin is **already** an allowed origin for `api/track/*` (same
|
||||||
|
proxy hop), and the policy is origin-scoped not path-scoped, so this should already be satisfied —
|
||||||
|
but it is a **verification item** (mirror of the CMS's G2 CORS check), not an assumption.
|
||||||
|
6. **Entry point / link** — once the form is reachable, decide whether/where the public site *links* to
|
||||||
|
it. The invite email's deep link lands directly on `/account/register?UserEmail=&RegistrationToken=`,
|
||||||
|
so the form works without any public nav link (the email is the entry point). A visible "Register"
|
||||||
|
link in the public nav is **optional** and arguably unwanted (registration is invite-only — a public
|
||||||
|
"Register" link invites confusion/abuse since there is no self-serve code issuance). **Recommendation:
|
||||||
|
no public nav link; the email deep link is the sole entry point.** Flag as OQ9.
|
||||||
|
|
||||||
|
This is why the public track is its **own wave (19.4)**, parallel to but independent of the CMS nav work.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -128,10 +250,10 @@ Three shapes, meaningfully different (diverge-before-converge):
|
|||||||
dashboard idiom). *Cost:* net-new surface (an admin dashboard) beyond what AuthBlocks ships;
|
dashboard idiom). *Cost:* net-new surface (an admin dashboard) beyond what AuthBlocks ships;
|
||||||
scope creep for v1.
|
scope creep for v1.
|
||||||
|
|
||||||
**Recommendation: G1-b.** It solves the actual gap (no nav) with the least bespoke code, reuses the
|
**DECIDED: G1-b (Daniel, 2026-06-19).** A real `MudDrawer` nav in `CmsLayout` mounting `UserAdminMenu`
|
||||||
shipped `UserAdminMenu`, and is the natural home for the CMS's other destinations too. G1-c's admin
|
+ the existing CMS destinations. It solves the actual gap (no nav) with the least bespoke code, reuses
|
||||||
dashboard is a good *later* idea (note it as deferred), not a v1 gate. G1-a is a stopgap that we'd
|
the shipped `UserAdminMenu`, and is the natural home for the CMS's other destinations too. G1-c's admin
|
||||||
replace with G1-b within a release.
|
dashboard remains deferred (good later idea, not a v1 gate); G1-a is the rejected stopgap.
|
||||||
|
|
||||||
> **Borrowed precedent:** this is the standard MudBlazor admin-template layout (persistent left
|
> **Borrowed precedent:** this is the standard MudBlazor admin-template layout (persistent left
|
||||||
> `MudDrawer` + `MudNavMenu`/`MudNavGroup`), which `UserAdminMenu` is already authored against — it
|
> `MudDrawer` + `MudNavMenu`/`MudNavGroup`), which `UserAdminMenu` is already authored against — it
|
||||||
@@ -144,14 +266,20 @@ Confirm against a running DeepDrftAPI + Auth DB:
|
|||||||
|
|
||||||
- `/useradmin/users` lists users (the `UsersClient` → `api/users/*` round-trip works cross-origin /
|
- `/useradmin/users` lists users (the `UsersClient` → `api/users/*` round-trip works cross-origin /
|
||||||
cross-host, with the bearer token the CMS already holds).
|
cross-host, with the bearer token the CMS already holds).
|
||||||
- `/account/superregister` (or `/useradmin/users/new`) creates a user — `admin-register` is
|
- `/account/superregister` (**path 1**) creates a live account immediately — `admin-register` is
|
||||||
`UserAdmin`-gated server-side and the admin's token must carry the role claim end-to-end.
|
`UserAdmin`-gated server-side and the admin's token must carry the role claim end-to-end.
|
||||||
- `/useradmin/registrations` lists + creates an invite; `/useradmin/permissions` reads + assigns roles.
|
- `/useradmin/registrations/new` (**path 3**) provisions a token **and sends the invite email** — verify
|
||||||
|
the email actually arrives (Mailtrap), the link/code in it are correct, and the rollback fires if the
|
||||||
|
send fails. This is the surface most likely to surface a *config* gap (`AuthBlocks:Email:Host`/`:Token`
|
||||||
|
must be real, not placeholder, in DeepDrftAPI's `environment/authblocks.json`).
|
||||||
|
- `/useradmin/registrations` lists invites; `/useradmin/permissions` reads + assigns roles.
|
||||||
- **CORS / token presentation:** the prior plan widened DeepDrftAPI CORS for the Manager origin for
|
- **CORS / token presentation:** the prior plan widened DeepDrftAPI CORS for the Manager origin for
|
||||||
login; confirm the *same* allowance covers `api/users/*` etc. (it should — same origin, same policy).
|
login; confirm the *same* allowance covers `api/users/*` / `api/pendingregistration/*` etc. (it should
|
||||||
- **Two create paths exist** — `SuperRegister` (`/account/superregister`, role-multiselect, calls
|
— same origin, same policy).
|
||||||
`admin-register`) and `NewUser` (`/useradmin/users/new`, the `ModelView` create form). Decide which
|
- **Two admin create verbs both stay** — `SuperRegister` (path 1, provision-now) and the
|
||||||
is the canonical "create user" entry the nav points at (see OQ2); verify whichever is chosen.
|
registration-token form (path 3, invite-by-email). The bare `NewUser` (`/useradmin/users/new`) is
|
||||||
|
redundant with `SuperRegister` and is **not** surfaced in nav (OQ2). Both nav-surfaced paths are
|
||||||
|
verified.
|
||||||
|
|
||||||
This pass is where any *latent* break surfaces (a client config typo, a missing role claim in the
|
This pass is where any *latent* break surfaces (a client config typo, a missing role claim in the
|
||||||
CMS-issued token, a package-version mismatch). It is real work even though no code may change if it all
|
CMS-issued token, a package-version mismatch). It is real work even though no code may change if it all
|
||||||
@@ -177,24 +305,33 @@ for this phase to function. Note it; let Daniel decide whether to bump in this p
|
|||||||
|
|
||||||
## 4. Scope boundaries
|
## 4. Scope boundaries
|
||||||
|
|
||||||
**In for v1:**
|
**In for v1 (two tracks):**
|
||||||
|
|
||||||
|
*CMS track (waves 19.1–19.3):*
|
||||||
- G1-b: a `MudDrawer` nav in `CmsLayout` mounting `UserAdminMenu` (+ the existing CMS destinations).
|
- G1-b: a `MudDrawer` nav in `CmsLayout` mounting `UserAdminMenu` (+ the existing CMS destinations).
|
||||||
- G2: end-to-end verification of list/create/deactivate users, registrations, permissions.
|
- All three CMS-side account paths surfaced in nav: path 1 (`SuperRegister`, provision-now) and path 3
|
||||||
|
(`/useradmin/registrations/new`, invite-by-email), plus the users/permissions grids.
|
||||||
|
- G2: end-to-end verification of list/create/deactivate users, registrations (incl. the **real invite
|
||||||
|
email** send), permissions.
|
||||||
- G3: accept-the-palette theming; fix only legibility breaks.
|
- G3: accept-the-palette theming; fix only legibility breaks.
|
||||||
- Pick + wire the canonical "create user" entry (OQ2).
|
|
||||||
|
*Public-site track (wave 19.4) — the reversed deferral:*
|
||||||
|
- **Path 2 — public self-service registration** (`/account/register`) wired on **DeepDrftPublic**: package
|
||||||
|
reference + `ConfigureAuthServices` + page discovery + layout + CORS verification (§2b). The invite
|
||||||
|
email's deep link is the entry point.
|
||||||
|
|
||||||
**Deferred (note, don't build):**
|
**Deferred (note, don't build):**
|
||||||
|
|
||||||
- **Admin dashboard (G1-c)** — a user-admin landing summarizing counts / pending invites. Good later;
|
- **Admin dashboard (G1-c)** — a user-admin landing summarizing counts / pending invites. Good later;
|
||||||
not a v1 gate.
|
not a v1 gate.
|
||||||
- **Reset Password** — the AuthBlocks `Users` page stubs it (`// todo integrate with email`). It is an
|
- **Reset Password** — the AuthBlocks `Users` page stubs it (`// todo integrate with email`; **no backing
|
||||||
*upstream AuthBlocks* gap, not a DeepDrft wiring task. If Daniel wants working password reset, that's
|
endpoint exists** in `AuthRoutes`). It is an *upstream AuthBlocks* gap, not a DeepDrft wiring task.
|
||||||
a change in the AuthBlocks repo (a new email-backed reset flow), then a version bump here — a
|
Daniel is handling it as a **separate AuthBlocks-repo effort** with another team — see the standalone
|
||||||
separate effort. **Do not implement password reset inside DeepDrftHome.**
|
`product-notes/authblocks-password-reset-brief.md`. **Do not implement password reset inside
|
||||||
- **Bespoke restyle** of the AuthBlocks grids to the editorial DeepDrft aesthetic.
|
DeepDrftHome.**
|
||||||
- **Self-service public registration** (`/account/register` invite flow) surfaced anywhere on the
|
- **Bespoke restyle** of the AuthBlocks grids to the editorial DeepDrft aesthetic (CMS or public).
|
||||||
*public* site — out of scope; this phase is CMS-admin-only.
|
- A lean public auth layout for `/account/register` (full public chrome is acceptable for v1 — OQ8).
|
||||||
|
- A visible public-nav "Register" link (registration is invite-only; the email deep link suffices — OQ9).
|
||||||
- **G4 version bump** — housekeeping, Daniel's call on timing.
|
- **G4 version bump** — housekeeping, Daniel's call on timing.
|
||||||
|
|
||||||
**Explicitly not needed (the brief's worried-about fork):**
|
**Explicitly not needed (the brief's worried-about fork):**
|
||||||
@@ -206,50 +343,91 @@ for this phase to function. Note it; let Daniel decide whether to bump in this p
|
|||||||
|
|
||||||
## 5. Phased breakdown (for clean dispatch)
|
## 5. Phased breakdown (for clean dispatch)
|
||||||
|
|
||||||
This is a small phase. One real wave, plus verification.
|
**Two tracks.** The CMS track (19.1–19.3) is the original exposure+verify+polish slice. The public-site
|
||||||
|
track (19.4) is a parallel, independent cold-start integration on a different host. They share only the
|
||||||
|
DeepDrftAPI auth surface (already mounted) and can proceed concurrently.
|
||||||
|
|
||||||
- **19.1 — CmsLayout navigation (cold-start, the only code wave).** Add a `MudDrawer` + toggle to
|
### CMS track
|
||||||
|
|
||||||
|
- **19.1 — CmsLayout navigation (cold-start, the only CMS code wave).** Add a `MudDrawer` + toggle to
|
||||||
`CmsLayout.razor`; mount the shipped `UserAdminMenu` fragment (self-gates to `UserAdmin`+) and the
|
`CmsLayout.razor`; mount the shipped `UserAdminMenu` fragment (self-gates to `UserAdmin`+) and the
|
||||||
existing CMS destinations (Catalogue `/catalogue`, Releases `/releases`, Upload `/tracks/upload`).
|
existing CMS destinations (Catalogue `/catalogue`, Releases `/releases`, Upload `/tracks/upload`).
|
||||||
Decide and wire the canonical create-user link (OQ2). Scope: `CmsLayout.razor`
|
Surface **both** admin account paths: path 1 (`SuperRegister`, `/account/superregister`) and path 3
|
||||||
(+ a small `.razor.css` if the drawer needs sizing). **No service, API, data, or AuthBlocks-source
|
(`/useradmin/registrations/new`, reachable via the `UserAdminMenu` Registrations link → its New
|
||||||
change.**
|
button). Do **not** surface the redundant bare `NewUser` (OQ2). Scope: `CmsLayout.razor` (+ a small
|
||||||
|
`.razor.css` if the drawer needs sizing). **No service, API, data, or AuthBlocks-source change.**
|
||||||
- Acceptance: an authenticated `Admin` sees a nav drawer; the User Administration group appears and
|
- Acceptance: an authenticated `Admin` sees a nav drawer; the User Administration group appears and
|
||||||
links to Users / Registrations / Permissions; a non-`UserAdmin` user (if any exist) does not see
|
links to Users / Registrations / Permissions; a "Create user" affordance reaches `SuperRegister`; a
|
||||||
the group; existing CMS destinations are reachable from the same drawer.
|
non-`UserAdmin` user does not see the group; existing CMS destinations are reachable from the drawer.
|
||||||
- **19.2 — End-to-end verification (after 19.1; may surface follow-ups).** Exercise G2 against a
|
- **19.2 — End-to-end verification (after 19.1; may surface follow-ups).** Exercise G2 against a
|
||||||
running DeepDrftAPI. Confirm list/create/deactivate/registration/permission round-trips and
|
running DeepDrftAPI. Confirm list/create/deactivate users, **invite-email send (path 3)**, permission
|
||||||
cross-host token + CORS. File any latent break as a follow-up (likely a one-line config fix, or an
|
round-trips, and cross-host token + CORS. File any latent break as a follow-up (likely a one-line
|
||||||
upstream AuthBlocks issue). **Mostly test, not code.**
|
config fix — esp. the Mailtrap creds — or an upstream AuthBlocks issue). **Mostly test, not code.**
|
||||||
- **19.3 — Theming legibility sweep (after 19.1, parallel-ok with 19.2).** Walk each user-admin page in
|
- **19.3 — Theming legibility sweep (after 19.1, parallel-ok with 19.2).** Walk each user-admin page in
|
||||||
the CMS palette; fix only contrast/legibility breaks. Defer bespoke restyle.
|
the CMS palette; fix only contrast/legibility breaks. Defer bespoke restyle.
|
||||||
|
|
||||||
**Dependency shape:** `19.1 → {19.2, 19.3}`. 19.1 is the only thing that must land first (it makes the
|
### Public-site track
|
||||||
surface reachable to verify and to view). 19.2 and 19.3 fan out behind it.
|
|
||||||
|
- **19.4 — Public self-service registration on DeepDrftPublic (cold-start, parallel to 19.1).** Wire
|
||||||
|
path 2 per §2b: add the `Cerebellum.AuthBlocks.Web` package reference to `DeepDrftPublic`, call
|
||||||
|
`ConfigureAuthServices` in `Program.cs` pointed at the existing DeepDrftAPI base URL, add page
|
||||||
|
discovery so `/account/register` is reachable, settle the layout (OQ8) and route-exposure posture
|
||||||
|
(OQ7), and verify CORS for the public origin. **This is real host-integration code on the public
|
||||||
|
site** (unlike the CMS, where wiring pre-exists) — scope: `DeepDrftPublic.csproj`,
|
||||||
|
`DeepDrftPublic/Program.cs`, the public router, possibly a lean layout. No AuthBlocks-source change.
|
||||||
|
- Acceptance: an invited user clicking the deep link in their registration email lands on
|
||||||
|
`/account/register` with email + token pre-filled, sets a username/password, and the account is
|
||||||
|
created (the row in `pending_registration` is consumed); the form renders coherently in the chosen
|
||||||
|
public layout; an unauthenticated visitor can reach the form (it is not role-gated). The full
|
||||||
|
path-3→path-2 loop (admin provisions in CMS → email arrives → user redeems on public site) works
|
||||||
|
end-to-end.
|
||||||
|
|
||||||
|
**Dependency shape:** `19.1 → {19.2, 19.3}` (CMS track); `19.4` is **independent** and parallel to the
|
||||||
|
CMS track (it touches a different host; its only dependency, the DeepDrftAPI `api/auth/register`
|
||||||
|
endpoint, is already live). The full invite→redeem acceptance test for 19.4 benefits from 19.1+19.2
|
||||||
|
being able to *generate* a real invite, but 19.4 can be built and unit-verified against a token minted
|
||||||
|
directly via the API. Recommended kick-off: 19.1 and 19.4 in parallel.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. Open questions for Daniel
|
## 6. Open questions for Daniel
|
||||||
|
|
||||||
1. **Nav shape (G1).** Confirm **G1-b** (real `MudDrawer` nav mounting `UserAdminMenu` + existing CMS
|
**Resolved (Daniel, 2026-06-19):**
|
||||||
destinations) over G1-a (app-bar overflow, stopgap) or G1-c (drawer + dedicated admin dashboard,
|
|
||||||
more scope). **Recommend G1-b.** This is the load-bearing decision — it sets how much CmsLayout
|
|
||||||
changes.
|
|
||||||
2. **Canonical "create user" entry.** AuthBlocks ships two create paths: `SuperRegister`
|
|
||||||
(`/account/superregister`, role multiselect, calls `admin-register`) and `NewUser`
|
|
||||||
(`/useradmin/users/new`, the `ModelView` create form). Which is the one the nav points at? **Recommend
|
|
||||||
`SuperRegister`** — it has the role-assignment multiselect inline, which is what "create an admin
|
|
||||||
user" actually needs; `NewUser` is the bare create form. (Both can stay route-reachable; this is just
|
|
||||||
which one the menu surfaces.)
|
|
||||||
3. **Admin dashboard (G1-c) — defer or include?** **Recommend defer.** It's net-new surface beyond what
|
|
||||||
AuthBlocks ships; v1 should expose the working pages, not build a new one. Flag if Daniel wants it in
|
|
||||||
scope.
|
|
||||||
4. **Package bump (G4) — now or separate?** Bump `Cerebellum.AuthBlocks.Web` 10.3.33 → 10.3.35 in this
|
|
||||||
pass, or leave it? **Recommend leave it** unless 19.2 surfaces a fix that needs it; keep this phase a
|
|
||||||
pure CMS-side wiring slice.
|
|
||||||
5. **Reset Password expectation.** The Users page's Reset Password is an upstream stub. Confirm Daniel
|
|
||||||
accepts it as **non-functional in v1** (and that working reset is a separate AuthBlocks-repo effort),
|
|
||||||
so the verification pass doesn't get filed as a DeepDrft bug.
|
|
||||||
|
|
||||||
Items 1, 2, and 5 change the shape of the work or the acceptance criteria; 3 and 4 are scope/timing
|
1. **Nav shape (G1) — DECIDED G1-b.** Real `MudDrawer` nav mounting `UserAdminMenu` + existing CMS
|
||||||
calls that don't block 19.1.
|
destinations. Locked.
|
||||||
|
2. **Admin create paths — DECIDED: surface path 1 (`SuperRegister`) + path 3 (registration-token form);
|
||||||
|
do NOT surface the bare `NewUser`.** Both of Daniel's two admin paths stay (they are not duplicates —
|
||||||
|
provision-now vs. invite-by-email); the rev-1 "which single canonical create path" question dissolves
|
||||||
|
because there are legitimately two. `NewUser` is redundant with `SuperRegister` and is hidden from nav.
|
||||||
|
5. **Reset Password — DECIDED: non-functional in v1, handled separately.** Confirmed an upstream
|
||||||
|
AuthBlocks gap (stub + no endpoint), not a DeepDrft bug. Daniel is running it as a separate
|
||||||
|
AuthBlocks-repo effort with another team (see `authblocks-password-reset-brief.md`). The 19.2
|
||||||
|
verification pass must not file it.
|
||||||
|
|
||||||
|
**Still open:**
|
||||||
|
|
||||||
|
3. **Admin dashboard (G1-c) — defer or include?** **Recommend defer.** Net-new surface beyond what
|
||||||
|
AuthBlocks ships; v1 should expose the working pages, not build a new one.
|
||||||
|
4. **Package bump (G4) — now or separate?** Bump `Cerebellum.AuthBlocks.Web` 10.3.33 → 10.3.35 in this
|
||||||
|
pass, or leave it? **Recommend leave it** unless 19.2 surfaces a fix that needs it. Note: if 19.4 adds
|
||||||
|
the package to DeepDrftPublic, pin **both** hosts to the same version to avoid a split-version RCL.
|
||||||
|
6. **Public-site auth footprint (19.4).** Wiring `ConfigureAuthServices` into DeepDrftPublic pulls in the
|
||||||
|
*entire* AuthBlocks client surface + cascading auth state — the public site gains a "logged-in" concept
|
||||||
|
it does not have today. Acceptable for v1 (inert without gated pages mounted), but it is a real posture
|
||||||
|
shift. **Recommend accept it** (it is the supported wiring path; scoping it down is bespoke work for no
|
||||||
|
v1 benefit) — confirm Daniel is comfortable the public site becomes nominally auth-aware.
|
||||||
|
7. **Public route exposure (19.4).** Adding the AuthBlocks RCL to the public router's `AdditionalAssemblies`
|
||||||
|
exposes not just `/account/register` but also `/account/login`, `/account/superregister`, and the
|
||||||
|
`/useradmin/*` routes on the **public origin** (all self-gating to `UserAdmin`, so not a data leak —
|
||||||
|
but admin routes visible on the public host). **Recommend:** accept them as present-but-gated for v1
|
||||||
|
(simplest); narrow discovery to just `Register` later if the exposure bothers us. Flag if Daniel wants
|
||||||
|
the narrow path now.
|
||||||
|
8. **Public-registration layout (19.4).** Render `/account/register` in the **full public chrome**
|
||||||
|
(player bar/nav/footer) or a **lean auth layout** (mirroring the CMS `CmsHomeLayout` splash)?
|
||||||
|
**Recommend full public chrome for v1**, lean layout as polish.
|
||||||
|
9. **Public "Register" nav link (19.4).** Add a visible Register link to the public nav, or rely solely on
|
||||||
|
the invite email's deep link? **Recommend deep-link-only** — registration is invite-only; a public
|
||||||
|
"Register" link with no self-serve code issuance invites confusion/abuse.
|
||||||
|
|
||||||
|
Items 6–9 shape the public-site track (19.4). 3, 4 are CMS scope/timing calls. None block 19.1.
|
||||||
|
|||||||
Reference in New Issue
Block a user