Files
nadir-agent/internal/auth/session.go
T
urania 2bf11dda91
build-and-release / release (push) Failing after 17m7s
feat: first release
2026-06-22 16:51:18 +02:00

95 lines
2.6 KiB
Go

package auth
import (
"crypto/rand"
"database/sql"
"encoding/hex"
"fmt"
"os"
"path/filepath"
"time"
_ "modernc.org/sqlite"
)
const sessionTTL = 24 * time.Hour
type Session struct {
Username string
}
// SessionStore persists sessions in an embedded SQLite database so they survive
// process restarts. Expired rows are dropped lazily on read.
type SessionStore struct {
db *sql.DB
}
// NewSessionStore opens (creating if needed) the SQLite database at path and
// ensures the sessions table exists. The parent directory is created too.
func NewSessionStore(path string) (*SessionStore, error) {
if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
return nil, fmt.Errorf("session db dir: %w", err)
}
db, err := sql.Open("sqlite", path)
if err != nil {
return nil, fmt.Errorf("open session db: %w", err)
}
// ponytail: single connection serializes access, avoiding SQLite's
// "database is locked" under concurrent writes. A sysadmin panel's session
// rate is trivial; switch to WAL + a real pool only if that ever bites.
db.SetMaxOpenConns(1)
if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS sessions (
token TEXT PRIMARY KEY,
username TEXT NOT NULL,
expires_at INTEGER NOT NULL
)`); err != nil {
return nil, fmt.Errorf("create sessions table: %w", err)
}
return &SessionStore{db: db}, nil
}
// Ping reports whether the session database is reachable. Used by the health
// check.
func (s *SessionStore) Ping() error { return s.db.Ping() }
func (s *SessionStore) Create(username string) (string, error) {
token := randomToken()
expires := time.Now().Add(sessionTTL)
if _, err := s.db.Exec(
`INSERT INTO sessions (token, username, expires_at) VALUES (?, ?, ?)`,
token, username, expires.Unix(),
); err != nil {
return "", err
}
return token, nil
}
// Delete removes a session, invalidating it immediately (logout). Deleting an
// unknown token is a no-op.
func (s *SessionStore) Delete(token string) error {
_, err := s.db.Exec(`DELETE FROM sessions WHERE token = ?`, token)
return err
}
func (s *SessionStore) GetByToken(token string) (Session, bool) {
var username string
var expires int64
err := s.db.QueryRow(
`SELECT username, expires_at FROM sessions WHERE token = ?`, token,
).Scan(&username, &expires)
if err != nil {
return Session{}, false
}
if time.Now().Unix() > expires {
s.db.Exec(`DELETE FROM sessions WHERE token = ?`, token)
return Session{}, false
}
return Session{Username: username}, true
}
func randomToken() string {
b := make([]byte, 32)
rand.Read(b) // never fails; rand.Read panics internally on misconfigured platforms.
return hex.EncodeToString(b)
}