"""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")