119 lines
2.8 KiB
Go
119 lines
2.8 KiB
Go
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()
|
|
}
|