migration 0030: app_user_invitations table — surrogate id PK + unique
token_hash, app_id FK cascading, pre-stages email + display_name +
roles for a user that doesn't exist yet. One-shot via atomic UPDATE
SET accepted_at = NOW() WHERE accepted_at IS NULL.
UsersServiceImpl gains invitations: Arc<dyn AppUserInvitationRepo>
plus a mint_session() helper factored from login() and reused by
accept_invite().
users::invite(email, opts) is gated on AppUsersAdmin (per brief —
the most senior of the three new capabilities). Optional
EmailTemplateOpts inside InviteOpts: omitting the template skips the
email send so an admin can stamp invitations for out-of-band
delivery (mailers, printed onboarding letters, etc.). If the template
is present and the email service isn't configured, surfaces as
NotConfigured; non-NotConfigured failures are logged but kept silent
so the invitation row remains valid for retry.
users::accept_invite(token, password, display_name?) atomically
consumes the invitation, validates the new password, creates the
user (returning () on DuplicateEmail — sign-up beat acceptance,
they'll log in normally), and mints a fresh session via mint_session
so the caller can return both the user and a working session token
in one round trip.
Pre-staged roles are stored on the invitation row but not yet
applied — the app_user_roles table arrives in commit 8 (migration
0031). For commit 7 the staged-but-not-applied case logs an info
record so an operator can audit the gap.
list_invitations + revoke_invitation (admin-mediated, gated on
AppUsersAdmin) ship in this commit and become reachable from the
HTTP surface later in the series.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>