c4e22c706c
Mark brief §5 decisions resolved (all recommendations accepted 2026-06-20): NewUser canonical for direct provision, SuperRegister deleted + redirected, Registration label tidied.
278 lines
19 KiB
Markdown
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.
|