Files
dre/backend/db.py
T
root be0750d001 DRE customer portal: FastAPI + SQLite backend (18 endpoints)
- Magic-link auth (sha256-only token storage, 15-min single-use, 7-day sessions)
- Staff-key auth via X-DRE-Staff-Key (constant-time compare)
- SQLite WAL, foreign_keys, parameterized queries, atomic DRE/CLT sequence allocation
- Intake validator rejects SSN/PAN patterns (FDCPA/TDCPA compliance)
- Document upload allowlist + magic-byte check, 20MB cap
- Unified error envelope, money as integer cents
- systemd unit (port 8093, User=root, hardening directives)
- Fixes import bug (auth.py relative imports) and audit_log placeholder mismatch
2026-08-21 18:43:33 -04:00

100 lines
3.2 KiB
Python

"""SQLite connection helper — stdlib sqlite3, parameterized queries only.
PRAGMA foreign_keys=ON, journal_mode=WAL, busy_timeout=5000 on every connection.
No ORM. Initializes schema on first boot and seeds number_sequences for current year.
"""
from __future__ import annotations
import os
import sqlite3
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
_SCHEMA_FILE = Path(__file__).resolve().parent / "schema.sql"
def _utcnow_iso() -> str:
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
def get_db_path() -> str:
return os.environ.get("DRE_DB_PATH", "/opt/dre-portal/data/dre.db")
def get_upload_dir() -> str:
return os.environ.get("DRE_UPLOAD_DIR", "/opt/dre-portal/data/uploads")
def _apply_pragmas(conn: sqlite3.Connection) -> None:
conn.execute("PRAGMA foreign_keys = ON;")
conn.execute("PRAGMA journal_mode = WAL;")
conn.execute("PRAGMA busy_timeout = 5000;")
def get_conn() -> sqlite3.Connection:
"""Return a connection with pragmas applied and row factory."""
db_path = get_db_path()
Path(db_path).parent.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(db_path, timeout=5.0)
conn.row_factory = sqlite3.Row
_apply_pragmas(conn)
return conn
def init_db() -> None:
"""Create schema if absent and seed number_sequences for the current year."""
with get_conn() as conn:
schema_sql = _SCHEMA_FILE.read_text()
conn.executescript(schema_sql)
# Seed sequences for current year (idempotent)
year = datetime.now(timezone.utc).year
for prefix in ("DRE", "CLT"):
conn.execute(
"INSERT OR IGNORE INTO number_sequences (prefix, year, last_value) VALUES (?, ?, 0)",
(prefix, year),
)
conn.commit()
def new_uuid() -> str:
return str(uuid.uuid4())
def utcnow_iso() -> str:
return _utcnow_iso()
def next_sequence_number(conn: sqlite3.Connection, prefix: str) -> str:
"""Atomically allocate the next DRE-YYYY-NNNN / CLT-YYYY-NNNN in a single transaction.
Uses UPDATE ... RETURNING (SQLite 3.35+) — no races."""
year = datetime.now(timezone.utc).year
# Ensure row exists
conn.execute(
"INSERT OR IGNORE INTO number_sequences (prefix, year, last_value) VALUES (?, ?, 0)",
(prefix, year),
)
cur = conn.execute(
"UPDATE number_sequences SET last_value = last_value + 1 WHERE prefix = ? AND year = ? "
"RETURNING last_value",
(prefix, year),
)
row = cur.fetchone()
if row is None:
# Should not happen, but handle defensively
raise RuntimeError(f"failed to allocate sequence for {prefix}/{year}")
n = row[0] if isinstance(row, tuple) else row["last_value"]
val = n if isinstance(n, int) else int(n)
return f"{prefix}-{year}-{val:04d}"
def query_one(conn: sqlite3.Connection, sql: str, params: tuple[Any, ...] = ()) -> sqlite3.Row | None:
cur = conn.execute(sql, params)
return cur.fetchone()
def query_all(conn: sqlite3.Connection, sql: str, params: tuple[Any, ...] = ()) -> list[sqlite3.Row]:
cur = conn.execute(sql, params)
return cur.fetchall()