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) }