package database import ( "database/sql" "fmt" "os" "path/filepath" _ "modernc.org/sqlite" ) // DB is the global database connection pointer. var DB *sql.DB // InitDB initializes the SQLite database, creates the data directory, // opens the connection, sets WAL mode, and creates default tables and data. func InitDB() error { dbPath := filepath.Join(".", "data", "portal.db") // Create data directory if not exists if err := os.MkdirAll(filepath.Dir(dbPath), 0755); err != nil { return fmt.Errorf("failed to create data directory: %w", err) } var err error DB, err = sql.Open("sqlite", dbPath) if err != nil { return fmt.Errorf("failed to open database: %w", err) } // SQLite requires max open conns = 1 for safe writes DB.SetMaxOpenConns(1) // Enable WAL mode for better concurrent read performance if _, err := DB.Exec("PRAGMA journal_mode=WAL"); err != nil { return fmt.Errorf("failed to set WAL mode: %w", err) } // Create tables if err := createTables(); err != nil { return fmt.Errorf("failed to create tables: %w", err) } // Seed default data if err := seedData(); err != nil { return fmt.Errorf("failed to seed data: %w", err) } return nil } // createTables creates the cards, settings, and admins tables. func createTables() error { _, err := DB.Exec(` CREATE TABLE IF NOT EXISTS cards ( id INTEGER PRIMARY KEY AUTOINCREMENT, icon TEXT, title TEXT NOT NULL, subtitle TEXT, url TEXT NOT NULL, sort INTEGER DEFAULT 0, enabled INTEGER DEFAULT 1, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS settings ( key TEXT PRIMARY KEY, value TEXT ); CREATE TABLE IF NOT EXISTS admins ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS login_logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, admin_id INTEGER, username TEXT NOT NULL, ip TEXT NOT NULL, user_agent TEXT, success INTEGER NOT NULL DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS ip_bans ( id INTEGER PRIMARY KEY AUTOINCREMENT, ip TEXT NOT NULL, reason TEXT, fail_count INTEGER DEFAULT 0, banned_until DATETIME NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS ip_whitelist ( id INTEGER PRIMARY KEY AUTOINCREMENT, ip TEXT NOT NULL, comment TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS access_logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, ip TEXT NOT NULL, user_agent TEXT, action_type TEXT NOT NULL, detail TEXT DEFAULT '', referer TEXT DEFAULT '', created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX IF NOT EXISTS idx_access_logs_ip ON access_logs(ip); CREATE INDEX IF NOT EXISTS idx_access_logs_action_type ON access_logs(action_type); CREATE INDEX IF NOT EXISTS idx_access_logs_created_at ON access_logs(created_at); `) return err } // seedData inserts default admin account and search engine setting if not present. func seedData() error { // Insert default admin if not exists var count int err := DB.QueryRow("SELECT COUNT(*) FROM admins WHERE username = ?", "admin").Scan(&count) if err != nil { return err } if count == 0 { _, err = DB.Exec( "INSERT INTO admins (username, password) VALUES (?, ?)", "admin", "$2a$10$h3Csm2HmWUtvim3MJ8VG0OHx/tevZorlUXQVDtN2EgWhROtiM3Sg.", // bcrypt hash for "admin123" ) if err != nil { return err } } // Insert default search engine setting if not exists var settingCount int err = DB.QueryRow("SELECT COUNT(*) FROM settings WHERE key = ?", "search_engine").Scan(&settingCount) if err != nil { return err } if settingCount == 0 { _, err = DB.Exec( "INSERT INTO settings (key, value) VALUES (?, ?)", "search_engine", "https://www.google.com/search?q=%s", ) if err != nil { return err } } // Insert default homepage settings if not exists defaultSettings := []struct { key string value string }{ {"homepage_title", "Portal"}, {"homepage_subtitle", ""}, {"homepage_background", ""}, } for _, s := range defaultSettings { var cnt int err = DB.QueryRow("SELECT COUNT(*) FROM settings WHERE key = ?", s.key).Scan(&cnt) if err != nil { return err } if cnt == 0 { _, err = DB.Exec("INSERT INTO settings (key, value) VALUES (?, ?)", s.key, s.value) if err != nil { return err } } } return nil } // CloseDB closes the database connection. func CloseDB() error { if DB != nil { return DB.Close() } return nil }