-- Phase 1: auth foundation -- Requires Postgres. citext for case-insensitive email; pgcrypto for gen_random_uuid. CREATE EXTENSION IF NOT EXISTS citext; CREATE EXTENSION IF NOT EXISTS pgcrypto; CREATE TABLE users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), email CITEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, display_name TEXT, email_verified BOOLEAN NOT NULL DEFAULT FALSE, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- One row per user; created on registration. CREATE TABLE user_settings ( user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, locale TEXT NOT NULL DEFAULT 'de', -- 'de' | 'en' currency TEXT NOT NULL DEFAULT 'EUR', theme TEXT NOT NULL DEFAULT 'breakcore', notify_email BOOLEAN NOT NULL DEFAULT TRUE, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- Email verification + password reset tokens. -- Only the SHA-256 hash of the token is stored; raw token lives in the emailed link. CREATE TYPE token_purpose AS ENUM ('verify_email', 'password_reset'); CREATE TABLE auth_tokens ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, token_hash BYTEA NOT NULL UNIQUE, purpose token_purpose NOT NULL, expires_at TIMESTAMPTZ NOT NULL, consumed_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX idx_auth_tokens_user ON auth_tokens(user_id); CREATE INDEX idx_auth_tokens_expires ON auth_tokens(expires_at); -- Touch updated_at on row changes. CREATE OR REPLACE FUNCTION set_updated_at() RETURNS trigger AS $$ BEGIN NEW.updated_at = now(); RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER trg_users_updated BEFORE UPDATE ON users FOR EACH ROW EXECUTE FUNCTION set_updated_at(); CREATE TRIGGER trg_user_settings_updated BEFORE UPDATE ON user_settings FOR EACH ROW EXECUTE FUNCTION set_updated_at();