feat(auth): implement REGISTRATION_POLICY=invite and REGISTRATION_POLICY=admin-approval #219

Closed
opened 2026-06-21 16:39:45 +00:00 by james · 0 comments
Owner

Context

REGISTRATION_POLICY is documented as a self-hoster env var with three accepted values: open, invite, admin-approval. Only open is implemented; the other two are accepted as configuration but reject every new registration with 503 policy_not_implemented. Self-hosters that want a closed instance today have no real option short of putting an external reverse-proxy auth in front.

Source

  • README "Configuration" table, REGISTRATION_POLICY row: "Accepts open / invite / admin-approval. Only open is implemented today — invite and admin-approval are accepted as values but reject new registrations with 503 policy_not_implemented."
  • apps/api/lib/auth/registration.ts lines 55-60: "Set REGISTRATION_POLICY=open or wait for the invite/admin-approval follow-up."

Scope

Invite policy

  • A user with is_admin = 1 can mint a one-time invite token (table: invites, fields: id, created_by, created_at, expires_at, consumed_by, consumed_at, email_hint?).
  • POST /api/auth/register requires the invite token on policy=invite; consumes it on success.
  • Admin UI: a panel under /account to mint + revoke + view consumed invites (admin-only).
  • Lifecycle: an invite is consumed on registration success, never on attempt.

Admin-approval policy

  • POST /api/auth/register on policy=admin-approval creates a user row with status = pending (new column on users).
  • Pending users cannot log in (/api/auth/login returns pending_approval).
  • Admin UI lists pending users with approve / reject actions; approve flips status = active.

Shared

  • Both policies preserve "first user is admin" semantics — the first registration on a fresh instance is always allowed (no invite needed) and lands as admin regardless of policy.
  • Migrations are automated (see CLAUDE.md conventions).
  • README env-var table updates to drop the "not yet implemented" note for both values.

Acceptance criteria

  • DB migration adds invites table and users.status column on both engines.
  • policy=invite end-to-end: admin mints, user registers with token, second-use returns invite_consumed.
  • policy=admin-approval end-to-end: registration succeeds but login is gated until admin approves.
  • First-user-is-admin still works on a fresh instance regardless of policy.
  • OpenAPI spec covers all new endpoints; drift gate green.
  • Tests cover both engines (SQLite + Postgres).
  • README "Configuration" table updated.

Out of scope

  • Email delivery of invites (Carol has no SMTP integration today; the invite token is shown to the admin to share manually).
  • Time-bounded auto-expiry of pending users (admins can reject; no cron sweep in scope).
  • Per-user role admin UI beyond is_admin (existing single-flag model is preserved).

Composes with

  • ADR-0004 (sessions).
  • The first-user-admin TOCTOU note in registration.ts lines 45-52 — still acceptable as documented.
## Context `REGISTRATION_POLICY` is documented as a self-hoster env var with three accepted values: `open`, `invite`, `admin-approval`. Only `open` is implemented; the other two are accepted as configuration but reject every new registration with `503 policy_not_implemented`. Self-hosters that want a closed instance today have no real option short of putting an external reverse-proxy auth in front. ## Source - README "Configuration" table, `REGISTRATION_POLICY` row: *"Accepts open / invite / admin-approval. Only open is implemented today — invite and admin-approval are accepted as values but reject new registrations with 503 policy_not_implemented."* - `apps/api/lib/auth/registration.ts` lines 55-60: *"Set REGISTRATION_POLICY=open or wait for the invite/admin-approval follow-up."* ## Scope ### Invite policy - A user with `is_admin = 1` can mint a one-time invite token (table: `invites`, fields: `id`, `created_by`, `created_at`, `expires_at`, `consumed_by`, `consumed_at`, `email_hint?`). - `POST /api/auth/register` requires the invite token on `policy=invite`; consumes it on success. - Admin UI: a panel under `/account` to mint + revoke + view consumed invites (admin-only). - Lifecycle: an invite is consumed on registration success, never on attempt. ### Admin-approval policy - `POST /api/auth/register` on `policy=admin-approval` creates a user row with `status = pending` (new column on `users`). - Pending users cannot log in (`/api/auth/login` returns `pending_approval`). - Admin UI lists pending users with approve / reject actions; approve flips `status = active`. ### Shared - Both policies preserve "first user is admin" semantics — the first registration on a fresh instance is always allowed (no invite needed) and lands as admin regardless of policy. - Migrations are automated (see CLAUDE.md conventions). - README env-var table updates to drop the "not yet implemented" note for both values. ## Acceptance criteria - [ ] DB migration adds `invites` table and `users.status` column on both engines. - [ ] `policy=invite` end-to-end: admin mints, user registers with token, second-use returns `invite_consumed`. - [ ] `policy=admin-approval` end-to-end: registration succeeds but login is gated until admin approves. - [ ] First-user-is-admin still works on a fresh instance regardless of policy. - [ ] OpenAPI spec covers all new endpoints; drift gate green. - [ ] Tests cover both engines (SQLite + Postgres). - [ ] README "Configuration" table updated. ## Out of scope - Email delivery of invites (Carol has no SMTP integration today; the invite token is shown to the admin to share manually). - Time-bounded auto-expiry of pending users (admins can reject; no cron sweep in scope). - Per-user role admin UI beyond `is_admin` (existing single-flag model is preserved). ## Composes with - ADR-0004 (sessions). - The first-user-admin TOCTOU note in `registration.ts` lines 45-52 — still acceptable as documented.
james closed this issue 2026-06-28 18:00:51 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
james/carol#219
No description provided.