From 54766fd5fc9589f78fd92e0ec246458d9ac99548 Mon Sep 17 00:00:00 2001 From: daniel-c-harvey Date: Fri, 19 Jun 2026 20:46:14 -0400 Subject: [PATCH] docs: correct Phase 19 to CMS-only host model (drop DeepDrftPublic track) All three AuthBlocks account paths live on DeepDrftManager; public registration is an unauthenticated CMS route like the CMS login. Path 2 reduces to a single auth-state-driven DefaultLayout fix (SkipperHaven pattern). --- PLAN.md | 147 +++-- product-notes/phase-19-user-management-cms.md | 573 +++++++++--------- 2 files changed, 382 insertions(+), 338 deletions(-) diff --git a/PLAN.md b/PLAN.md index 4ee556e..cad6899 100644 --- a/PLAN.md +++ b/PLAN.md @@ -380,91 +380,104 @@ opacity + muted-text mixes are tune-on-screen details, not decision gates. --- -## Phase 19 — AuthBlocks User Management (CMS admin + public self-registration) +## Phase 19 — AuthBlocks User Management (CMS-only: admin surfaces + public self-registration) -Wire **all three** AuthBlocks account-creation paths into DeepDrft, each on its correct host: the -CMS-side user-administration surface (provision users, manage accounts, manage registration invites, -manage role permissions) on `DeepDrftManager`, **and** the public-facing self-service registration form -on `DeepDrftPublic`. Daniel's framing: *"already part of the AuthBlocks library so we just wire it up."* -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`. +Wire **all three** AuthBlocks account-creation paths into the `DeepDrftManager` CMS — the admin +user-administration surface (provision users, manage accounts, manage registration invites, manage role +permissions) **and** the public-facing self-service registration form. **All three paths live on +`DeepDrftManager` (the CMS app); there are NO changes to `DeepDrftPublic` in this phase.** Daniel's +framing: *"already part of the AuthBlocks library so we just wire it up."* Correct — and **further along +than it implies**: almost everything landed by side-effect of the prior startup separation. Full design, +the verified three-path model, the already-done-vs-remaining split, the SkipperHaven pattern + concrete +deltas, scope boundaries, and open questions: `product-notes/phase-19-user-management-cms.md`. -**The three account-creation paths (verified against AuthBlocks source 2026-06-19):** +**The three account-creation paths (verified against AuthBlocks source 2026-06-19) — ALL CMS routes:** 1. **Admin provisions directly** — `SuperRegister.razor` → `/account/superregister` → `POST - api/auth/admin-register` (UserAdmin-gated, **working**). Host: **CMS**. Creates a live account now. + api/auth/admin-register` (UserAdmin-gated, **working**). Creates a live account now. 2. **Public self-service** — `Register.razor` → `/account/register` → `POST api/auth/register` - (unauthenticated, **working**). Host: **PUBLIC SITE**. Invited user redeems a code (pre-filled from the - invite email's deep link) and self-registers. + (**unauthenticated, no role gate, working**). A **public-facing CMS route, exactly like the CMS + `/account/login` page** — invited user redeems a code (pre-filled from the invite email's deep link) + and self-registers, all on the CMS host. 3. **Admin provisions a token + triggers the invite email** — `NewRegistration(Form).razor` → - `/useradmin/registrations/new` → `POST api/pendingregistration/create` (UserAdmin-gated). Host: - **CMS**. **Sends a real email server-side** via Mailtrap (`RegistrationEmailTemplate` + - `IGeneralEmailSender`, configured in DeepDrftAPI from `environment/authblocks.json`) — **not stubbed.** + `/useradmin/registrations/new` → `POST api/pendingregistration/create` (UserAdmin-gated). **Sends a + real email server-side** via Mailtrap (`RegistrationEmailTemplate` + `IGeneralEmailSender`, configured + in DeepDrftAPI from `environment/authblocks.json`) — **not stubbed.** -**Scope reversal (Daniel, 2026-06-19).** Rev. 1 deferred public registration and treated "create user" -as one CMS path. Both reversed: all three paths are in scope, and public registration (path 2) is now a -distinct **public-site track**. The only genuinely stubbed surface is **Reset Password** (`Users.razor`, -`// 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`). +**Host-model correction (Daniel, 2026-06-19).** A prior revision placed public registration (path 2) on +`DeepDrftPublic` as a cold-start integration. **Wrong — there are NO `DeepDrftPublic` changes.** Public +registration is an unauthenticated route *on the CMS app*, mirroring the CMS's already-public +`/account/login`. The only genuinely stubbed surface is **Reset Password** (`Users.razor`, `// todo`; **no +backing endpoint** in `AuthRoutes`) — handled separately by Daniel in the AuthBlocks repo (see +`product-notes/authblocks-password-reset-brief.md`). -**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`. +**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 surface +in place on `DeepDrftManager`: `Cerebellum.AuthBlocks.Web` referenced, `ConfigureAuthServices` registers +every client + ViewModel **and** the `JwtAuthenticationStateProvider` path 2 needs, the router discovers +every page (`AdditionalAssemblies`) — **including the public `/account/register`** — and the DeepDrft +`Admin` role **inherits** `UserAdmin` (the seeded admin passes the gate with no change). The pages ship in +a published **RCL**, so the worried-about "extract pages into an RCL" fork **does not arise**. -**Public side — genuine cold start.** `DeepDrftPublic` has **no AuthBlocks footprint at all** (verified: -no package ref, no `ConfigureAuthServices`, no page discovery). Path 2 requires real host integration: -package ref + service wiring + page discovery + layout + CORS verification. The render-mode substrate is -compatible (the public site already has InteractiveServer, which `Register.razor` needs). +**Two real gaps remain.** (a) **No nav** — `CmsLayout` is just an app bar + Home button, so nothing links +to `/useradmin/*` or `/account/superregister` (admin surface invisible). (b) **Wrong layout for public +pages** — `Routes.razor` uses a **static** `DefaultLayout="typeof(CmsLayout)"`, so an unauthenticated +visitor to `/account/register` (or `/account/login`) lands in the authenticated app shell instead of the +lean splash. -**Two parallel tracks.** CMS track `19.1 → {19.2, 19.3}`; public track `19.4` independent and parallel. +**SkipperHaven is the canonical pattern.** `SkipperHaven` (same AuthBlocks library) exposes login + +register as public/unauthenticated routes correctly by making `Routes.razor`'s `DefaultLayout` +**auth-state-driven** — unauthenticated → home/lean layout, authenticated → app shell (resolved in +`OnParametersSetAsync` off the cascaded `AuthenticationState`). **The concrete delta DeepDrftManager +needs is exactly one change** (spec §2c): make its `DefaultLayout` auth-state-driven, resolving +`CmsHomeLayout` (unauth) vs. `CmsLayout` (auth). Everything else SkipperHaven does — service wiring, page +discovery, both layouts — DeepDrftManager **already has** (it even already ships `CmsHomeLayout`, used by +the `/` home splash). So path 2 is **one router edit**, not a host integration. -*CMS track:* -- **19.1 — CmsLayout navigation (cold-start, the only CMS code wave). DECIDED nav shape: G1-b.** Add a +**One host (`DeepDrftManager`), two parallel tracks** (different files), then verify + theme. + +- **19.1 — CmsLayout navigation (admin-nav track; the main 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 - MudBlazor-default grids; fix only contrast/legibility breaks. Bespoke restyle deferred. - -*Public-site track:* -- **19.4 — Public self-service registration on DeepDrftPublic (cold-start, parallel to 19.1).** Wire path - 2: add `Cerebellum.AuthBlocks.Web` to `DeepDrftPublic`, call `ConfigureAuthServices` in `Program.cs` - 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. + redundant bare `NewUser` (OQ2 resolved). Scope: `CmsLayout.razor`. **No service, API, data, or + AuthBlocks-source change.** +- **19.2 — Public-route layout (public-route track; parallel to 19.1). DECIDED: G0-a.** Make + `Routes.razor`'s `DefaultLayout` auth-state-driven (mirroring SkipperHaven, spec §2c D1): cascade + `Task`, resolve `_currentLayout = authed ? CmsLayout : CmsHomeLayout`, bind + `DefaultLayout="@_currentLayout"`. This renders `/account/register` (path 2) **and** `/account/login` in + the lean `CmsHomeLayout` for unauthenticated visitors. Scope: `Routes.razor` only. **No new layout (both + exist), no package, no service, no AuthBlocks-source change.** +- **19.3 — End-to-end verification (after 19.1 + 19.2).** Exercise provision-now (path 1), **invite-email + send (path 3) incl. that the invite link `{ReturnHost}` points at the CMS origin**, list/deactivate + users, permissions against a running DeepDrftAPI; confirm cross-host token + CORS, and **the full + path-3→path-2 loop on the single CMS host** (admin provisions → email arrives → invitee redeems on the + CMS `/account/register` in the lean layout). Mostly test; any break is likely a one-line config fix + (esp. Mailtrap creds + return host) or an upstream AuthBlocks issue. +- **19.4 — Theming legibility sweep (after 19.1 + 19.2, parallel-ok with 19.3).** Accept the CMS palette + for the MudBlazor-default grids and the public pages now in `CmsHomeLayout`; fix only contrast/legibility + breaks. Bespoke restyle deferred. **Deferred (note, don't build):** admin dashboard landing (G1-c); working **Reset Password** (separate -AuthBlocks-repo effort); bespoke restyle of the AuthBlocks grids; a lean public auth layout (OQ8); a -visible public-nav Register link (OQ9 — invite-only, deep-link entry); bumping -`Cerebellum.AuthBlocks.Web` 10.3.33 → 10.3.35 (housekeeping; if 19.4 adds the package to the public host, -pin both hosts to one version). +AuthBlocks-repo effort); bespoke restyle of the AuthBlocks grids; a visible public Register nav link +(invite-only — the email deep link is the entry point); bumping `Cerebellum.AuthBlocks.Web` 10.3.33 → +10.3.35 (housekeeping). -**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. +**Explicitly not needed:** any change to `DeepDrftPublic` (corrected host model — all three paths are CMS); +extracting AuthBlocks pages into a new RCL; new DI/service wiring, role seeding, or Auth connection string +(all present); editing the AuthBlocks `Login`/`Register` pages' layout (impossible without forking the +RCL — G0-a fixes layout host-side instead). + +**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; (6) **host model — all +three on the CMS, no `DeepDrftPublic` changes**; (7) **public-route layout G0-a** (auth-state-driven +`DefaultLayout`, reusing `CmsHomeLayout`). *Still open:* (3) admin dashboard defer (recommend defer); (4) +package bump (recommend leave); (8) a logged-in admin visiting `/account/register` sees it in the app +shell under G0-a (recommend accept). None block 19.1 or 19.2. **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 -management only — same AuthBlocks substrate, different surface. They are not the same work; this phase -does not satisfy or depend on that one. +per-user* identity (favourites, listening history, playlists). This phase is *CMS* account management only +(admin surfaces + invite-based self-registration) — same AuthBlocks substrate, different surface. They are +not the same work; this phase does not satisfy or depend on that one. --- diff --git a/product-notes/phase-19-user-management-cms.md b/product-notes/phase-19-user-management-cms.md index 839b36e..2463a83 100644 --- a/product-notes/phase-19-user-management-cms.md +++ b/product-notes/phase-19-user-management-cms.md @@ -1,57 +1,79 @@ # Phase 19 — AuthBlocks User Management in the CMS -Status: proposed (rev. 2 — scope expanded by Daniel 2026-06-19). Author: product-designer. +Status: proposed (rev. 3 — host model corrected 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 -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 — **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. +Wire **all three** AuthBlocks account-creation paths into the `DeepDrftManager` CMS so an admin can run +account management from inside the same CMS they already use, **and** so an invited user can redeem a +registration code and create their own account — **all on `DeepDrftManager` (the CMS app, +demoapp.deepdrft.com)**. There are **no changes to `DeepDrftPublic` in this phase.** Daniel's framing: *"this is already part of the AuthBlocks library so we just need to wire it up -properly."* **That framing is correct for the CMS surface — and the wiring there is further along than -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 -login/logout integration; what remains there is a thin **navigation + verification + polish** slice, -not an integration project. +properly."* **That framing is correct, and the wiring is further along than it implies.** Almost the +entire integration already landed as a side-effect of the prior AuthBlocks startup separation +(`PLAN_authblocks_trackmanager.md`, landed 2026-05-25) and the login/logout integration; what remains +is a thin **navigation + public-route-exposure + verification + polish** slice, not an integration +project. -**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. +**Host-model correction (Daniel, 2026-06-19 — the crux of this revision).** Rev. 2 placed public +self-service registration (path 2) on `DeepDrftPublic` as a cold-start integration. **That was wrong.** +Public registration belongs on the **CMS app**, exactly where login already lives: the CMS app +*already hosts a public-facing, unauthenticated `/account/login` page* (reachable without being signed +in). The registration redemption page is public-facing **in exactly the same way** — an unauthenticated +route on the CMS app itself. An invited user clicks the email link, lands on the CMS app's public +registration route (`/account/register`), redeems their code, sets a password. **No second host, no +`DeepDrftPublic` involvement.** The entire rev-2 "public-site track" (wave 19.4) and its open questions +(OQ6–OQ9) are **deleted** — they were artifacts of the wrong host assumption. -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). +So all three paths live on `DeepDrftManager`. The real remaining questions for path 2 are narrow: it is +likely already *route-reachable* (the CMS router discovers `/account/register` via `AdditionalAssemblies`, +same as it discovers `/account/login`), so the work is (a) confirming it is correctly **unauthenticated** +(no role gate — verified below, it has none), and (b) giving an unauthenticated visitor the **right +layout** (the lean splash chrome, not the authenticated app shell), mirroring how login should render. +SkipperHaven — another app on the **same AuthBlocks library** — already implements this dual public +login/register pattern, and is the canonical reference (§2c). --- -## 0. The three account-creation paths (verified against AuthBlocks source) +## 0. The three account-creation paths (verified against AuthBlocks source) — ALL on the CMS -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: +Verified against `C:\Development\AuthBlocks` source. Daniel's three-path understanding is **correct and +complete**, and all three are CMS routes: -| # | Path | Component(s) | Route | Host | Backed by | Email? | +| # | Path | Component(s) | Route | Gate | 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** | +| 1 | **Admin provisions a user directly** (bypasses email/code loop) | `SuperRegister.razor` | `/account/superregister` | UserAdmin | `POST api/auth/admin-register` — **working** | No | +| 2 | **Public self-service** — invited user redeems a code and self-registers | `Register.razor` | `/account/register` | **none (public)** | `POST api/auth/register` — **working** | No (consumes code) | +| 3 | **Admin provisions a registration token + triggers the invite email** | `NewRegistration.razor` → `NewRegistrationForm.razor` | `/useradmin/registrations/new` | UserAdmin | `POST api/pendingregistration/create` — **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` +All three are **CMS routes on `DeepDrftManager`.** Paths 1 and 3 are admin-gated (UserAdmin). Path 2 is +**public-facing**, reachable by an unauthenticated visitor — exactly like the CMS `/account/login` page, +which is also unauthenticated and on the same host. + +**Path 2 has no role gate (verified).** `Register.razor` declares `@page "/account/register"` + +`@rendermode InteractiveServer` and **no** `[HierarchicalRoleAuthorize]` attribute — identical in this +respect to `Login.razor` (`@page "/account/login"`, no gate). It reads `UserEmail` + `RegistrationToken` +from the query string and pre-fills, so the invite email's deep link lands ready to submit; it calls +`AuthStateProvider.RegisterAsync` → `POST api/auth/register`. It is meant to be reached by an +unauthenticated visitor. + +**Path 3's email is real.** `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 route **rolls back** the pending-registration row and returns 500. The full invite→email→redeem +loop is functional end-to-end across paths 2 and 3, **entirely within the CMS host**: an admin +provisions (path 3, CMS) → the prospective user receives an email with a code + link → they land on the +CMS app's public `/account/register` (path 2, CMS) with email + token pre-filled → they set a password +and the account is created. + +> **Note on `{ReturnHost}`.** The invite email's deep link is built from a configured return host. For +> the loop to land on the CMS app, that host must point at the CMS origin (demoapp.deepdrft.com), not the +> public site. Verify this config value in 19.3 (it is the one place the wrong-host assumption could be +> baked into a *config* rather than code). **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` @@ -61,75 +83,67 @@ the separate `authblocks-password-reset-brief.md`; it must **not** be filed as a **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.) +serve different needs (provision-now vs. invite-by-email); both belong in the CMS nav. (The older +`NewUser` `ModelView` create form at `/useradmin/users/new` 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.) --- ## 1. What AuthBlocks ships, and how it is packaged -Read from local source at `C:\Development\AuthBlocks` (not public docs). The key question the brief -raised — *is the user-admin surface consumable or host-bound?* — resolves cleanly: **it is -consumable.** +Read from local source at `C:\Development\AuthBlocks`. The key question — *is the user-admin surface +consumable or host-bound?* — resolves cleanly: **it is consumable.** ### The user-admin surface is a published RCL, despite the "Web" name `AuthBlocksWeb` is an `Microsoft.NET.Sdk.Razor` project (not `Sdk.Web`) with **no `Program.cs`** — it is a Razor Class Library, not a runnable host. `pack.ps1` packs it as **`Cerebellum.AuthBlocks.Web`** -and pushes it to nuget.org alongside the other four packages. So the user-admin Razor components are -distributed as a normal RCL and are consumed by reference, exactly like any MudBlazor-based component -package. **No extraction fork is needed** — the architectural risk the brief flagged ("if the pages -are host-bound and need extracting into an RCL") does not materialize. The pages are already in the -RCL. +and pushes it to nuget.org. So the user-admin Razor components are distributed as a normal RCL and +consumed by reference. **No extraction fork is needed** — the pages are already in the RCL. ### What's in the package (the consumable surface) Components under `AuthBlocksWeb/Components/`: - **Account pages** (`Pages/Account/`): - - `Login`, `Logout`, `AccessDenied`. + - `Login.razor` → `/account/login` (**public — no gate, no `@layout`**; `@rendermode InteractiveServer`). - `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). + InteractiveServer`; **no role gate, no `@layout`**; reads `UserEmail` + `RegistrationToken` from the + query string and pre-fills; calls `AuthStateProvider.RegisterAsync` → `POST api/auth/register`). + - `Logout`, `AccessDenied`. - `SuperRegister.razor` → `/account/superregister` (**path 1** — admin creates a live account - immediately, with a role multiselect; gated `[HierarchicalRoleAuthorize(UserAdmin)]`; calls + immediately, role multiselect; gated `[HierarchicalRoleAuthorize(UserAdmin)]`; calls `IAuthApiClient.AdminRegisterAsync` → `POST api/auth/admin-register`). - **User admin pages** (`Pages/UserAdmin/`), each `@page`-routed and gated `[HierarchicalRoleAuthorize(SystemRoleConstants.UserAdmin)]`: - `Users/Users.razor` → `/useradmin/users` — searchable user grid; per-row Reset Password - (**stubbed — `// todo integrate with email`, no backing endpoint**), Deactivate/Reactivate, edit modal. + (**stubbed — no backing endpoint**), Deactivate/Reactivate, edit modal. - `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 - (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. + - `Registrations/Registrations.razor` → `/useradmin/registrations` — pending-invite grid, 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. - **Menu fragments** (`Components/Layout/`): `AccountNavMenu`, `UserAdminMenu` (a `MudNavGroup` with the three user-admin `MudNavLink`s, itself wrapped in a `HierarchicalRoleAuthorizeView` so it only renders for `UserAdmin`+). - **Shared** (`Components/Shared/`): `LogoutButton`, `StatusMessage`. - **DI entry point** (`Startup.cs`): `ConfigureAuthServices(IServiceCollection, string apiBaseUrl)` - registers the cascading auth state, the JWT client stack, **and every user-admin client + ViewModel** - (`UsersClient`/`UsersViewModel`, `RoleClient`, `UserRolesClient`/`PermissionsViewModel`, - `PendingRegistrationClient`/`RegistrationsViewModel`), all pointed at `apiBaseUrl`. + registers the cascading auth state, the JWT client stack, **and every user-admin client + ViewModel**, + all pointed at `apiBaseUrl`. -The pages lean on `Cerebellum.BlazorBlocks.Web` for their grid scaffolding (`ModelView`, -`ModelPageViewModel`, `ConfirmCancelModal`) and MudBlazor for chrome — both already present in the CMS. +The pages lean on `Cerebellum.BlazorBlocks.Web` for grid scaffolding and MudBlazor for chrome — both +already present in the CMS. ### The API side is already hosted The clients those ViewModels use call the AuthBlocks **API** surface, which `DeepDrftAPI` already -mounts via `app.MapAuthBlocks()` (`Program.cs:184`): `api/auth/*` (incl. `admin-register`, gated -`UserAdmin`; and `roles`), `api/users/*`, `api/roles/*`, `api/user-roles/*`, -`api/pending-registration/*`. `AddAuthBlocks` + `UseAuthBlocksStartupAsync` (migrate + seed) are wired, -and the Auth DB + secrets live in `DeepDrftAPI/environment/`. This all landed with the startup -separation. +mounts via `app.MapAuthBlocks()` (`Program.cs:184`): `api/auth/*` (incl. `admin-register`, `register`, +`roles`), `api/users/*`, `api/roles/*`, `api/user-roles/*`, `api/pending-registration/*`. `AddAuthBlocks` ++ `UseAuthBlocksStartupAsync` (migrate + seed) are wired, and the Auth DB + secrets live in +`DeepDrftAPI/environment/`. This all landed with the startup separation. --- @@ -141,252 +155,279 @@ would propose — and they are **already done**: 1. **Package reference.** `DeepDrftManager.csproj:11` references `Cerebellum.AuthBlocks.Web` (10.3.33), which transitively brings `AuthBlocksWeb.Client`, `AuthBlocksLib`, `AuthBlocksModels`. 2. **Service wiring.** `Program.cs:35` calls - `AuthBlocksWeb.Startup.ConfigureAuthServices(builder.Services, contentApiUrl)` — so the user-admin - clients and ViewModels are **already in the container**, already pointed at DeepDrftAPI. + `AuthBlocksWeb.Startup.ConfigureAuthServices(builder.Services, contentApiUrl)` — the user-admin + clients and ViewModels are **already in the container**, already pointed at DeepDrftAPI. **This same + wiring also registers the `JwtAuthenticationStateProvider` that `Register.razor` (path 2) depends on** + — so path 2's service dependency is already satisfied (it is the same provider login uses). 3. **Page discovery.** `Routes.razor:2` sets `AdditionalAssemblies="new[] { typeof(AuthBlocksWeb._Imports).Assembly }"` and `Program.cs:131` mirrors it for endpoint mapping. **The Blazor router already discovers every AuthBlocksWeb page**, - including `/useradmin/users`, `/useradmin/registrations`, `/useradmin/permissions`, - `/useradmin/users/new`, `/account/superregister`. They are route-reachable *today* by typing the URL. + including `/account/login`, `/account/register`, `/account/superregister`, and the `/useradmin/*` + pages. They are route-reachable *today* by typing the URL — **including the public `/account/register`.** 4. **Default layout.** `Routes.razor:6` sets `DefaultLayout="typeof(Layout.CmsLayout)"`. Since the - AuthBlocks pages declare no `@layout`, they **already render inside CmsLayout chrome.** -5. **Role gating already satisfied.** The pages gate on `SystemRoleConstants.UserAdmin`. The DeepDrft - admin is seeded in role **`Admin`**, and `SystemRole` (id 1, `Admin`) is the **parent** of - `UserAdmin` (id 2) — `Admin.InheritsFrom`/hierarchical authorize means **the existing admin already - passes the `UserAdmin` gate** with no role change, no new seed, no DB edit. + AuthBlocks pages declare no `@layout`, they **render inside CmsLayout chrome** — the authenticated app + shell. **This is the one wrong thing for the public pages** (login, register): an unauthenticated + visitor sees the full authenticated CMS shell rather than the lean splash. See §2b. +5. **Role gating already satisfied.** The admin pages gate on `SystemRoleConstants.UserAdmin`. The + DeepDrft admin is seeded in role **`Admin`**, parent of `UserAdmin` — hierarchical authorize means + **the existing admin already passes the `UserAdmin` gate** with no role change, no new seed, no DB edit. 6. **Auth-state + redirect plumbing.** `AuthorizeRouteView` with `RedirectToLogin` / - `RedirectToAccessDenied` (`Routes.razor`) already protects the surface coherently. + `RedirectToAccessDenied` (`Routes.razor`) already protects the gated surface coherently, and the + public pages (no gate) pass straight through it. -**Net:** an authenticated DeepDrft admin can, right now, navigate to `/useradmin/users` and the page -should render and call DeepDrftAPI. The reason it *feels* unbuilt is that **nothing in the CMS UI links -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. +**Net:** an authenticated DeepDrft admin can navigate to `/useradmin/users` today and the page should +render and call DeepDrftAPI; and an unauthenticated visitor can reach `/account/register` today. The +reasons it *feels* unbuilt: (a) **nothing in the CMS UI links to the admin pages** — `CmsLayout` has no +nav drawer at all, so the admin surface is invisible (§G1); and (b) **the public pages render in the +wrong (authenticated-shell) layout** for an unauthenticated visitor (§G0/§2b). -This is the crux: the CMS-side work is not *integration*, it is *exposure + verification + fit-and-finish*. +This is the crux: the CMS work is not *integration*, it is *exposure + layout-fix + verification + +fit-and-finish*. --- -## 2b. What is NOT wired on DeepDrftPublic (the public-registration track — genuine cold start) +## 2b. The public-route layout gap (path 2 + login) — the one real public-facing fix -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: +The public pages — `/account/login` and `/account/register` — are route-reachable and unauthenticated, +but DeepDrftManager's router uses a **static** `DefaultLayout="typeof(Layout.CmsLayout)"`. Because the +AuthBlocks public pages declare no `@layout`, an **unauthenticated visitor** lands inside the +**authenticated app shell** (`CmsLayout` — the dense admin app bar with a Catalogue/Home button, and +soon a nav drawer linking to gated admin surfaces). That is the wrong frame: a visitor who is not +signed in should see the lean splash chrome the site already uses for its `/` home splash +(`CmsHomeLayout`), not the admin shell. -- **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. +DeepDrftManager **already has both layouts**: +- `CmsLayout` — the authenticated app shell (`MudThemeProvider` + app bar + main content; gains the nav + drawer in 19.1). +- `CmsHomeLayout` — the lean splash (`MudThemeProvider` + minimal app bar, centered narrow container), + already used by `Home.razor` (`@layout Layout.CmsHomeLayout`) for the unauthenticated `/` splash. -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 fix is to render the **public auth pages in the lean layout** for unauthenticated visitors, and the +**gated pages in the app shell** — exactly the SkipperHaven pattern (§2c). The two clean shapes: -The public-track integration steps (mirror of §2 items 1–4, but on the public host): +- **G0-a — Auth-state-driven `DefaultLayout` in `Routes.razor` (the SkipperHaven pattern; recommended).** + Make the router's `DefaultLayout` a function of auth state: unauthenticated → `CmsHomeLayout`, + authenticated → `CmsLayout`. This is exactly what SkipperHaven does (its `Routes.razor` swaps + `AuthenticatedLayout`/`UnauthenticatedLayout` in `OnParametersSetAsync` off the cascaded + `AuthenticationState`). **DeepDrftManager already has both target layouts**, so this is a small + router change, no new layout to author. *Cost:* the gated admin pages also resolve their layout via + this switch — but an unauthenticated visitor to a gated page is redirected to login by `NotAuthorized` + before layout matters, and an authenticated admin gets `CmsLayout`, so it composes correctly. + *Caveat:* a logged-in admin who visits `/account/register` would see it in `CmsLayout` (the app shell) + — acceptable, and arguably correct (an admin poking at the public form is in an admin session). +- **G0-b — Per-page `@layout` on the public pages.** Add `@layout CmsHomeLayout` to the AuthBlocks + public pages. **Rejected — not possible without forking the RCL:** `Login.razor`/`Register.razor` ship + inside `Cerebellum.AuthBlocks.Web`; we cannot edit them, and there is no host-side override for an RCL + page's `@layout`. G0-a is the only no-fork path. -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. +**DECIDED direction: G0-a** (auth-state-driven `DefaultLayout`), mirroring SkipperHaven. It is the +supported, no-fork way to give the public auth pages the lean layout, it reuses the two layouts +DeepDrftManager already has, and it fixes login's layout at the same time as registration's. -This is why the public track is its **own wave (19.4)**, parallel to but independent of the CMS nav work. +--- + +## 2c. SkipperHaven — the canonical pattern, and the concrete DeepDrftManager deltas + +`SkipperHaven` (`C:\Development\skipper\SkipperHaven\SkipperHaven`) consumes the **same AuthBlocks +library** and already exposes login + register as public/unauthenticated routes with the right layout. +The load-bearing piece is its `Components/Routes.razor`: + +- It declares **`[Parameter] AuthenticatedLayout`** (`MainApplicationLayout`) and **`[Parameter] + UnauthenticatedLayout`** (`MainHomeLayout`), takes the **cascaded `Task`**, and in + `OnParametersSetAsync` sets `_currentLayout` to the authenticated layout iff + `authState.User.Identity?.IsAuthenticated == true`, else the unauthenticated layout. +- `AuthorizeRouteView` uses **`DefaultLayout="@_currentLayout"`** (the resolved switch), **not** a static + type. So the AuthBlocks public pages (login, register — both layout-less) render in `MainHomeLayout` + for a signed-out visitor and the app shell once signed in. +- Its `NotAuthorized` renders `RedirectToLogin` for unauthenticated and an inline "not authorized" for + authenticated-but-unprivileged. +- Wiring is otherwise identical to DeepDrftManager: `AuthBlocksWeb.Startup.ConfigureAuthServices(..., + apiBaseUrl)` in `Program.cs`, and `AddAdditionalAssemblies(typeof(AuthBlocksWeb._Imports).Assembly)` on + the mapped components. (Skipper also adds `AuthBlocksWeb.Client` assemblies because it uses the + client-rendered auth surface; **DeepDrftManager is server-rendered `InteractiveServer` and does not need + the `.Client` assembly** — its single `AuthBlocksWeb._Imports` entry is sufficient.) + +**Concrete deltas DeepDrftManager needs to match the pattern (this is the whole public-route slice):** + +| # | SkipperHaven | DeepDrftManager today | Delta | +|---|--------------|-----------------------|-------| +| D1 | `Routes.razor` resolves `DefaultLayout` from auth state (`AuthenticatedLayout` / `UnauthenticatedLayout`, switched in `OnParametersSetAsync` off the cascaded `AuthenticationState`) | `Routes.razor` uses a **static** `DefaultLayout="typeof(Layout.CmsLayout)"` | **Make `DefaultLayout` auth-state-driven**: cascade `Task`, resolve `_currentLayout` = authed ? `CmsLayout` : `CmsHomeLayout`, bind `DefaultLayout="@_currentLayout"`. (G0-a.) **The only required public-route code change.** | +| D2 | Two layouts exist (`MainApplicationLayout`, `MainHomeLayout`) | **Already has both** (`CmsLayout`, `CmsHomeLayout`) | **None** — no new layout to author. DeepDrftManager is ahead of Skipper here. | +| D3 | `ConfigureAuthServices(..., apiBaseUrl)` in `Program.cs` (registers `JwtAuthenticationStateProvider` etc.) | **Already wired** (`Program.cs:35`) | **None.** | +| D4 | AuthBlocks `_Imports` in router `AdditionalAssemblies` + mapped components | **Already wired** (`Routes.razor:2`, `Program.cs:131`) | **None** — `/account/register` is already route-reachable. | +| D5 | `NotAuthorized` → `RedirectToLogin` (unauth) / inline message (authed) | `NotAuthorized` → `RedirectToLogin` (unauth) / `RedirectToAccessDenied` (authed) | **None functionally** — DeepDrftManager's existing handling is equivalent (it redirects rather than inlines; fine). | + +**So the public-registration "track" reduces to a single change: D1 (auth-state-driven `DefaultLayout`).** +Everything else SkipperHaven does is already present in DeepDrftManager. This is why path 2 is no longer +its own host track — it is one router edit, parallelizable with (and smaller than) the admin-nav work. --- ## 3. The genuine remaining work -### G1 — Navigation: there is no way to reach the surface from the UI *(the real gap)* +### G0 — Public-route layout (the path-2 + login fix) — see §2b/§2c + +Make `Routes.razor`'s `DefaultLayout` auth-state-driven (G0-a), so the public `/account/login` and +`/account/register` pages render in `CmsHomeLayout` for unauthenticated visitors. Single router change; +no new layout (both already exist); no AuthBlocks-source change. This is **independent** of the admin-nav +work below and can run in parallel. + +### G1 — Navigation: there is no way to reach the admin surface from the UI *(the real admin gap)* `CmsLayout.razor` is an app bar + a single Home `MudIconButton` — **no `MudDrawer`, no nav menu.** The -catalogue, releases, upload, and now user-admin surfaces are all reachable only by typed URL or -in-page buttons. Mounting `UserAdminMenu` requires a navigation container to mount it *into*. +catalogue, releases, upload, and user-admin surfaces are all reachable only by typed URL or in-page +buttons. Mounting `UserAdminMenu` requires a navigation container to mount it *into*. -Three shapes, meaningfully different (diverge-before-converge): +Three shapes were considered (diverge-before-converge): G1-a app-bar overflow menu (doesn't scale); +**G1-b a real `MudDrawer` nav** mounting the existing CMS destinations + the shipped `UserAdminMenu` +fragment; G1-c a maximal dedicated Administration section with its own dashboard (scope creep for v1). -- **G1-a — Minimal: app-bar overflow menu.** Add a `MudMenu` (or a few `MudIconButton`s) to the - existing app bar with links to the three user-admin routes (+ SuperRegister). Smallest change; - keeps CmsLayout's current spare aesthetic. *Cost:* doesn't scale — the CMS already has - catalogue/releases/upload that arguably belong in a real nav too, and an overflow menu gets - crowded. -- **G1-b — Recommended: a real `MudDrawer` nav in CmsLayout.** Add a left drawer (toggle in the app - bar) holding the existing primary destinations (Catalogue, Releases, Upload) **and** the shipped - `UserAdminMenu` fragment (which self-gates to `UserAdmin`+). This is the idiomatic Blazor/MudBlazor - CMS shape, it reuses AuthBlocks' own menu component verbatim, and it gives the CMS the navigation - spine it's currently missing. *Cost:* slightly larger CmsLayout change; a small visual-design pass - on the drawer. -- **G1-c — Maximal: dedicated "Administration" section.** A drawer *plus* a distinct admin sub-area - (its own landing page summarizing user counts / pending registrations, mirroring the catalogue - dashboard idiom). *Cost:* net-new surface (an admin dashboard) beyond what AuthBlocks ships; - scope creep for v1. +**DECIDED: G1-b (Daniel, 2026-06-19).** A real `MudDrawer` nav in `CmsLayout` (toggle in the app bar) +holding the existing primary destinations (Catalogue `/catalogue`, Releases `/releases`, Upload +`/tracks/upload`) **and** the shipped `UserAdminMenu` fragment (self-gates to `UserAdmin`+, so it only +shows for admins). Surface **both** admin account paths: path 1 (`SuperRegister`, +`/account/superregister`) and path 3 (via the `UserAdminMenu` Registrations link → its New button). Do +**not** surface the redundant bare `NewUser` (OQ2). It solves the actual gap (no nav) with the least +bespoke code, reuses AuthBlocks' own `MudNavGroup` component verbatim, and gives the CMS the navigation +spine it's missing. G1-c's admin dashboard remains deferred; G1-a is the rejected stopgap. -**DECIDED: G1-b (Daniel, 2026-06-19).** A real `MudDrawer` nav in `CmsLayout` mounting `UserAdminMenu` -+ the existing CMS destinations. It solves the actual gap (no nav) with the least bespoke code, reuses -the shipped `UserAdminMenu`, and is the natural home for the CMS's other destinations too. G1-c's admin -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 -> `MudDrawer` + `MudNavMenu`/`MudNavGroup`), which `UserAdminMenu` is already authored against — it -> *is* a `MudNavGroup`. We are adopting the pattern the component was built for, not inventing one. +> **Borrowed precedent:** the standard MudBlazor admin-template layout (persistent left `MudDrawer` + +> `MudNavMenu`/`MudNavGroup`), which `UserAdminMenu` is already authored against. SkipperHaven's +> `MainApplicationLayout`/`NavMenu` is the same shape on the same library — a second confirmation this is +> the idiom, not an invention. ### G2 — Verification pass (the surface is wired but unproven end-to-end) Because nothing exercised these pages in the CMS, treat first-light as verification, not assumption. Confirm against a running DeepDrftAPI + Auth DB: -- `/useradmin/users` lists users (the `UsersClient` → `api/users/*` round-trip works cross-origin / - cross-host, with the bearer token the CMS already holds). +- `/account/register` (**path 2**) renders for an **unauthenticated** visitor in the **lean + `CmsHomeLayout`** (post-G0), pre-fills `UserEmail` + `RegistrationToken` from the query string, and + creates the account (consuming the `pending_registration` row) on submit. +- `/account/login` likewise renders in the lean layout for an unauthenticated visitor (G0 fixes login's + layout as a side benefit). +- `/useradmin/users` lists users (the `UsersClient` → `api/users/*` round-trip works cross-host with the + bearer token the CMS holds). - `/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; the admin's token must carry the role claim end-to-end. - `/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`). + the email arrives (Mailtrap), the link/code are correct, the rollback fires on send failure, and + **critically that the invite link's `{ReturnHost}` points at the CMS origin** so the deep link lands on + the CMS `/account/register` (the place the wrong-host assumption could hide as config — §0 note). This + is the surface most likely to surface a *config* gap (`AuthBlocks:Email:Host`/`:Token` + the return + host in DeepDrftAPI's `environment/authblocks.json`). +- **The full path-3→path-2 loop on one host:** admin provisions in the CMS → email arrives → invited user + opens the deep link → lands on the CMS `/account/register` (lean layout) → redeems → account created. - `/useradmin/registrations` lists invites; `/useradmin/permissions` reads + assigns roles. - **CORS / token presentation:** the prior plan widened DeepDrftAPI CORS for the Manager origin for - login; confirm the *same* allowance covers `api/users/*` / `api/pendingregistration/*` etc. (it should - — same origin, same policy). -- **Two admin create verbs both stay** — `SuperRegister` (path 1, provision-now) and the - 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. + login; confirm the *same* allowance covers `api/users/*` / `api/pendingregistration/*` / `api/auth/register` + (it should — same origin, same policy). -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 -passes. +This pass is where any *latent* break surfaces (a client config typo, a missing role claim, a wrong +return host, a package-version mismatch). Real work even though no code may change if it all passes. ### G3 — Theming / fit-and-finish -The AuthBlocks pages are MudBlazor-default-styled and were authored against AuthBlocks' own theme, not -the DeepDrft CMS palette (`DeepDrftPalettes.Cms`, mounted in CmsLayout with `IsDarkMode="false"`). -Expect minor visual seams: the AuthBlocks `ThemeColorDemo`/MudBlazor defaults vs. the CMS's DM-Sans / -charleston palette. Scope for v1: **accept MudBlazor-default styling inside the CMS palette** (the -`MudThemeProvider` in CmsLayout already themes Mud components, so the pages inherit the CMS palette for -free) and only fix outright legibility/contrast breaks. A deeper bespoke restyle of the AuthBlocks -grids is explicitly **out of v1** — flag as deferred polish. +The AuthBlocks pages are MudBlazor-default-styled, authored against AuthBlocks' own theme, not the +DeepDrft CMS palette (`DeepDrftPalettes.Cms`). Both `CmsLayout` and `CmsHomeLayout` mount a +`MudThemeProvider` with that palette, so the pages inherit it for free. Scope for v1: **accept +MudBlazor-default styling inside the CMS palette** and only fix outright legibility/contrast breaks +(especially the public `/account/register` + `/account/login` now rendering in `CmsHomeLayout`). A deeper +bespoke restyle of the AuthBlocks grids is explicitly **out of v1** — deferred polish. ### G4 — Package version alignment *(housekeeping, flag don't gate)* DeepDrftManager references `Cerebellum.AuthBlocks.Web` **10.3.33**; AuthBlocks source is at **10.3.35**. -Minor lag. Bumping to 10.3.35 is low-risk and gets the latest user-admin fixes, but is **not required** -for this phase to function. Note it; let Daniel decide whether to bump in this pass or separately. +Minor lag. Bumping is low-risk but **not required** for this phase. Note it; Daniel's call on timing. --- ## 4. Scope boundaries -**In for v1 (two tracks):** +**In for v1 (one host — `DeepDrftManager` — two parallel tracks):** -*CMS track (waves 19.1–19.3):* +*Admin-nav track:* - G1-b: a `MudDrawer` nav in `CmsLayout` mounting `UserAdminMenu` (+ the existing CMS destinations). -- 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. +- All three account paths reachable in the CMS: path 1 (`SuperRegister`, provision-now) and path 3 + (`/useradmin/registrations/new`, invite-by-email) via nav, plus the users/permissions grids; path 2 + (`/account/register`) via the public-route track below. -*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. +*Public-route track (the corrected, much smaller path-2 work):* +- G0-a: auth-state-driven `DefaultLayout` in `Routes.razor` so `/account/register` (path 2) **and** + `/account/login` render in the lean `CmsHomeLayout` for unauthenticated visitors. **One router edit** + (§2c D1); both target layouts already exist. The invite email's deep link is the entry point. + +*Shared:* +- G2: end-to-end verification of list/create/deactivate users, registrations (incl. the **real invite + email** send + correct return host), permissions, **and the path-3→path-2 loop on one host**. +- G3: accept-the-palette theming; fix only legibility breaks (incl. the public pages in `CmsHomeLayout`). **Deferred (note, don't build):** - **Admin dashboard (G1-c)** — a user-admin landing summarizing counts / pending invites. Good later; not a v1 gate. -- **Reset Password** — the AuthBlocks `Users` page stubs it (`// todo integrate with email`; **no backing - endpoint exists** in `AuthRoutes`). It is an *upstream AuthBlocks* gap, not a DeepDrft wiring task. - Daniel is handling it as a **separate AuthBlocks-repo effort** with another team — see the standalone - `product-notes/authblocks-password-reset-brief.md`. **Do not implement password reset inside - DeepDrftHome.** -- **Bespoke restyle** of the AuthBlocks grids to the editorial DeepDrft aesthetic (CMS or public). -- 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). +- **Reset Password** — the AuthBlocks `Users` page stubs it; **no backing endpoint exists** in + `AuthRoutes`. An *upstream AuthBlocks* gap, not a DeepDrft wiring task. Daniel is handling it as a + **separate AuthBlocks-repo effort** — see the standalone `product-notes/authblocks-password-reset-brief.md`. + **Do not implement password reset inside DeepDrftHome.** +- **Bespoke restyle** of the AuthBlocks grids to the editorial DeepDrft aesthetic. +- A visible public "Register" nav link. Registration is invite-only (the email deep link is the entry + point); a visible Register link with no self-serve code issuance invites confusion/abuse. + **Recommend: no nav link; deep link only.** (Carried over from the dropped OQ9 — still the right call, + now trivially so since the form lives on the CMS host the admin already knows.) - **G4 version bump** — housekeeping, Daniel's call on timing. -**Explicitly not needed (the brief's worried-about fork):** +**Explicitly not needed:** -- Extracting AuthBlocks pages into a new RCL. They already ship in `Cerebellum.AuthBlocks.Web`. -- New DI/service wiring, new routing, new role seeding, new Auth connection string. All present. +- **Any change to `DeepDrftPublic`.** The corrected host model puts all three paths on the CMS. The public + site is untouched. (This deletes the entire rev-2 cold-start track.) +- Extracting AuthBlocks pages into a new RCL. They ship in `Cerebellum.AuthBlocks.Web`. +- New DI/service wiring, new role seeding, new Auth connection string. All present. +- Editing the AuthBlocks `Login`/`Register` pages to set their layout — impossible without forking the + RCL, and unnecessary (G0-a handles layout host-side). --- ## 5. Phased breakdown (for clean dispatch) -**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. +**One host (`DeepDrftManager`), two parallel tracks.** The admin-nav track (19.1) exposes the gated +admin surfaces; the public-route track (19.2) fixes the public auth pages' layout. They touch different +files (`CmsLayout.razor` vs. `Routes.razor`) and are independent — kick both off together. Verification +(19.3) follows both; theming (19.4) follows and is parallel-ok with verification. -### 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 - existing CMS destinations (Catalogue `/catalogue`, Releases `/releases`, Upload `/tracks/upload`). - Surface **both** admin account paths: path 1 (`SuperRegister`, `/account/superregister`) and path 3 - (`/useradmin/registrations/new`, reachable via the `UserAdminMenu` Registrations link → its New - 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.** +- **19.1 — CmsLayout navigation (admin-nav track; the main CMS code wave). DECIDED nav shape: G1-b.** + Add a `MudDrawer` + toggle to `CmsLayout.razor`; mount the shipped `UserAdminMenu` fragment + (self-gates to `UserAdmin`+) and the existing CMS destinations (Catalogue `/catalogue`, Releases + `/releases`, Upload `/tracks/upload`). Surface **both** admin account paths: path 1 (`SuperRegister`, + `/account/superregister`) and path 3 (`/useradmin/registrations/new`, via the `UserAdminMenu` + Registrations link → its New 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 links to Users / Registrations / Permissions; a "Create user" affordance reaches `SuperRegister`; a 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 - running DeepDrftAPI. Confirm list/create/deactivate users, **invite-email send (path 3)**, permission - round-trips, and cross-host token + CORS. File any latent break as a follow-up (likely a one-line - 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 - the CMS palette; fix only contrast/legibility breaks. Defer bespoke restyle. +- **19.2 — Public-route layout (public-route track; parallel to 19.1). DECIDED: G0-a.** Make + `Routes.razor`'s `DefaultLayout` auth-state-driven, mirroring SkipperHaven (§2c D1): cascade + `Task`, resolve `_currentLayout = authed ? CmsLayout : CmsHomeLayout`, bind + `DefaultLayout="@_currentLayout"`. Scope: `DeepDrftManager/Components/Routes.razor` only. **No new + layout (both exist), no package, no service, no AuthBlocks-source change.** + - Acceptance: an **unauthenticated** visitor to `/account/register` sees the form in the lean + `CmsHomeLayout` (not the admin app shell), can pre-fill from the deep link, and self-registers; + `/account/login` likewise renders in the lean layout for an unauthenticated visitor; an authenticated + admin still gets `CmsLayout` for the gated pages. +- **19.3 — End-to-end verification (after 19.1 + 19.2).** Exercise G2 against a running DeepDrftAPI. + Confirm list/create/deactivate users, **invite-email send (path 3) + correct `{ReturnHost}` → CMS + origin**, permission round-trips, cross-host token + CORS, and the **full path-3→path-2 loop on the + single CMS host**. File any latent break as a follow-up (likely a one-line config fix — esp. the + Mailtrap creds + return host — or an upstream AuthBlocks issue). **Mostly test, not code.** +- **19.4 — Theming legibility sweep (after 19.1 + 19.2, parallel-ok with 19.3).** Walk each user-admin + page in the CMS palette, plus the public `/account/register` + `/account/login` in `CmsHomeLayout`; fix + only contrast/legibility breaks. Defer bespoke restyle. -### Public-site track - -- **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. +**Dependency shape:** `{19.1, 19.2} → 19.3`; `19.4` follows `{19.1, 19.2}` and is parallel-ok with +`19.3`. 19.1 and 19.2 are mutually independent (different files) and should kick off together. The +path-3→path-2 acceptance in 19.3 needs 19.1 (to generate an invite) and 19.2 (to land the redeem in the +lean layout); a token minted directly via the API can verify path 2 ahead of 19.1 if needed. --- @@ -395,39 +436,29 @@ directly via the API. Recommended kick-off: 19.1 and 19.4 in parallel. **Resolved (Daniel, 2026-06-19):** 1. **Nav shape (G1) — DECIDED G1-b.** Real `MudDrawer` nav mounting `UserAdminMenu` + existing CMS - destinations. Locked. + destinations. 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. + do NOT surface the bare `NewUser`.** Both admin paths stay (provision-now vs. invite-by-email — not + duplicates); `NewUser` is redundant with `SuperRegister` and hidden from nav. +5. **Reset Password — DECIDED: non-functional in v1, handled separately** as an upstream AuthBlocks-repo + effort (see `authblocks-password-reset-brief.md`). The 19.3 verification pass must not file it. +6. **Host model — DECIDED (this revision): all three paths on `DeepDrftManager`; NO `DeepDrftPublic` + changes.** Public registration is a public/unauthenticated CMS route exactly like the CMS login. The + public-route work reduces to one router edit (G0-a, §2c D1). +7. **Public-route layout — DECIDED G0-a:** auth-state-driven `DefaultLayout` in `Routes.razor`, mirroring + SkipperHaven; reuses the existing `CmsHomeLayout`. (G0-b — per-page `@layout` — rejected: requires + forking the RCL.) **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. + pass, or leave it? **Recommend leave it** unless 19.3 surfaces a fix that needs it. +8. **Logged-in admin visiting `/account/register`.** Under G0-a, an authenticated admin who navigates to + the public register page sees it in `CmsLayout` (the app shell) rather than the lean layout. **Recommend + accept** — it is coherent (an admin in a session sees the admin shell) and the page still works; the + primary audience (unauthenticated invitees) gets the lean layout correctly. Flag only if Daniel wants + the register page forced lean regardless of session. -Items 6–9 shape the public-site track (19.4). 3, 4 are CMS scope/timing calls. None block 19.1. +None block 19.1 or 19.2.