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
This commit is contained in:
100
backend/app/db.py
Normal file
100
backend/app/db.py
Normal file
@@ -0,0 +1,100 @@
|
||||
"""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")
|
||||
Reference in New Issue
Block a user