package db import ( "database/sql" "fmt" "os" "path/filepath" _ "modernc.org/sqlite" ) // Database wraps the SQLite connection and provides migration helpers. type Database struct { DB *sql.DB } // Initialize opens (or creates) the SQLite database at the given path. func Initialize(dbPath string) (*Database, error) { // Ensure the data directory exists dir := filepath.Dir(dbPath) if err := os.MkdirAll(dir, 0755); err != nil { return nil, fmt.Errorf("create data directory: %w", err) } db, err := sql.Open("sqlite", dbPath) if err != nil { return nil, fmt.Errorf("open database: %w", err) } // Enable WAL mode for better concurrency if _, err := db.Exec("PRAGMA journal_mode=WAL"); err != nil { return nil, fmt.Errorf("enable WAL mode: %w", err) } // Enable foreign keys if _, err := db.Exec("PRAGMA foreign_keys=ON"); err != nil { return nil, fmt.Errorf("enable foreign keys: %w", err) } return &Database{DB: db}, nil } // Migrate runs automatic schema migrations on startup. func (d *Database) Migrate() error { migrations := []string{ `users`, `sessions`, `audit_logs`, } // Verify all required tables exist for _, table := range migrations { if err := d.ensureTable(table); err != nil { return fmt.Errorf("ensure table %s: %w", table, err) } } return nil } func (d *Database) ensureTable(name string) error { switch name { case "users": // Create table if it doesn't exist _, err := d.DB.Exec(` CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, display_name TEXT NOT NULL DEFAULT '', email TEXT NOT NULL DEFAULT '', role TEXT NOT NULL DEFAULT 'user', groups TEXT NOT NULL DEFAULT '', password_hash TEXT NOT NULL, disabled INTEGER NOT NULL DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ) `) if err != nil { return err } // Migrate: add role column if missing (for existing databases) d.DB.Exec(`ALTER TABLE users ADD COLUMN role TEXT NOT NULL DEFAULT 'user'`) return nil case "sessions": _, err := d.DB.Exec(` CREATE TABLE IF NOT EXISTS sessions ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL, token_hash TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, expires_at DATETIME NOT NULL ) `) return err case "audit_logs": _, err := d.DB.Exec(` CREATE TABLE IF NOT EXISTS audit_logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, action TEXT NOT NULL, actor TEXT NOT NULL, target TEXT, details TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) `) return err } return fmt.Errorf("unknown table: %s", name) } // Close cleanly shuts down the database connection. func (d *Database) Close() error { return d.DB.Close() }