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).
This commit is contained in:
daniel-c-harvey
2026-06-19 20:46:14 -04:00
parent 042641d841
commit 54766fd5fc
2 changed files with 382 additions and 338 deletions
+302 -271
View File
@@ -1,57 +1,79 @@
# Phase 19 — AuthBlocks User Management in the CMS
Status: proposed (rev. 2scope expanded by Daniel 2026-06-19). Author: product-designer.
Status: proposed (rev. 3host 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
(OQ6OQ9) 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.119.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:106109`; `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:3334, 147148`), 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 14, 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<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
### G1Navigation: there is no way to reach the surface from the UI *(the real gap)*
### G0Public-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.119.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.119.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<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.
### 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 69 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.