Files
Felis/internal/store/migrations/0012_setup_tokens.sql
flyemoji 0c1cc598c1 feat(auth): migrate console login to passwordless
Replace console password auth with a passwordless surface — the pre-session
login doors plus an identifier-first discovery endpoint — and remove the
password paths.

- Login doors (Public, pre-session): email-OTP, passkey assertion, op.console
  login with in-game approval, and setup-token redeem.
- /api/v1/auth/options: identifier-first discovery reporting which console
  methods an email can use. The single sanctioned existence oracle; methods
  are computed with no role branch, so staff and player accounts in the same
  credential state return byte-identical bodies (staffness invisible by
  construction).
- Remove password auth: drop StaffUser.PasswordHash and the /auth/login,
  /auth/change-password and /users/{id}/reset-password endpoints (and test).
- Data layer: UserByEmail, verified-email uniqueness, setup-token store
  (migration 0012).
- Reconcile docs/openapi.yaml with the served surface; the method/path/face/
  tier parity gate (TestOpenAPIMatchesServedRoutes) passes.
- felis TUI: in-game MC bind, owner/break-glass OP provisioning, version.
- Velocity /felis command suite.

Consolidates the accumulated backend migration work; the frontend (panel/)
is left untouched. Full Go tree green on WSL (go build ./... && go test ./...).
2026-07-04 21:47:12 +09:00

20 lines
1.4 KiB
SQL

-- One-time setup tokens for the first-web-login bootstrap (spec §B).
-- `felis setup` binds the Owner's Minecraft account, promotes it to the
-- passwordless Owner (role='admin'), and mints one of these — the raw token
-- rides in the op.console /setup?token=... URL while only its sha-256 hash is
-- stored here, mirroring sessions and account_link_codes. Opening the URL
-- redeems the token once (ConsumeSetupToken) for a lockdown session in which the
-- Owner verifies their email and enrols a passkey instead of setting a password.
-- Tokens are single-use (consumed_at) and short-lived (expires_at, 30 min); a
-- redeemed row is spent, not deleted, so a replayed URL is a clean miss rather
-- than a fresh mint. ON DELETE CASCADE keeps pending tokens from outliving the
-- user they bootstrap (mirrors webauthn_credentials, migration 0008).
CREATE TABLE setup_tokens (
token_hash text PRIMARY KEY, -- sha-256(raw token); the raw value only ever lives in the URL
user_id text NOT NULL REFERENCES users(id) ON DELETE CASCADE,
expires_at timestamptz NOT NULL, -- redemption refused once passed (ConsumeSetupToken)
consumed_at timestamptz, -- non-NULL once redeemed; the single-use gate
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX setup_tokens_user_id_idx ON setup_tokens (user_id);