package store import ( "crypto/rand" "database/sql" "encoding/hex" "fmt" _ "modernc.org/sqlite" "os" "sync" "time" ) type User struct { ID string `json:"id"` Username string `json:"username"` Admin bool `json:"admin"` Quota int64 `json:"quota_bytes"` Created time.Time `json:"created_at"` } type Instance struct { ID string `json:"id"` UID string `json:"uid"` Name string `json:"name"` Version string `json:"version"` ImageID string `json:"image_id"` Status string `json:"status"` ContainerID string `json:"container_id"` Created time.Time `json:"created_at"` } type Store struct { DB *sql.DB sessionTTL time.Duration closed chan struct{} closeOnce sync.Once } func Open(path string) (*Store, error) { ttl, err := sessionLifetime(os.Getenv("SESSION_TTL")) if err != nil { return nil, err } db, e := sql.Open("sqlite", path) if e != nil { return nil, e } db.SetMaxOpenConns(1) if _, e = db.Exec("PRAGMA busy_timeout=5000; PRAGMA journal_mode=WAL;"); e != nil { db.Close() return nil, e } _, e = db.Exec("PRAGMA foreign_keys=ON; CREATE TABLE IF NOT EXISTS users(id TEXT PRIMARY KEY,username TEXT UNIQUE NOT NULL,pass_hash TEXT NOT NULL,admin INTEGER NOT NULL,quota INTEGER NOT NULL,created INTEGER NOT NULL); CREATE TABLE IF NOT EXISTS sessions(id TEXT PRIMARY KEY,uid TEXT NOT NULL,csrf TEXT NOT NULL,expires INTEGER NOT NULL); CREATE TABLE IF NOT EXISTS instances(id TEXT PRIMARY KEY,uid TEXT NOT NULL,name TEXT NOT NULL,version TEXT,image_id TEXT,status TEXT,container_id TEXT,created INTEGER NOT NULL); CREATE TABLE IF NOT EXISTS audit(id INTEGER PRIMARY KEY,uid TEXT,action TEXT,target TEXT,detail TEXT,at INTEGER NOT NULL);") if e != nil { db.Close() return nil, e } return &Store{DB: db, sessionTTL: ttl, closed: make(chan struct{})}, nil } func sessionLifetime(value string) (time.Duration, error) { if value == "" { return 24 * time.Hour, nil } ttl, err := time.ParseDuration(value) if err != nil || ttl < time.Second || ttl%time.Second != 0 { return 0, fmt.Errorf("SESSION_TTL must be a positive whole-second duration, e.g. 24h or 168h") } return ttl, nil } func (s *Store) Close() { s.closeOnce.Do(func() { close(s.closed); s.DB.Close() }) } func (s *Store) Done() <-chan struct{} { return s.closed } func ID() string { b := make([]byte, 16); _, _ = rand.Read(b); return hex.EncodeToString(b) } func (s *Store) UserByName(n string) (User, error) { var u User var a, t int64 e := s.DB.QueryRow("SELECT id,username,admin,quota,created FROM users WHERE username=?", n).Scan(&u.ID, &u.Username, &a, &u.Quota, &t) u.Admin = a != 0 u.Created = time.Unix(t, 0) return u, e } func (s *Store) UserByID(id string) (User, error) { var u User var a, t int64 e := s.DB.QueryRow("SELECT id,username,admin,quota,created FROM users WHERE id=?", id).Scan(&u.ID, &u.Username, &a, &u.Quota, &t) u.Admin = a != 0 u.Created = time.Unix(t, 0) return u, e } func (s *Store) CreateUser(u User, hash string) error { _, e := s.DB.Exec("INSERT INTO users(id,username,pass_hash,admin,quota,created) VALUES(?,?,?,?,?,?)", u.ID, u.Username, hash, boolInt(u.Admin), u.Quota, time.Now().Unix()) return e } func boolInt(v bool) int { if v { return 1 } return 0 } func (s *Store) Password(name string) (string, error) { var p string e := s.DB.QueryRow("SELECT pass_hash FROM users WHERE username=?", name).Scan(&p) return p, e } func (s *Store) Session(uid string) (string, string, error) { id, csrf := ID(), ID() _, e := s.DB.Exec("INSERT INTO sessions VALUES(?,?,?,?)", id, uid, csrf, time.Now().Add(s.sessionTTL).Unix()) return id, csrf, e } func (s *Store) SessionUser(id string) (User, string, error) { var uid, csrf string var exp int64 e := s.DB.QueryRow("SELECT uid,csrf,expires FROM sessions WHERE id=?", id).Scan(&uid, &csrf, &exp) if e != nil || exp <= time.Now().Unix() { return User{}, "", sql.ErrNoRows } u, e := s.UserByID(uid) return u, csrf, e } func (s *Store) DeleteSession(id string) { _, _ = s.DB.Exec("DELETE FROM sessions WHERE id=?", id) } func (s *Store) AddAudit(uid, a, t, d string) { _, _ = s.DB.Exec("INSERT INTO audit(uid,action,target,detail,at) VALUES(?,?,?,?,?)", uid, a, t, d, time.Now().Unix()) } func (s *Store) Instances(uid string) ([]Instance, error) { rows, e := s.DB.Query("SELECT id,uid,name,version,image_id,status,container_id,created FROM instances WHERE uid=? ORDER BY created", uid) if e != nil { return nil, e } defer rows.Close() var out []Instance for rows.Next() { var i Instance var t int64 if e = rows.Scan(&i.ID, &i.UID, &i.Name, &i.Version, &i.ImageID, &i.Status, &i.ContainerID, &t); e != nil { return nil, e } i.Created = time.Unix(t, 0) out = append(out, i) } return out, rows.Err() } func (s *Store) AddInstance(i Instance) error { _, e := s.DB.Exec("INSERT INTO instances VALUES(?,?,?,?,?,?,?,?)", i.ID, i.UID, i.Name, i.Version, i.ImageID, i.Status, i.ContainerID, time.Now().Unix()) return e } func (s *Store) GetInstance(id string) (Instance, error) { var i Instance var t int64 e := s.DB.QueryRow("SELECT id,uid,name,version,image_id,status,container_id,created FROM instances WHERE id=?", id).Scan(&i.ID, &i.UID, &i.Name, &i.Version, &i.ImageID, &i.Status, &i.ContainerID, &t) i.Created = time.Unix(t, 0) return i, e } func (s *Store) SetInstance(id, status string) { _, _ = s.DB.Exec("UPDATE instances SET status=? WHERE id=?", status, id) } func (s *Store) DeleteInstance(id string) { _, _ = s.DB.Exec("DELETE FROM instances WHERE id=?", id) }