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

19 KiB

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:
    // 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).

  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.csAdminRegisterRequest (UserName, Email, Password, ConfirmPassword, RoleIds) is the model NewUser should bind.
  7. AuthBlocksWeb/ApiClients/IAuthApiClient.cs / AuthApiClient.csAdminRegisterAsync + GetRolesAsync.
  8. AuthBlocksWeb/AuthBlocksWeb.csproj<Version>10.3.36</Version> to bump.
  9. pack.ps1 — pack/push after the bump; record the published version.