-- +goose Up -- Phone verification tables (privacy-first: only hashed phones stored) -- Stores phone verification status (hashed phone only, no plaintext) CREATE TABLE phone_verifications ( did TEXT PRIMARY KEY REFERENCES users(did) ON DELETE CASCADE, phone_hash TEXT UNIQUE NOT NULL, -- HMAC-SHA256(phone, pepper) for duplicate detection verified_at TIMESTAMP WITH TIME ZONE NOT NULL, expires_at TIMESTAMP WITH TIME ZONE NOT NULL, -- Annual re-verification (365 days) created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); -- Index for checking if phone is already verified (duplicate prevention) CREATE INDEX idx_phone_verifications_phone_hash ON phone_verifications(phone_hash); CREATE INDEX idx_phone_verifications_expires_at ON phone_verifications(expires_at); -- Stores pending verification requests (short-lived, auto-cleanup) CREATE TABLE phone_verification_requests ( request_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), did TEXT NOT NULL REFERENCES users(did) ON DELETE CASCADE, phone_hash TEXT NOT NULL, -- Same hash as phone_verifications for duplicate check otp_code_hash TEXT NOT NULL, -- Bcrypt hash of OTP code attempts INT DEFAULT 0, -- Track failed attempts (max 3) created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, expires_at TIMESTAMP WITH TIME ZONE NOT NULL -- OTP expires in 10 minutes ); CREATE INDEX idx_phone_verification_requests_did ON phone_verification_requests(did); CREATE INDEX idx_phone_verification_requests_expires_at ON phone_verification_requests(expires_at); -- Rate limiting table (prevent SMS abuse) CREATE TABLE phone_verification_rate_limits ( identifier TEXT PRIMARY KEY, -- Could be DID or phone_hash request_count INT DEFAULT 1, window_start TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, last_request_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_phone_verification_rate_limits_window ON phone_verification_rate_limits(window_start); -- Audit log for security monitoring CREATE TABLE phone_verification_audit_log ( id BIGSERIAL PRIMARY KEY, did TEXT REFERENCES users(did) ON DELETE SET NULL, event_type TEXT NOT NULL, -- 'request_sent', 'verification_success', 'verification_failed', 'rate_limit_hit' phone_hash TEXT, -- Optional, for tracking abuse patterns ip_address INET, -- Track source IP for fraud detection user_agent TEXT, metadata JSONB, -- Additional context (error details, etc.) created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_phone_verification_audit_log_did ON phone_verification_audit_log(did); CREATE INDEX idx_phone_verification_audit_log_event_type ON phone_verification_audit_log(event_type); CREATE INDEX idx_phone_verification_audit_log_created_at ON phone_verification_audit_log(created_at); -- Cleanup function for expired requests (run via cron) CREATE OR REPLACE FUNCTION cleanup_expired_verification_requests() RETURNS void AS $$ BEGIN DELETE FROM phone_verification_requests WHERE expires_at < NOW(); DELETE FROM phone_verification_rate_limits WHERE window_start < NOW() - INTERVAL '1 hour'; END; $$ LANGUAGE plpgsql; -- +goose Down DROP FUNCTION IF EXISTS cleanup_expired_verification_requests(); DROP TABLE IF EXISTS phone_verification_audit_log; DROP TABLE IF EXISTS phone_verification_rate_limits; DROP TABLE IF EXISTS phone_verification_requests; DROP TABLE IF EXISTS phone_verifications;