Account creation UI: optional initial contact-card fields

closed
#c83864d opened by agent Sep 21

Goal

Expand the domain-page account-creation form so a tenant admin can optionally populate the new mailbox owner’s initial RFC 9553 contact card at create time. This is the UI half of bug 858fad1 (backend already ships the contact input on at.sovrn.mail.createAccount and writes the card via internal/contact).

Scope

  • Add optional form fields to the “Add a mailbox” form (internal/appview/ui/domain.templ): given name, surname, titles (repeatable), organizations (repeatable), and online services (service/user/uri; Atmosphere is auto-added).
  • Extend Handler.createAccount (internal/appview/ui/handler.go) to parse these fields into the appview.AccountContact already accepted by ProvisionAccountWithDetails.
  • Keep all fields optional; a bare create still produces the Atmosphere onlineServices entry when a handle is known.
  • Show a non-fatal warning if the card write failed (best-effort semantics from 858fad1).
  • HTMX + no-JS behavior consistent with the existing mailbox form; tests in internal/appview/ui/create_account_test.go.

Depends on

  • 858fad1 (backend: lexicon contact input, internal/contact, ProvisionAccountWithDetails).

2 Comments

agent c185328 Sep 24

Implementation Plan — “Add a user” page (/domains/{id}/users/new)

The “Add a user” buttons on home and the domain page already link to /domains/{id}/users/new, which has no handler yet. The old HTMX mailbox form was removed in the domain-page redesign, so the POST /domains/{id}/accounts UI handler and its fragments are no longer used.

Decisions

  • Form order follows the admin’s choices: (1) the Atmosphere account choice, a radio with explanatory text: they already have one or create one for them (sovrn hosts their PDS); (2) email username (@domain suffix fixed) plus the handle field; (3) optional address-book details: given name, surname, organization, job title.
  • New handle: the admin edits only the first label. .at.<domain> is a fixed suffix because the hosted PDS serves that namespace. The label is filled in from the username as it’s typed: lowercase, every character outside [a-z0-9] (., _, +, …) becomes -, repeated hyphens collapse to one, leading and trailing hyphens are trimmed, and the result is cut to 63 characters. Once the admin edits the label, autofill stops. The rule lives in Go (pdsprovisioner.SuggestLabel and ValidLabel, which are authoritative) and is mirrored in JS.
  • Existing account: handle input with the existing typeahead (data-typeahead); the server resolves the handle to a DID, and that result is authoritative.
  • Live validation: GET /domains/{id}/users/new/check-handle (htmx, debounced) returns a status line. For an existing handle it checks that the handle resolves. For a new handle it checks the label is valid and that label.at.domain doesn’t already resolve (already taken).
  • onlineServices is not a form field: the server builds the Atmosphere entry (@handle, at://<did>) from the chosen handle and the DID, which is safer than trusting a hidden input. The page shows a read-only line saying the address book entry will list their Atmosphere handle.
  • Submit: a plain POST to /domains/{id}/users/new that works without JS. On an error the page is re-rendered with a 422, the submitted values kept and the error shown on the field it belongs to. On success it redirects (post/redirect/get) to ?created=<accountId>[&card=failed].
  • Success dialog: the shadcn-templ dialog component (added via go tool shadcn-templ add dialog), open on load. It shows the address, the handle and its status, and a non-fatal warning if the card write failed. Two actions: Add another user (a fresh form) and Back to domain settings.
  • Card failure surfaced: a new appview.ProvisionAccountWithContact returns (id, cardErr, err). ProvisionAccountWithDetails wraps it, so the XRPC path is unchanged.

Tasks

  1. internal/pdsprovisioner: SuggestLabel, ValidLabel and tests.
  2. internal/appview/provision.go: ProvisionAccountWithContact (card error returned) and a test.
  3. internal/appview/ui: add_user.templ (page and dialog), a users_new.go handler (GET form, POST create, GET check-handle), and routes. Remove the orphaned createAccount/writeMailboxError/mailboxList UI path and new_account.js; static/add_user.js (radio toggle, label autofill, validation hooks) replaces it.
  4. Tests in create_account_test.go, rewritten for the new route: BYO resolves; new handle is derived and edited; invalid label gives a 422 with values kept; duplicate address gives a 422; the card fields reach Stalwart ContactCard/set; the success redirect renders the dialog; non-owners get a 403.
  5. go generate ./internal/appview/ui, go build ./..., go test ./internal/..., then check it visually in the browser.
agent c0833a8 Sep 24

Implementation complete: ready for review

This is on jj change zurxussy (“feat(ui): add-a-user page…”). The bug stays open until you’ve reviewed it.

What shipped

  • /domains/{id}/users/new (GET form, POST create): the target of the existing “Add a user” buttons on home and the domain page. It’s owner-only (non-owners get a 403).
    1. Their Atmosphere account: two radio cards, Create one for them (the default; sovrn hosts their PDS account) or They already have one, each with a sentence explaining what will happen.
    2. Email address and handle: the username with a fixed @domain suffix. For a new account there’s an editable handle label with a fixed .at.<domain> suffix, filled in from the username until the admin edits it (maria.lopez → maria-lopez). For an existing account there’s a handle field using the typeahead. CSS :has() shows the fields for the checked choice, so this works without JS.
    3. Address book details: given name, surname, organization and job title, all optional. The Atmosphere onlineServices entry is built on the server from the handle and DID; there’s no hidden input. The page says the entry will list their handle.
  • Live handle check at GET …/users/new/check-handle (htmx). For a new handle it checks the label is valid and the handle is free (not a pending account at this domain, and it doesn’t resolve). For an existing handle it checks the handle resolves and shows the DID.
  • Submit: invalid input re-renders the page with a 422, the values kept and each error on its field. The PDS account is provisioned only after every check passes. On success it redirects to ?created=<id>[&card=failed], which opens a shadcn-templ dialog: address, handle, pending note, a non-fatal warning if the card write failed, and Add another user / Back to domain settings. Without JS, a <noscript> copy shows the same message.
  • Backend: pdsprovisioner.SuggestLabel and HostedHandle (mirrored in static/add_user.js). appview.ProvisionAccountWithContact returns the card error; ProvisionAccountWithDetails wraps it, so the XRPC path is unchanged. After you pick a typeahead result it fires change, so the check runs.
  • Removed (orphaned since the domain-page redesign): the UI POST /domains/{id}/accounts handler, writeMailboxError, mailboxList/mailboxRow, static/new_account.js, and their tests.

Verification

  • go build ./... and go test ./internal/... pass. New tests cover decision order, 403, the derived and the edited label, an invalid label with values kept, a taken handle, a duplicate address, a card with all fields plus Atmosphere, an unknown handle, the dialog with the card-failure warning, and the check-handle endpoint.
  • JS suggestLabel matches the Go test cases.
  • Checked visually in headless Chromium: new, existing, error and created-dialog states.
  • Not run against the live dev stack with Stalwart.