80 lines
3.1 KiB
SQL
80 lines
3.1 KiB
SQL
-- 001_create_users.sql
|
|
-- Auth tables for user management, sessions, and authentication codes.
|
|
-- Compatible with both PostgreSQL (local dev) and CockroachDB (production).
|
|
|
|
-- Core user identity. Email is the primary identifier for humans.
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id TEXT PRIMARY KEY,
|
|
email TEXT NOT NULL UNIQUE,
|
|
email_verified BOOL NOT NULL DEFAULT FALSE,
|
|
name TEXT NOT NULL DEFAULT '',
|
|
avatar_url TEXT NOT NULL DEFAULT '',
|
|
status TEXT NOT NULL DEFAULT 'active',
|
|
last_login_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
-- Password credentials. Separate table because OAuth-only users have no password.
|
|
CREATE TABLE IF NOT EXISTS user_passwords (
|
|
user_id TEXT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
|
|
password_hash TEXT NOT NULL,
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
-- OAuth provider connections (Google, GitHub, Apple, etc.).
|
|
CREATE TABLE IF NOT EXISTS oauth_connections (
|
|
id TEXT PRIMARY KEY,
|
|
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
provider TEXT NOT NULL,
|
|
provider_user_id TEXT NOT NULL,
|
|
provider_email TEXT NOT NULL DEFAULT '',
|
|
access_token TEXT NOT NULL DEFAULT '',
|
|
refresh_token TEXT NOT NULL DEFAULT '',
|
|
token_expires_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
UNIQUE (provider, provider_user_id)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_oauth_connections_user_id ON oauth_connections (user_id);
|
|
|
|
-- Verification codes for OTP login, magic links, password reset, and email verification.
|
|
CREATE TABLE IF NOT EXISTS auth_codes (
|
|
id TEXT PRIMARY KEY,
|
|
user_id TEXT REFERENCES users(id) ON DELETE CASCADE,
|
|
email TEXT NOT NULL,
|
|
code TEXT NOT NULL,
|
|
purpose TEXT NOT NULL,
|
|
expires_at TIMESTAMPTZ NOT NULL,
|
|
used_at TIMESTAMPTZ,
|
|
ip_address TEXT NOT NULL DEFAULT '',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_auth_codes_email_purpose ON auth_codes (email, purpose, expires_at)
|
|
WHERE used_at IS NULL;
|
|
|
|
-- Sessions track where and when users are logged in.
|
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
id TEXT PRIMARY KEY,
|
|
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
ip_address TEXT NOT NULL DEFAULT '',
|
|
user_agent TEXT NOT NULL DEFAULT '',
|
|
device_label TEXT NOT NULL DEFAULT '',
|
|
last_active_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
expires_at TIMESTAMPTZ NOT NULL,
|
|
revoked_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions (user_id)
|
|
WHERE revoked_at IS NULL;
|
|
|
|
-- User roles. Separate table so users can have multiple roles.
|
|
CREATE TABLE IF NOT EXISTS user_roles (
|
|
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
role TEXT NOT NULL,
|
|
granted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
PRIMARY KEY (user_id, role)
|
|
);
|