package db import ( "database/sql" "errors" "strings" "time" _ "modernc.org/sqlite" ) var ErrUserNotFound = errors.New("user not found") var ErrUserExists = errors.New("username already exists") // User represents a local account stored in SQLite. type User struct { ID int64 Username string PassHash string Role string CreatedAt time.Time } // DB wraps the SQLite connection. type DB struct { sql *sql.DB } // New opens (or creates) the SQLite database at path and runs migrations. func New(path string) (*DB, error) { sqldb, err := sql.Open("sqlite", path+"?_journal=WAL&_timeout=5000") if err != nil { return nil, err } d := &DB{sql: sqldb} if err := d.init(); err != nil { sqldb.Close() return nil, err } return d, nil } func (d *DB) Close() error { return d.sql.Close() } func (d *DB) init() error { _, err := d.sql.Exec(` CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL UNIQUE, pass_hash TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'user', created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) `) return err } // HasUsers returns true if at least one user account exists. func (d *DB) HasUsers() bool { var n int d.sql.QueryRow(`SELECT COUNT(*) FROM users`).Scan(&n) return n > 0 } // CreateUser inserts a new user. Returns ErrUserExists if the username is taken. func (d *DB) CreateUser(username, passHash, role string) error { _, err := d.sql.Exec( `INSERT INTO users (username, pass_hash, role) VALUES (?, ?, ?)`, username, passHash, role, ) if err != nil && strings.Contains(err.Error(), "UNIQUE constraint failed") { return ErrUserExists } return err } // GetUser fetches a single user by username. func (d *DB) GetUser(username string) (*User, error) { row := d.sql.QueryRow( `SELECT id, username, pass_hash, role, created_at FROM users WHERE username = ?`, username, ) u := &User{} var createdAt string err := row.Scan(&u.ID, &u.Username, &u.PassHash, &u.Role, &createdAt) if errors.Is(err, sql.ErrNoRows) { return nil, ErrUserNotFound } if err != nil { return nil, err } u.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", createdAt) return u, nil } // ListUsers returns all users ordered by id. func (d *DB) ListUsers() ([]*User, error) { rows, err := d.sql.Query( `SELECT id, username, role, created_at FROM users ORDER BY id`, ) if err != nil { return nil, err } defer rows.Close() var users []*User for rows.Next() { u := &User{} var createdAt string if err := rows.Scan(&u.ID, &u.Username, &u.Role, &createdAt); err != nil { return nil, err } u.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", createdAt) users = append(users, u) } return users, rows.Err() } // DeleteUser removes a user. Returns ErrUserNotFound if they don't exist. func (d *DB) DeleteUser(username string) error { res, err := d.sql.Exec(`DELETE FROM users WHERE username = ?`, username) if err != nil { return err } n, _ := res.RowsAffected() if n == 0 { return ErrUserNotFound } return nil } // UpdatePassword replaces the stored hash for a user. func (d *DB) UpdatePassword(username, passHash string) error { res, err := d.sql.Exec( `UPDATE users SET pass_hash = ? WHERE username = ?`, passHash, username, ) if err != nil { return err } n, _ := res.RowsAffected() if n == 0 { return ErrUserNotFound } return nil } // UserCount returns the total number of local accounts. func (d *DB) UserCount() (int, error) { var n int err := d.sql.QueryRow(`SELECT COUNT(*) FROM users`).Scan(&n) return n, err }