-- Migration 027 — TOTP two-factor auth. -- -- totp_secret holds the base32 shared secret once enrollment is confirmed -- (NULL while disabled or mid-enrollment). totp_enabled flips true only after -- the user verifies their first code, so a half-finished enrollment never locks -- anyone out. Recovery codes are one-time bcrypt-hashed fallbacks; used_at marks -- a code as spent. ALTER TABLE users ADD COLUMN IF NOT EXISTS totp_secret TEXT; ALTER TABLE users ADD COLUMN IF NOT EXISTS totp_enabled BOOLEAN NOT NULL DEFAULT FALSE; CREATE TABLE IF NOT EXISTS user_recovery_codes ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id UUID NOT NULL REFERENCES users ON DELETE CASCADE, code_hash TEXT NOT NULL, used_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_user_recovery_codes_user ON user_recovery_codes(user_id);