# Phase 19 — AuthBlocks User Management in the CMS

Status: proposed (rev. 3 — host model corrected by Daniel 2026-06-19). Author: product-designer.
Date: 2026-06-19. Implementer: TBD (separate delegation).

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, 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.

**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.

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) — ALL on the CMS

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 | Gate | Backed by | Email? |
|---|------|--------------|-------|------|-----------|--------|
| 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** |

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. 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`
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. (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`. 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. 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.razor` → `/account/login` (**public — no gate, no `@layout`**; `@rendermode InteractiveServer`).
  - `Register.razor` → `/account/register` (**path 2** — public self-service via invite code; `@rendermode
    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, 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 — 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, 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**,
  all pointed at `apiBaseUrl`.

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`, `register`,
`roles`), `api/users/*`, `api/roles/*`, `api/user-roles/*`, `api/pendingregistration/*`. `AddAuthBlocks`
+ `UseAuthBlocksStartupAsync` (migrate + seed) are wired, and the Auth DB + secrets live in
`DeepDrftAPI/environment/`. This all landed with the startup separation.

---

## 2. What is ALREADY wired in DeepDrftManager (do not redo)

Verified against the current `DeepDrftManager` source. These are the integration steps a naive plan
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)` — 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 `/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 **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 gated surface coherently, and the
   public pages (no gate) pass straight through it.

**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 work is not *integration*, it is *exposure + layout-fix + verification +
fit-and-finish*.

---

## 2b. The public-route layout gap (path 2 + login) — the one real public-facing fix

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.

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.

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:

- **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.

**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.

---

## 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<AuthenticationState>`**, 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<AuthenticationState>`, 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

### 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 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 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).

**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.

> **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:

- `/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; 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 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/*` / `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, 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, 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 is low-risk but **not required** for this phase. Note it; Daniel's call on timing.

---

## 4. Scope boundaries

**In for v1 (one host — `DeepDrftManager` — two parallel tracks):**

*Admin-nav track:*
- G1-b: a `MudDrawer` nav in `CmsLayout` mounting `UserAdminMenu` (+ the existing CMS destinations).
- 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-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; **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:**

- **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)

**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.

- **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 — 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<AuthenticationState>`, 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.

**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.

---

## 6. Open questions for Daniel

**Resolved (Daniel, 2026-06-19):**

1. **Nav shape (G1) — DECIDED G1-b.** Real `MudDrawer` nav mounting `UserAdminMenu` + existing CMS
   destinations.
2. **Admin create paths — DECIDED: surface path 1 (`SuperRegister`) + path 3 (registration-token form);
   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.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.

None block 19.1 or 19.2.
