# 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, **decisions approved 2026-06-20** — ready for implementation. 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) — resolved 2026-06-20 1. **Which page is canonical for direct provision?** **DECISION (2026-06-20): New User** (`/useradmin/users/new`) is the canonical direct-provision page, absorbing SuperRegister (§3.1). ✅ approved. 2. **What happens to SuperRegister?** **DECISION (2026-06-20): delete + redirect** `/account/superregister` → `/useradmin/users/new`. ✅ approved. 3. **Route naming.** **DECISION (2026-06-20):** keep `/useradmin/users/new` as the canonical direct-provision route. No `/account/*` route retained. ✅ approved. 4. **Copy/wording on NewUser.** **DECISION (2026-06-20):** go with the recommended strings in §3.1.2 — header "New User — Direct Provision" or "Activate New User"; button "Create Account"; accurate direct-provision body copy. Implementer discretion within that intent. ✅ approved. 5. **Tidy the Registration label** ("Provision New User" → "Invite New User"/"New Registration")? **DECISION (2026-06-20):** yes, in scope. ✅ approved. 6. **Future chooser page (Alternative D)** — **DECISION (2026-06-20):** deferred; captured as a possible later enhancement. ✅ approved (defer). --- ## 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.