Files
BanGUI/backend/app/db.py
Lukas 7392c930d6 feat: Stage 1 — backend and frontend scaffolding
Backend (tasks 1.1, 1.5–1.8):
- pyproject.toml with FastAPI, Pydantic v2, aiosqlite, APScheduler 3.x,
  structlog, bcrypt; ruff + mypy strict configured
- Pydantic Settings (BANGUI_ prefix env vars, fail-fast validation)
- SQLite schema: settings, sessions, blocklist_sources, import_log;
  WAL mode + foreign keys; idempotent init_db()
- FastAPI app factory with lifespan (DB, aiohttp session, scheduler),
  CORS, unhandled-exception handler, GET /api/health
- Fail2BanClient: async Unix-socket wrapper using run_in_executor,
  custom error types, async context manager
- Utility modules: ip_utils, time_utils, constants
- 47 tests; ruff 0 errors; mypy --strict 0 errors

Frontend (tasks 1.2–1.4):
- Vite + React 18 + TypeScript strict; Fluent UI v9; ESLint + Prettier
- Custom brand theme (#0F6CBD, WCAG AA contrast) with light/dark variants
- Typed fetch API client (ApiError, get/post/put/del) + endpoints constants
- tsc --noEmit 0 errors
2026-02-28 21:15:01 +01:00

101 lines
3.4 KiB
Python

"""Application database schema definition and initialisation.
BanGUI maintains its own SQLite database that stores configuration, session
state, blocklist source definitions, and import run logs. This module is
the single source of truth for the schema — all ``CREATE TABLE`` statements
live here and are applied on first run via :func:`init_db`.
The fail2ban database is separate and is accessed read-only by the history
and ban services.
"""
import aiosqlite
import structlog
log: structlog.stdlib.BoundLogger = structlog.get_logger()
# ---------------------------------------------------------------------------
# DDL statements
# ---------------------------------------------------------------------------
_CREATE_SETTINGS: str = """
CREATE TABLE IF NOT EXISTS settings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
key TEXT NOT NULL UNIQUE,
value TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
);
"""
_CREATE_SESSIONS: str = """
CREATE TABLE IF NOT EXISTS sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
token TEXT NOT NULL UNIQUE,
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
expires_at TEXT NOT NULL
);
"""
_CREATE_SESSIONS_TOKEN_INDEX: str = """
CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_token ON sessions (token);
"""
_CREATE_BLOCKLIST_SOURCES: str = """
CREATE TABLE IF NOT EXISTS blocklist_sources (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
url TEXT NOT NULL UNIQUE,
enabled INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
);
"""
_CREATE_IMPORT_LOG: str = """
CREATE TABLE IF NOT EXISTS import_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source_id INTEGER REFERENCES blocklist_sources(id) ON DELETE SET NULL,
source_url TEXT NOT NULL,
timestamp TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
ips_imported INTEGER NOT NULL DEFAULT 0,
ips_skipped INTEGER NOT NULL DEFAULT 0,
errors TEXT
);
"""
# Ordered list of DDL statements to execute on initialisation.
_SCHEMA_STATEMENTS: list[str] = [
_CREATE_SETTINGS,
_CREATE_SESSIONS,
_CREATE_SESSIONS_TOKEN_INDEX,
_CREATE_BLOCKLIST_SOURCES,
_CREATE_IMPORT_LOG,
]
# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------
async def init_db(db: aiosqlite.Connection) -> None:
"""Create all BanGUI application tables if they do not already exist.
This function is idempotent — calling it on an already-initialised
database has no effect. It should be called once during application
startup inside the FastAPI lifespan handler.
Args:
db: An open :class:`aiosqlite.Connection` to the application database.
"""
log.info("initialising_database_schema")
async with db.execute("PRAGMA journal_mode=WAL;"):
pass
async with db.execute("PRAGMA foreign_keys=ON;"):
pass
for statement in _SCHEMA_STATEMENTS:
await db.executescript(statement)
await db.commit()
log.info("database_schema_ready")