- 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
100 lines
3.2 KiB
Python
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()
|