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.
This commit is contained in:
@@ -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 `<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) — 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` — `<Version>10.3.36</Version>` to bump.
|
||||
9. `pack.ps1` — pack/push after the bump; record the published version.
|
||||
Reference in New Issue
Block a user