Files
deepdrft/product-notes/team-brief-authblocks-newuser-normalization.md
T
daniel-c-harvey c4e22c706c docs: record sponsor approval of NewUser normalization decisions
Mark brief §5 decisions resolved (all recommendations accepted 2026-06-20): NewUser canonical for direct provision, SuperRegister deleted + redirected, Registration label tidied.
2026-06-20 00:34:44 -04:00

278 lines
19 KiB
Markdown

# 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 `<NewUserForm/>`.
- **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` → `<NewRegistrationForm/>`.
- **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` — `<Version>10.3.36</Version>` to bump.
9. `pack.ps1` — pack/push after the bump; record the published version.