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
27 lines
928 B
SQL
27 lines
928 B
SQL
-- Peer invitations (issue #332): a user invites an e-mail address; the token
|
|
-- allows exactly one registration even while registration is closed.
|
|
|
|
-- CreateTable
|
|
CREATE TABLE "invitations" (
|
|
"id" TEXT NOT NULL,
|
|
"inviter_id" TEXT NOT NULL,
|
|
"email" TEXT NOT NULL,
|
|
"token_hash" TEXT NOT NULL,
|
|
"expires_at" TIMESTAMP(3) NOT NULL,
|
|
"revoked_at" TIMESTAMP(3),
|
|
"accepted_at" TIMESTAMP(3),
|
|
"accepted_user_id" TEXT,
|
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
CONSTRAINT "invitations_pkey" PRIMARY KEY ("id")
|
|
);
|
|
|
|
-- CreateIndex
|
|
CREATE UNIQUE INDEX "invitations_token_hash_key" ON "invitations"("token_hash");
|
|
|
|
-- CreateIndex
|
|
CREATE INDEX "invitations_inviter_id_idx" ON "invitations"("inviter_id");
|
|
|
|
-- AddForeignKey
|
|
ALTER TABLE "invitations" ADD CONSTRAINT "invitations_inviter_id_fkey" FOREIGN KEY ("inviter_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|