Files
Atlas/internal/store/store.go
T
2026-06-29 11:16:11 +00:00

152 lines
3.5 KiB
Go

package store
import (
"context"
"database/sql"
"embed"
"fmt"
"os"
"path/filepath"
"github.com/pressly/goose/v3"
_ "modernc.org/sqlite"
)
//go:embed migrations/*.sql
var migrationsFS embed.FS
type Store struct {
db *sql.DB
}
func Open(dataDir string) (*Store, error) {
if err := os.MkdirAll(dataDir, 0o755); err != nil {
return nil, err
}
dbPath := filepath.Join(dataDir, "app.db")
db, err := sql.Open("sqlite", dbPath+"?_pragma=foreign_keys(1)&_pragma=journal_mode(WAL)&_pragma=busy_timeout(5000)")
if err != nil {
return nil, err
}
db.SetMaxOpenConns(1)
s := &Store{db: db}
if err := s.migrate(); err != nil {
db.Close()
return nil, err
}
return s, nil
}
func (s *Store) DB() *sql.DB { return s.db }
func (s *Store) Close() error { return s.db.Close() }
func (s *Store) migrate() error {
goose.SetBaseFS(migrationsFS)
if err := goose.SetDialect("sqlite3"); err != nil {
return err
}
return goose.Up(s.db, "migrations")
}
func (s *Store) GetSetting(ctx context.Context, key string) (string, error) {
var value string
err := s.db.QueryRowContext(ctx, `SELECT value FROM settings WHERE key = ?`, key).Scan(&value)
return value, err
}
func (s *Store) GetSettings(ctx context.Context) (map[string]string, error) {
rows, err := s.db.QueryContext(ctx, `SELECT key, value FROM settings`)
if err != nil {
return nil, err
}
defer rows.Close()
out := make(map[string]string)
for rows.Next() {
var k, v string
if err := rows.Scan(&k, &v); err != nil {
return nil, err
}
out[k] = v
}
return out, rows.Err()
}
func (s *Store) SetSettings(ctx context.Context, settings map[string]string) error {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
stmt, err := tx.PrepareContext(ctx, `INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value`)
if err != nil {
return err
}
defer stmt.Close()
for k, v := range settings {
if _, err := stmt.ExecContext(ctx, k, v); err != nil {
return err
}
}
return tx.Commit()
}
func (s *Store) ListAuditLogs(ctx context.Context, limit, offset int) ([]AuditLog, int, error) {
var total int
if err := s.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM audit_logs`).Scan(&total); err != nil {
return nil, 0, err
}
rows, err := s.db.QueryContext(ctx,
`SELECT id, action, resource, detail, created_at FROM audit_logs ORDER BY id DESC LIMIT ? OFFSET ?`,
limit, offset,
)
if err != nil {
return nil, 0, err
}
defer rows.Close()
var items []AuditLog
for rows.Next() {
var item AuditLog
if err := rows.Scan(&item.ID, &item.Action, &item.Resource, &item.Detail, &item.CreatedAt); err != nil {
return nil, 0, err
}
items = append(items, item)
}
return items, total, rows.Err()
}
func (s *Store) InsertAuditLog(ctx context.Context, action, resource, detail string) error {
_, err := s.db.ExecContext(ctx,
`INSERT INTO audit_logs (action, resource, detail) VALUES (?, ?, ?)`,
action, resource, detail,
)
return err
}
type AuditLog struct {
ID int64 `json:"id"`
Action string `json:"action"`
Resource string `json:"resource"`
Detail string `json:"detail"`
CreatedAt string `json:"created_at"`
}
func MaskSecret(value string) string {
if value == "" {
return ""
}
if len(value) <= 4 {
return "****"
}
return value[:2] + "****" + value[len(value)-2:]
}
func ValidateSettingKey(key string) error {
switch key {
case "app_name", "api_port", "admin_password", "auth_enabled", "ui_locale":
return nil
default:
return fmt.Errorf("unknown setting: %s", key)
}
}