package db import ( "database/sql" "embed" "fmt" "log/slog" "os" "sort" _ "modernc.org/sqlite" "golang.org/x/crypto/bcrypt" ) //go:embed migrations/*.sql var migrations embed.FS func Open(path string) (*sql.DB, error) { db, err := sql.Open("sqlite", path+"?_journal_mode=WAL&_busy_timeout=5000") if err != nil { return nil, fmt.Errorf("open db: %w", err) } db.SetMaxOpenConns(1) if err := runMigrations(db); err != nil { return nil, fmt.Errorf("run migrations: %w", err) } if err := seedAdmin(db); err != nil { return nil, fmt.Errorf("seed admin: %w", err) } return db, nil } func runMigrations(db *sql.DB) error { entries, err := migrations.ReadDir("migrations") if err != nil { return err } sort.Slice(entries, func(i, j int) bool { return entries[i].Name() < entries[j].Name() }) for _, entry := range entries { if entry.IsDir() { continue } sqlBytes, err := migrations.ReadFile("migrations/" + entry.Name()) if err != nil { return err } if _, err := db.Exec(string(sqlBytes)); err != nil { return fmt.Errorf("migration %s: %w", entry.Name(), err) } slog.Info("migration applied", "file", entry.Name()) } return nil } func seedAdmin(db *sql.DB) error { var count int if err := db.QueryRow("SELECT COUNT(*) FROM users").Scan(&count); err != nil { return err } if count > 0 { return nil } password := os.Getenv("ADMIN_PASSWORD") if password == "" { slog.Warn("ADMIN_PASSWORD not set, admin user will have no password") } hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { return fmt.Errorf("hash password: %w", err) } _, err = db.Exec("INSERT INTO users (username, password_hash) VALUES (?, ?)", "admin", string(hash)) if err != nil { return fmt.Errorf("insert admin: %w", err) } slog.Info("admin user seeded") return nil }