Account creation UI: optional initial contact-card fields
closedGoal
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 theappview.AccountContactalready accepted byProvisionAccountWithDetails. - Keep all fields optional; a bare create still produces the Atmosphere
onlineServicesentry 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
contactinput,internal/contact,ProvisionAccountWithDetails).
2 Comments
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 thePOST /domains/{id}/accountsUI handler and its fragments are no longer used.Decisions
@domainsuffix fixed) plus the handle field; (3) optional address-book details: given name, surname, organization, job title..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.SuggestLabelandValidLabel, which are authoritative) and is mirrored in JS.data-typeahead); the server resolves the handle to a DID, and that result is authoritative.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 thatlabel.at.domaindoesn’t already resolve (already taken).@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./domains/{id}/users/newthat 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].dialogcomponent (added viago 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.appview.ProvisionAccountWithContactreturns(id, cardErr, err).ProvisionAccountWithDetailswraps it, so the XRPC path is unchanged.Tasks
internal/pdsprovisioner:SuggestLabel,ValidLabeland tests.internal/appview/provision.go:ProvisionAccountWithContact(card error returned) and a test.internal/appview/ui:add_user.templ(page and dialog), ausers_new.gohandler (GET form, POST create, GET check-handle), and routes. Remove the orphanedcreateAccount/writeMailboxError/mailboxListUI path andnew_account.js;static/add_user.js(radio toggle, label autofill, validation hooks) replaces it.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 StalwartContactCard/set; the success redirect renders the dialog; non-owners get a 403.go generate ./internal/appview/ui,go build ./...,go test ./internal/..., then check it visually in the browser.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).@domainsuffix. 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.onlineServicesentry is built on the server from the handle and DID; there’s no hidden input. The page says the entry will list their handle.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.?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.pdsprovisioner.SuggestLabelandHostedHandle(mirrored instatic/add_user.js).appview.ProvisionAccountWithContactreturns the card error;ProvisionAccountWithDetailswraps it, so the XRPC path is unchanged. After you pick a typeahead result it fireschange, so the check runs.POST /domains/{id}/accountshandler,writeMailboxError,mailboxList/mailboxRow,static/new_account.js, and their tests.Verification
go build ./...andgo 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.suggestLabelmatches the Go test cases.