add auth
This commit is contained in:
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS users;
|
||||
@@ -0,0 +1,31 @@
|
||||
-- Required Postgres extensions:
|
||||
-- pgcrypto → gen_random_uuid() for primary keys
|
||||
-- citext → case-insensitive emails (so "Foo@x.com" == "foo@x.com")
|
||||
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||
CREATE EXTENSION IF NOT EXISTS citext;
|
||||
|
||||
-- Users table. Stores all auth + profile data.
|
||||
--
|
||||
-- Fields:
|
||||
-- email unique, citext (case-insensitive)
|
||||
-- password_hash bcrypt hash; never logged or returned in API responses
|
||||
-- email_verified_at NULL until the user clicks the verification link
|
||||
-- avatar_url absolute URL to a file in the R2 bucket (or NULL)
|
||||
-- token_version incremented whenever issued JWTs should be invalidated
|
||||
-- (password change, forced logout). JWTs carry this value
|
||||
-- as a "tv" claim; mismatch ⇒ middleware rejects the token.
|
||||
-- role "user" | "admin" | "moderator"
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
email CITEXT NOT NULL UNIQUE,
|
||||
password_hash TEXT NOT NULL,
|
||||
name VARCHAR(120) NOT NULL,
|
||||
avatar_url TEXT,
|
||||
role VARCHAR(32) NOT NULL DEFAULT 'user',
|
||||
email_verified_at TIMESTAMPTZ,
|
||||
token_version INTEGER NOT NULL DEFAULT 1,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS users_created_at_idx ON users (created_at DESC);
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS auth_tokens;
|
||||
@@ -0,0 +1,28 @@
|
||||
-- One-shot tokens used by the forgot-password and email-verification flows.
|
||||
--
|
||||
-- We never store the raw token; only the SHA-256 hash. The plaintext lives
|
||||
-- only in the email link. This means a DB leak can't be replayed against the
|
||||
-- live flow.
|
||||
--
|
||||
-- `purpose` discriminates the two flows so we can keep one narrow table:
|
||||
-- 'password_reset' → expires in ~1h
|
||||
-- 'email_verification' → expires in ~24h
|
||||
--
|
||||
-- `used_at` is set the first time a token is consumed. The handler must check
|
||||
-- both `used_at IS NULL` AND `expires_at > now()` before honouring a token.
|
||||
CREATE TABLE IF NOT EXISTS auth_tokens (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
purpose VARCHAR(32) NOT NULL,
|
||||
token_hash CHAR(64) NOT NULL UNIQUE,
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
used_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
CHECK (purpose IN ('password_reset', 'email_verification'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS auth_tokens_user_purpose_idx
|
||||
ON auth_tokens (user_id, purpose);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS auth_tokens_expires_at_idx
|
||||
ON auth_tokens (expires_at);
|
||||
Reference in New Issue
Block a user