From 0c22ce8f097036c849d37515f6aaa7230ccf3efa Mon Sep 17 00:00:00 2001 From: daniel-c-harvey Date: Sat, 20 Jun 2026 00:28:06 -0400 Subject: [PATCH] docs: add AuthBlocks NewUser/SuperRegister normalization team brief Brief the AuthBlocks team to make NewUser the canonical direct-provision page (absorbing SuperRegister) and keep Registration as the invite flow. --- ...-brief-authblocks-newuser-normalization.md | 278 ++++++++++++++++++ 1 file changed, 278 insertions(+) create mode 100644 product-notes/team-brief-authblocks-newuser-normalization.md diff --git a/product-notes/team-brief-authblocks-newuser-normalization.md b/product-notes/team-brief-authblocks-newuser-normalization.md new file mode 100644 index 0000000..6b11982 --- /dev/null +++ b/product-notes/team-brief-authblocks-newuser-normalization.md @@ -0,0 +1,278 @@ +# Team Brief — AuthBlocks: Normalize the Account-Creation Pages (NewUser vs. Registration vs. SuperRegister) + +**Audience:** an orchestrator (and its implementers) working **only** in the AuthBlocks repository at +`C:\Development\AuthBlocks`. You do not need, and should not assume, any knowledge of the products that +consume AuthBlocks. Everything you need is in this brief or in that one repo. + +**Status:** scoped request, not yet started. Author: product-designer (for a downstream consumer team). +Date: 2026-06-20. + +--- + +## 1. The problem in one sentence + +AuthBlocks ships **three** account-creation pages whose identities have drifted: the **New User** page +(`/useradmin/users/new`) is broken and has become a half-built *duplicate of the invite/registration flow* +instead of the **direct admin-provisioning** path it is named for — while a separate page, +**SuperRegister** (`/account/superregister`), is the page that actually performs direct admin provisioning. +The two "create an account now" identities live in different places, one of them is broken, and the labels +lie about what each does. This brief normalizes the three paths into distinct, correctly-named flows. + +The intended end state (per the consuming team): + +1. **Direct provision** — admin creates a *live* account immediately (username + email + password + roles), + bypassing email entirely. This is what **New User** should be. +2. **Admin invite-by-email** — admin sends a registration code + link to an email; the recipient redeems it + to create their own account. This is the **Registration** flow. +3. **Public self-service redeem** — the recipient lands on the public **Register** page and completes + account creation with the code. (Already correct; included for completeness.) + +--- + +## 2. Current-state analysis (read this before designing — it is the crux) + +### 2.1 New User — `/useradmin/users/new` (BROKEN + mislabeled + duplicate) + +- **Page:** `AuthBlocksWeb/Components/Pages/UserAdmin/Users/NewUser.razor` — just renders ``. +- **Form markup:** `.../Users/NewUserForm.razor`. The card header reads **"Activate New User"**. The body + text reads: + > *"Create a new user account, bypassing email registration. The password must be provided now."* + This message describes **direct provisioning** — and it is the **wrong message for what the page actually + does**, because: + - The form has **only an Email field**. There is **no username field, no password field, and no role + selector** — despite the copy promising "the password must be provided now." + - The submit button is labeled **"Send Registration Code"** — i.e. invite-flow language, contradicting the + "bypassing email registration" body copy directly above it. +- **What it actually does:** **nothing — it throws.** The form's `OnValidSubmit` is wired to a stub: + ```csharp + // NewUserForm.razor.cs + private void X() + { + throw new NotImplementedException(); + } + ``` + The code-behind also carries a **commented-out body** that, if enabled, would call + `Client.CreatePendingRegistration(...)` and pop a `UserSubmittedModal` — i.e. it would make NewUser + **identical to the invite/Registration flow**. So the page is mid-migration: someone started turning the + direct-provision page into a second copy of the invite page, didn't finish, and left a throwing stub. +- **Backing model is the invite model, not the provision model:** `NewUserForm` binds + `PendingRegistrationInputModel Input` and injects `PendingRegistrationClient` — the *registration* model + and client, **not** `AdminRegisterRequest` / the admin-register path. This is the concrete duplication: + NewUser is plumbed for invites, not for direct provisioning. + +**In short:** NewUser is named for direct provisioning, *says* it does direct provisioning ("bypassing email +registration… password must be provided now"), is *wired* for invites (PendingRegistration model/client + +commented-out invite call), is *labeled* for invites ("Send Registration Code"), and **actually throws +`NotImplementedException`**. Every layer disagrees with every other layer. + +### 2.2 SuperRegister — `/account/superregister` (the REAL direct-provision page, working) + +- **Page:** `AuthBlocksWeb/Components/Pages/Account/SuperRegister.razor`. `[HierarchicalRoleAuthorize(UserAdmin)]`, + `@rendermode InteractiveServer`. Title "Admin Register"; header "Create a new account." +- **This is the genuine direct-provision UI.** Full form: Username, Email, Password, Confirm Password, and a + **multi-select Roles** dropdown populated from `AuthApiClient.GetRolesAsync`. +- **Backing model + call:** binds `AdminRegisterRequest` (UserName, Email, Password, ConfirmPassword, + RoleIds) and calls `AuthApiClient.AdminRegisterAsync(Input, token)` → `POST api/auth/admin-register`. +- **What that endpoint does** (`AuthBlocksLib/Routes/AuthRoutes.cs`, `AdminRegister`, role-gated to + `UserAdmin`): rejects a duplicate email; resolves each `RoleId` to a role name up front (fail-fast); + creates the user via `userService.Add(user, request.Password)` with `EmailConfirmed = true`; assigns + roles (deleting the half-created user if a role assignment fails); returns an `AuthResponse`. **No email, + no token, no pending row — a live account immediately.** This is exactly the behavior the consumer wants + *New User* to have. + +### 2.3 New Registration — `/useradmin/registrations/new` (the invite flow, working) + +- **Page:** `AuthBlocksWeb/Components/Pages/UserAdmin/Registrations/NewRegistration.razor` → ``. +- **Form:** `.../Registrations/NewRegistrationForm.razor`. Header **"Provision New User"** (note: this label + is *also* slightly off — it performs an *invite*, not a provision; see decisions §5). Body: *"A registration + code and link will be sent to the email address provided."* Fields: Email + multi-select Roles. Button: + "Send Registration Code". +- **Backing model + call:** binds `PendingRegistrationInputModel`, injects `PendingRegistrationClient`, and + on submit calls `Client.CreatePendingRegistration(email, roles, returnHost=.../account/register)`, then + shows `RegistrationSubmittedModal` (which reports "An email has been sent to: …"). +- **What that hits:** `POST api/pendingregistration/create` (`AuthBlocksLib/Routes/PendingRegistrationRoutes.cs`, + `Create`, group role-gated to `UserAdmin`): rejects existing user / existing pending registration; + generates a registration token; persists a `PendingRegistration` row with the token **hash** and an expiry; + emails a code + a deep link (`returnHost?UserEmail=&RegistrationToken=`) via `IGeneralEmailSender`. The + recipient later redeems at the public **Register** page (`POST api/auth/register`, which validates the + code, creates the user, consumes the token). + +### 2.4 The duplication, stated precisely + +`NewUserForm` and `NewRegistrationForm` **bind the same model (`PendingRegistrationInputModel`) and the same +client (`PendingRegistrationClient`), and NewUser's commented-out handler is a near-copy of +NewRegistration's `CreatePendingRegistration` handler.** NewUser is, in its half-built state, a strictly +worse duplicate of NewRegistration (no roles field, throwing stub) — while the page that *should* own +NewUser's intended behavior (direct provision) is the separate `SuperRegister`. The system has: + +- **two pages aimed at the invite flow** (Registration = working; NewUser = broken duplicate), and +- **one page doing direct provision under a different name/route** (SuperRegister), +- **zero working pages at the New User route doing what "New User" implies.** + +### 2.5 The API is already correct — this is a Web-layer normalization + +Both endpoints exist and work today: `admin-register` (direct provision, role-gated) and +`pendingregistration/create` (invite). **No new API endpoints are required.** This task is about pointing +the right *page* at the right *existing* endpoint, with honest labels and routes. (Contrast the +password-reset brief, which was build-from-scratch on both tiers.) + +--- + +## 3. The normalization design + +End state: **three crisp paths, each at one page, each correctly labeled, each on the right endpoint.** + +| Path | Page (canonical) | Route | Endpoint | Result | +|---|---|---|---|---| +| Direct provision | **New User** | `/useradmin/users/new` | `POST api/auth/admin-register` | live account now | +| Admin invite | **New Registration** | `/useradmin/registrations/new` | `POST api/pendingregistration/create` | emailed code + link | +| Public redeem | **Register** | `/account/register` | `POST api/auth/register` | recipient self-creates | + +The core move: **make `NewUser` the canonical direct-provision page by absorbing SuperRegister's behavior**, +fix its copy, and resolve the now-redundant SuperRegister. Registration stays as-is (modulo a label tidy). + +### 3.1 Recommended approach — "Absorb into NewUser, retire SuperRegister" + +1. **Rebuild `NewUserForm` as the direct-provision form.** Re-bind it from `PendingRegistrationInputModel` / + `PendingRegistrationClient` to **`AdminRegisterRequest`** and the **auth client** that calls + `AdminRegisterAsync` (the `IAuthApiClient` + `IAuthSession` token pattern SuperRegister already uses). + Bring across SuperRegister's full field set: Username, Email, Password, Confirm Password, and the + role multi-select sourced from `GetRolesAsync`. Delete the throwing `X()` stub and the commented-out + invite handler. Keep the existing card/`MudContainer` chrome so it matches the other UserAdmin pages + (NewUser/NewRegistration share a card layout that the standalone SuperRegister does not). +2. **Fix the copy.** Header → e.g. **"New User — Direct Provision"** (or "Activate New User", kept, now that + the page genuinely activates one). Body → keep the accurate *"Create a live account now, bypassing email + registration. Set the password directly."* Button → **"Create Account"** (retire the misleading + "Send Registration Code" on this page). On success, show a confirmation and route back to + `/useradmin/users` (force-reload so the grid refreshes — mirror NewRegistration's post-submit nav). +3. **Retire `SuperRegister`.** Once NewUser owns direct provision, SuperRegister is a duplicate. Preferred: + **delete the page and redirect `/account/superregister` → `/useradmin/users/new`** so any existing + bookmark or consumer nav link doesn't 404 during the consumer's catch-up window (see §7). Keep the + redirect lightweight (a `NavigationManager.NavigateTo` in a thin page, or a server redirect). Do **not** + leave two separate-but-identical "create now" pages alive. +4. **Tidy Registration's label** (small, optional but recommended for the normalization to be coherent): + the invite page header currently says **"Provision New User"**, which collides with the direct-provision + concept now owned by NewUser. Rename it to **"Invite New User"** / **"New Registration"** so "provision" + unambiguously means *direct* and "invite/registration" means *emailed code*. No behavior change. + +**Why this approach:** NewUser's route (`/useradmin/users/new`) is where an admin looking at the Users grid +expects to click "add a user," and it lives in the UserAdmin/Users area beside the grid — the natural home +for the canonical create-now action. SuperRegister sits oddly under `/account/*` (the *public* auth area, +alongside Login/Register) despite being an admin-only action; folding it into UserAdmin/Users fixes that +mis-placement as a side effect. The API already supports it, so this is low-risk re-pointing, not new +behavior. + +### 3.2 Alternatives considered + +- **B — Keep SuperRegister canonical; make NewUser a redirect to it.** Inverse of the recommendation: + delete `NewUserForm`'s logic, point `/useradmin/users/new` → `/account/superregister`. Cheaper (no form + rebuild), but it **enshrines the mis-placement** (admin-only page under `/account/*`) and leaves the + visual inconsistency (SuperRegister doesn't use the UserAdmin card chrome). Rejected: it normalizes the + *names* but not the *information architecture*. Choose this only if rebuilding the form is deemed + out-of-budget for now — and even then, treat it as interim. +- **C — Keep both pages, share one form component.** Extract a single `DirectProvisionForm` component and + render it from both `NewUser.razor` and `SuperRegister.razor`. Eliminates code duplication but **leaves + two routes for one action** — exactly the "two create-now pages" the consumer is asking to remove. Rejected + for the explicit goal; the duplication the consumer dislikes is at the *page/route* level, not just code. +- **D — Make NewUser a *chooser*** (two buttons: "Create now" vs. "Invite by email").** A single entry point + that branches to the two real flows. Genuinely nice UX and worth noting as a *future* enhancement, but it + is scope-creep on a "normalize what exists" request and introduces a fourth surface. Defer. + +**Recommendation: A.** It produces the cleanest end state (correct names, correct routes, correct IA, no +redundant page) at the cost of one form rebuild that is mostly a copy of SuperRegister's already-working +form. + +--- + +## 4. Constraints + +- **No new API endpoints.** `admin-register` and `pendingregistration/create` already exist and are correct + (§2.5). If you find yourself adding an endpoint, stop — you've taken a wrong turn. +- **Preserve role-gating.** Direct provision must stay `[HierarchicalRoleAuthorize(UserAdmin)]` (SuperRegister + has it; ensure rebuilt NewUser keeps it — NewUser currently inherits whatever the UserAdmin pages set, so + verify the attribute is present on the page). +- **Reuse the existing client + session pattern.** Direct provision uses `IAuthApiClient.AdminRegisterAsync` + + `IAuthSession.GetValidTokenAsync` (as SuperRegister does). Do not introduce a new client; do not route + direct provision through `PendingRegistrationClient`. +- **Match the UserAdmin page conventions.** NewUser/NewRegistration use a `MudContainer` + `MudCard` + + back-button layout; keep the rebuilt NewUser in that house style rather than transplanting SuperRegister's + bare `MudGrid` layout verbatim. +- **No 404s for retired routes.** If SuperRegister is removed, `/account/superregister` must redirect, not + break (§3.1.3, §7). +- **Versioning:** lands as a normal AuthBlocks Web version bump, packed/pushed via `pack.ps1`. `AuthBlocksWeb` + is currently `Cerebellum.AuthBlocks.Web` **10.3.36**; bump to **10.3.37** (or the next free patch if + another bump has landed since this brief). **Record the published version** so the consumer can pin. + +--- + +## 5. Decisions for the sponsor (Daniel) — resolve before/while implementing + +1. **Which page is canonical for direct provision?** Recommendation: **New User** (`/useradmin/users/new`), + absorbing SuperRegister (§3.1). Confirm, or choose Alternative B (SuperRegister stays, NewUser redirects). +2. **What happens to SuperRegister?** Recommendation: **delete + redirect `/account/superregister` → + `/useradmin/users/new`.** Alternatives: keep it as a permanent alias (more surface to maintain), or hard- + delete with no redirect (risks a 404 until the consumer updates its nav — see §7). Confirm. +3. **Route naming.** Recommendation: keep `/useradmin/users/new` as the canonical direct-provision route (it + matches the UserAdmin IA). Confirm there's no desire to keep an `/account/*` route for it. +4. **Copy/wording on NewUser.** Header ("Activate New User" vs. "New User — Direct Provision"), button + ("Create Account" vs. "Provision User"), and body text. Recommendation in §3.1.2 — confirm exact strings, + or leave to implementer discretion within the intent. +5. **Tidy the Registration label** ("Provision New User" → "Invite New User"/"New Registration")? Recommended + for coherence (§3.1.4) but strictly optional and behavior-neutral. In or out? +6. **Future chooser page (Alternative D)** — explicitly defer, or capture as a follow-up? Recommendation: + defer; note as a possible later enhancement. + +--- + +## 6. Acceptance criteria + +1. Navigating to `/useradmin/users/new` shows a **direct-provision form** with Username, Email, Password, + Confirm Password, and a Roles multi-select — **no "Send Registration Code" language**, no throwing stub. +2. Submitting that form with a valid username/email/password (and optional roles) calls + `POST api/auth/admin-register`, creates a **live account immediately** (no email sent, no pending- + registration row), assigns the selected roles, shows a success confirmation, and returns to + `/useradmin/users` with the new user visible in the grid. +3. The page's copy accurately describes direct provisioning; the button reads "Create Account" (or the + sponsor-approved string). +4. `NewUserForm` no longer binds `PendingRegistrationInputModel` / `PendingRegistrationClient`, no longer + contains the `X()` stub or the commented-out invite handler. +5. SuperRegister is resolved per the sponsor's decision: if retired, `/account/superregister` **redirects** + to `/useradmin/users/new` (no 404, no second working create-now page); if kept as alias, it is explicitly + an alias, not an independent duplicate. +6. The invite flow at `/useradmin/registrations/new` still works unchanged (emails a code + link); if its + label was tidied, the change is cosmetic only. +7. Direct provision remains `UserAdmin`-role-gated; an unauthorized user cannot reach it. +8. Published as a version bump from 10.3.36 (expected **10.3.37**); the new version number is recorded. + +--- + +## 7. Downstream consequence (for the consumer to handle later — NOT this team's work) + +The consuming product (DeepDrftManager) currently surfaces **SuperRegister (`/account/superregister`)** in +its CMS navigation as the **"Provision User"** entry, alongside a **Registrations** link. If this +normalization changes which page is canonical or its route — specifically if `/account/superregister` is +retired in favor of `/useradmin/users/new` — the consumer's nav link will need a small follow-up update +**after this AuthBlocks change ships and the consumer bumps its package reference**. The recommended +delete-**and-redirect** (§3.1.3, decision §5.2) is precisely to keep that consumer working in the interval +between this ship and the consumer's catch-up. **This is noted only so the implementing team understands why +the redirect matters; updating the consumer's nav is out of scope for AuthBlocks.** + +--- + +## 8. Suggested reading order in the repo + +1. `AuthBlocksWeb/Components/Pages/UserAdmin/Users/NewUserForm.razor` + `.razor.cs` — the broken page; the + wrong message, the missing fields, the `X()` stub, the commented-out invite handler. **Start here.** +2. `AuthBlocksWeb/Components/Pages/Account/SuperRegister.razor` — the working direct-provision form to + absorb (fields, role multi-select, `AdminRegisterAsync` call, `IAuthSession` token pattern). +3. `AuthBlocksWeb/Components/Pages/UserAdmin/Registrations/NewRegistrationForm.razor` + `.razor.cs` — the + invite flow NewUser was wrongly duplicating; the post-submit modal + force-reload nav pattern to mirror. +4. `AuthBlocksLib/Routes/AuthRoutes.cs` — the `AdminRegister` endpoint (direct provision; what NewUser must + call) and `Register` (public redeem); the `ApiResult`/result conventions. +5. `AuthBlocksLib/Routes/PendingRegistrationRoutes.cs` — the `Create` endpoint (invite); confirms the two + endpoints are already distinct and correct (no API work needed). +6. `AuthBlocksModels/ApiModels/AuthModels.cs` — `AdminRegisterRequest` (UserName, Email, Password, + ConfirmPassword, RoleIds) is the model NewUser should bind. +7. `AuthBlocksWeb/ApiClients/IAuthApiClient.cs` / `AuthApiClient.cs` — `AdminRegisterAsync` + `GetRolesAsync`. +8. `AuthBlocksWeb/AuthBlocksWeb.csproj` — `10.3.36` to bump. +9. `pack.ps1` — pack/push after the bump; record the published version.