Invitation flow with per-user quota (#332) #334

Merged
fable-5 merged 2 commits from feat/332-invitations into main 2026-08-05 13:40:48 +02:00
Collaborator

Implements #332. Chained on #333 (branched off feat/331-admin-create-user to avoid the audit-catalogue conflict) - merge #333 first, then this one fast-forwards.

  • invitations table + migration; token stored as SHA-256 hash, 14-day expiry, single-use (atomic redeem, un-redeem when the gated signup fails so the link survives a taken-username attempt).
  • POST/GET /invitations, DELETE /invitations/:id (AuthenticatedOnly), POST /invitations/preview (@Public, POST keeps the token out of logs).
  • POST /auth/signup accepts invitationToken: a valid token passes a CLOSED registration exactly once; the verification mail still applies (a forwarded link proves nothing about the mailbox).
  • Quota: setting invitations.maxOpenPerUser (default 5, 0 disables inviting -> 403 invitations_disabled) counts OPEN (pending, unexpired) invitations; plus a 20/day per-user rate limit against revoke-and-recreate mail loops.
  • UI: settings section (invite/list/revoke, quota line, .table-scroll region), signup page banner + prefill via ?invitation=, admin quota field (flat RHF name per #322, VS-NfD marking/hiding).
  • Governance: audit catalogue 1.10 (invitation.*), VS-NfD profile entry (compliant 0) + hardening-guide row (fence green), i18n de+en incl. mail template.
  • CI: new invitations e2e pack (needs Mailpit like auth) with its own rate-limit reset before it and before the following pack.
  • Local verification: full api suite 107/607 green (fresh DB), invitations/auth/admin-settings/a11y(settings incl. reflow) packs green against a fresh local stack; typecheck/lint/i18n green.

Merge only on Stefan's go (fast-forward-only, after #333).

Implements #332. **Chained on #333** (branched off `feat/331-admin-create-user` to avoid the audit-catalogue conflict) - merge #333 first, then this one fast-forwards. - `invitations` table + migration; token stored as SHA-256 hash, 14-day expiry, single-use (atomic redeem, un-redeem when the gated signup fails so the link survives a taken-username attempt). - `POST/GET /invitations`, `DELETE /invitations/:id` (AuthenticatedOnly), `POST /invitations/preview` (@Public, POST keeps the token out of logs). - `POST /auth/signup` accepts `invitationToken`: a valid token passes a CLOSED registration exactly once; the verification mail still applies (a forwarded link proves nothing about the mailbox). - Quota: setting `invitations.maxOpenPerUser` (default **5**, `0` disables inviting -> 403 `invitations_disabled`) counts OPEN (pending, unexpired) invitations; plus a 20/day per-user rate limit against revoke-and-recreate mail loops. - UI: settings section (invite/list/revoke, quota line, `.table-scroll` region), signup page banner + prefill via `?invitation=`, admin quota field (flat RHF name per #322, VS-NfD marking/hiding). - Governance: audit catalogue **1.10** (`invitation.*`), VS-NfD profile entry (compliant `0`) + hardening-guide row (fence green), i18n de+en incl. mail template. - CI: new `invitations` e2e pack (needs Mailpit like auth) with its own rate-limit reset before it and before the following pack. - Local verification: full api suite 107/607 green (fresh DB), invitations/auth/admin-settings/a11y(settings incl. reflow) packs green against a fresh local stack; typecheck/lint/i18n green. Merge only on Stefan's go (fast-forward-only, after #333).
fable-5 added 2 commits 2026-08-05 12:44:39 +02:00
Admin can create user accounts directly (#331)
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 6m47s
CI / Build container images (pull_request) Successful in 3m59s
CI / Auth e2e pack (pull_request) Successful in 9m7s
CI / Import/export fidelity gate (pull_request) Successful in 57s
CD / Deploy to Test (push) Blocked by required conditions
CD / Smoke tests against Test (push) Blocked by required conditions
CD / Promote to Int (push) Blocked by required conditions
CI / Build container images (push) Blocked by required conditions
CI / Lint, typecheck, test (push) Has been cancelled
CI / Auth e2e pack (push) Blocked by required conditions
CI / Import/export fidelity gate (push) Blocked by required conditions
CD / Build and push images (push) Has been cancelled
9cf7b85b93
POST /admin/users (Site-Admin guard) creates an account with the same
field rules as self-registration, but active immediately: the admin
vouches for the address, so the e-mail is marked verified and the
personal pond is provisioned exactly like the verify-email path does
(markEmailVerified alone would skip the pond).

The user manager gains a create dialog (useModalFocus/useDismissable,
Field wiring, flat RHF field names per the #322 lesson). New audit
action user.created_by_admin, catalogue bumped to 1.9.

Tests: api e2e-db (create + immediate login + personal pond, duplicate
username 409, non-admin 403), web e2e through the dialog, and the
admin a11y scan now opens the dialog too. Both packs verified locally
against a fresh stack.

Closes #331
Invitation flow with per-user quota (#332)
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 6m50s
CI / Build container images (pull_request) Successful in 3m57s
CI / Auth e2e pack (pull_request) Failing after 6m18s
CI / Import/export fidelity gate (pull_request) Has been skipped
c2a4dde5cc
Any authenticated user can invite an e-mail address; the mailed
single-use token lets exactly one signup through even while
registration is closed. Open (pending, unexpired) invitations count
against the new instance setting invitations.maxOpenPerUser (default 5,
0 disables inviting) — plus a 20/day per-user rate limit so a
revoke-and-recreate loop cannot become a mail cannon. Only the SHA-256
token hash is stored (auth-tokens pattern); a failed signup (taken
username) un-redeems the token so the invitee can retry.

Surfaces: invitations section in the user settings (list, invite,
revoke, quota line; wide table in a focusable .table-scroll region),
signup page reads ?invitation=<token> (preview banner, e-mail prefill,
closed-mode gate opens only for a previewed-valid token), admin general
card gets the quota field (flat RHF name per #322; VS-NfD marked and
hideable).

Governance: audit actions invitation.created/revoked/accepted
(catalogue 1.10), VS-NfD profile entry (compliant: 0) + hardening-guide
row, i18n de+en including the invitation mail template.

Tests: api e2e-db (mail link, closed-mode single-use signup with
un-redeem on failure, quota + revoke frees slot, quota 0 = 403, auth
matrix), new web e2e pack invitations.spec.ts (full UI loop through
Mailpit, wired into ci.yml with its own rate-limit reset), a11y scan
waits for the new section. Full api suite (107 files / 607 tests),
auth/admin-settings/a11y packs green against a fresh local stack.

Closes #332
fable-5 added 1 commit 2026-08-05 13:06:34 +02:00
settings-nav fence: 10 sections since the invitations section (#332)
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 6m52s
CI / Auth e2e pack (pull_request) Successful in 9m36s
CI / Import/export fidelity gate (pull_request) Successful in 58s
CI / Build container images (pull_request) Successful in 1m14s
CD / Build and push images (push) Successful in 16s
CD / Deploy to Test (push) Successful in 16s
CD / Smoke tests against Test (push) Successful in 1m26s
CD / Promote to Int (push) Successful in 12s
CI / Lint, typecheck, test (push) Successful in 7m0s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 9m19s
CI / Import/export fidelity gate (push) Successful in 59s
Release / Build release images and notes (push) Successful in 2m54s
Release / Release-candidate operations QA (push) Successful in 49s
Prod deploy / Deploy the released images to Prod (push) Successful in 19s
7e17a2dba6
fable-5 merged commit 7e17a2dba6 into main 2026-08-05 13:40:48 +02:00
Sign in to join this conversation.
No description provided.