diff --git a/db_migrations/20260408_broker_callback_state.sql b/db_migrations/20260408_broker_callback_state.sql new file mode 100644 index 0000000..032c86d --- /dev/null +++ b/db_migrations/20260408_broker_callback_state.sql @@ -0,0 +1,18 @@ +BEGIN; + +CREATE TABLE IF NOT EXISTS broker_callback_state ( + id TEXT PRIMARY KEY, + state_hash TEXT NOT NULL UNIQUE, + user_id TEXT NOT NULL REFERENCES app_user(id) ON DELETE CASCADE, + session_id TEXT NOT NULL REFERENCES app_session(id) ON DELETE CASCADE, + broker TEXT NOT NULL, + flow TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL, + expires_at TIMESTAMPTZ NOT NULL, + consumed_at TIMESTAMPTZ +); + +CREATE INDEX IF NOT EXISTS idx_broker_callback_state_lookup + ON broker_callback_state (user_id, session_id, broker, flow, expires_at DESC); + +COMMIT; diff --git a/db_migrations/20260408_execution_claims.sql b/db_migrations/20260408_execution_claims.sql new file mode 100644 index 0000000..29528ba --- /dev/null +++ b/db_migrations/20260408_execution_claims.sql @@ -0,0 +1,18 @@ +BEGIN; + +CREATE TABLE IF NOT EXISTS execution_claim ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL REFERENCES app_user(id) ON DELETE CASCADE, + run_id TEXT NOT NULL REFERENCES strategy_run(run_id) ON DELETE CASCADE, + mode TEXT NOT NULL, + logical_time TIMESTAMPTZ NOT NULL, + claimed_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE UNIQUE INDEX IF NOT EXISTS uq_execution_claim_scope + ON execution_claim (user_id, run_id, logical_time); + +CREATE INDEX IF NOT EXISTS idx_execution_claim_run_claimed + ON execution_claim (run_id, claimed_at DESC); + +COMMIT; diff --git a/db_migrations/20260408_run_leases.sql b/db_migrations/20260408_run_leases.sql new file mode 100644 index 0000000..28d784d --- /dev/null +++ b/db_migrations/20260408_run_leases.sql @@ -0,0 +1,14 @@ +BEGIN; + +CREATE TABLE IF NOT EXISTS run_leases ( + run_id TEXT PRIMARY KEY REFERENCES strategy_run(run_id) ON DELETE CASCADE, + owner_id TEXT NOT NULL, + leased_at TIMESTAMPTZ NOT NULL, + expires_at TIMESTAMPTZ NOT NULL, + heartbeat_at TIMESTAMPTZ +); + +CREATE INDEX IF NOT EXISTS idx_run_leases_owner_expires + ON run_leases (owner_id, expires_at DESC); + +COMMIT; diff --git a/db_migrations/20260408_support_request_audit.sql b/db_migrations/20260408_support_request_audit.sql new file mode 100644 index 0000000..d1b2faf --- /dev/null +++ b/db_migrations/20260408_support_request_audit.sql @@ -0,0 +1,20 @@ +BEGIN; + +CREATE TABLE IF NOT EXISTS support_request_audit ( + id BIGSERIAL PRIMARY KEY, + endpoint TEXT NOT NULL, + ip_hash TEXT, + email_hash TEXT, + ticket_hash TEXT, + blocked BOOLEAN NOT NULL DEFAULT FALSE, + reason TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_support_request_audit_endpoint_ip_created + ON support_request_audit (endpoint, ip_hash, created_at DESC); + +CREATE INDEX IF NOT EXISTS idx_support_request_audit_ticket_created + ON support_request_audit (ticket_hash, created_at DESC); + +COMMIT; diff --git a/schema.sql b/schema.sql index 01f0a5c..9964de0 100644 --- a/schema.sql +++ b/schema.sql @@ -288,3 +288,106 @@ CREATE TABLE IF NOT EXISTS market_close ( CREATE INDEX IF NOT EXISTS idx_market_close_symbol ON market_close(symbol); CREATE INDEX IF NOT EXISTS idx_market_close_date ON market_close(date); + +-- ========================================= +-- 9) Bootstrap compatibility patch +-- Keeps bootstrap schema aligned with later migrations. +-- ========================================= + +ALTER TABLE app_session + ADD COLUMN IF NOT EXISTS ip TEXT, + ADD COLUMN IF NOT EXISTS user_agent TEXT; + +ALTER TABLE user_broker + ADD COLUMN IF NOT EXISTS api_secret TEXT, + ADD COLUMN IF NOT EXISTS auth_state TEXT; + +CREATE TABLE IF NOT EXISTS password_reset_otp ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + email TEXT NOT NULL, + otp_hash TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + expires_at TIMESTAMPTZ NOT NULL, + used_at TIMESTAMPTZ +); + +CREATE INDEX IF NOT EXISTS idx_password_reset_otp_email + ON password_reset_otp(email); + +CREATE INDEX IF NOT EXISTS idx_password_reset_otp_expires_at + ON password_reset_otp(expires_at); + +CREATE TABLE IF NOT EXISTS support_ticket ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name TEXT NOT NULL, + email TEXT NOT NULL, + subject TEXT NOT NULL, + message TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'NEW', + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE INDEX IF NOT EXISTS idx_support_ticket_email + ON support_ticket(email); + +CREATE INDEX IF NOT EXISTS idx_support_ticket_created_at + ON support_ticket(created_at DESC); + +CREATE TABLE IF NOT EXISTS broker_callback_state ( + id TEXT PRIMARY KEY, + state_hash TEXT NOT NULL UNIQUE, + user_id TEXT NOT NULL REFERENCES app_user(id) ON DELETE CASCADE, + session_id TEXT NOT NULL REFERENCES app_session(id) ON DELETE CASCADE, + broker TEXT NOT NULL, + flow TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL, + expires_at TIMESTAMPTZ NOT NULL, + consumed_at TIMESTAMPTZ +); + +CREATE INDEX IF NOT EXISTS idx_broker_callback_state_lookup + ON broker_callback_state(user_id, session_id, broker, flow, expires_at DESC); + +CREATE TABLE IF NOT EXISTS execution_claim ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL REFERENCES app_user(id) ON DELETE CASCADE, + run_id TEXT NOT NULL REFERENCES strategy_run(run_id) ON DELETE CASCADE, + mode TEXT NOT NULL, + logical_time TIMESTAMPTZ NOT NULL, + claimed_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE UNIQUE INDEX IF NOT EXISTS uq_execution_claim_scope + ON execution_claim(user_id, run_id, logical_time); + +CREATE INDEX IF NOT EXISTS idx_execution_claim_run_claimed + ON execution_claim(run_id, claimed_at DESC); + +CREATE TABLE IF NOT EXISTS run_leases ( + run_id TEXT PRIMARY KEY REFERENCES strategy_run(run_id) ON DELETE CASCADE, + owner_id TEXT NOT NULL, + leased_at TIMESTAMPTZ NOT NULL, + expires_at TIMESTAMPTZ NOT NULL, + heartbeat_at TIMESTAMPTZ +); + +CREATE INDEX IF NOT EXISTS idx_run_leases_owner_expires + ON run_leases(owner_id, expires_at DESC); + +CREATE TABLE IF NOT EXISTS support_request_audit ( + id BIGSERIAL PRIMARY KEY, + endpoint TEXT NOT NULL, + ip_hash TEXT, + email_hash TEXT, + ticket_hash TEXT, + blocked BOOLEAN NOT NULL DEFAULT FALSE, + reason TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE INDEX IF NOT EXISTS idx_support_request_audit_endpoint_ip_created + ON support_request_audit(endpoint, ip_hash, created_at DESC); + +CREATE INDEX IF NOT EXISTS idx_support_request_audit_ticket_created + ON support_request_audit(ticket_hash, created_at DESC);