from __future__ import annotations from contextlib import contextmanager from typing import Any, Iterator import psycopg from psycopg.rows import dict_row from . import settings def configured() -> bool: return bool(settings.PORTAL_DATABASE_URL) @contextmanager def connect() -> Iterator[psycopg.Connection[Any]]: if not settings.PORTAL_DATABASE_URL: raise RuntimeError("portal database not configured") with psycopg.connect(settings.PORTAL_DATABASE_URL, row_factory=dict_row) as conn: yield conn def ensure_schema() -> None: if not settings.PORTAL_DATABASE_URL: return with connect() as conn: conn.execute( """ CREATE TABLE IF NOT EXISTS access_requests ( request_code TEXT PRIMARY KEY, username TEXT NOT NULL, contact_email TEXT, note TEXT, status TEXT NOT NULL, email_verification_token_hash TEXT, email_verification_sent_at TIMESTAMPTZ, email_verified_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), decided_at TIMESTAMPTZ, decided_by TEXT, initial_password TEXT, initial_password_revealed_at TIMESTAMPTZ, provision_attempted_at TIMESTAMPTZ ) """ ) conn.execute("ALTER TABLE access_requests ADD COLUMN IF NOT EXISTS initial_password TEXT") conn.execute("ALTER TABLE access_requests ADD COLUMN IF NOT EXISTS initial_password_revealed_at TIMESTAMPTZ") conn.execute("ALTER TABLE access_requests ADD COLUMN IF NOT EXISTS provision_attempted_at TIMESTAMPTZ") conn.execute("ALTER TABLE access_requests ADD COLUMN IF NOT EXISTS email_verification_token_hash TEXT") conn.execute("ALTER TABLE access_requests ADD COLUMN IF NOT EXISTS email_verification_sent_at TIMESTAMPTZ") conn.execute("ALTER TABLE access_requests ADD COLUMN IF NOT EXISTS email_verified_at TIMESTAMPTZ") conn.execute( """ CREATE TABLE IF NOT EXISTS access_request_tasks ( request_code TEXT NOT NULL REFERENCES access_requests(request_code) ON DELETE CASCADE, task TEXT NOT NULL, status TEXT NOT NULL, detail TEXT, updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), PRIMARY KEY (request_code, task) ) """ ) conn.execute( """ CREATE TABLE IF NOT EXISTS access_request_onboarding_steps ( request_code TEXT NOT NULL REFERENCES access_requests(request_code) ON DELETE CASCADE, step TEXT NOT NULL, completed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), PRIMARY KEY (request_code, step) ) """ ) conn.execute( """ CREATE TABLE IF NOT EXISTS access_request_onboarding_artifacts ( request_code TEXT NOT NULL REFERENCES access_requests(request_code) ON DELETE CASCADE, artifact TEXT NOT NULL, value_hash TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), PRIMARY KEY (request_code, artifact) ) """ ) conn.execute( """ CREATE INDEX IF NOT EXISTS access_requests_status_created_at ON access_requests (status, created_at) """ ) conn.execute( """ CREATE INDEX IF NOT EXISTS access_request_tasks_request_code ON access_request_tasks (request_code) """ ) conn.execute( """ CREATE INDEX IF NOT EXISTS access_request_onboarding_steps_request_code ON access_request_onboarding_steps (request_code) """ ) conn.execute( """ CREATE INDEX IF NOT EXISTS access_request_onboarding_artifacts_request_code ON access_request_onboarding_artifacts (request_code) """ ) conn.execute( """ CREATE UNIQUE INDEX IF NOT EXISTS access_requests_username_pending ON access_requests (username) WHERE status = 'pending' """ )