go-crypt/auth/session_store_sqlite.go
Claude 7407b89b8d
refactor(ax): AX RFC-025 compliance sweep pass 1
Remove banned imports (fmt, strings, os, errors, path/filepath) across all
production and test files, replace with core.* primitives, coreio.ReadStream,
and coreerr.E. Upgrade dappco.re/go/core v0.5.0 → v0.7.0 for core.PathBase
and core.Is. Fix isRepoScoped to exclude pr.* capabilities (enforcement is at
the forge layer, not the policy engine). Add Good/Bad/Ugly test coverage to
all packages missing the mandatory three-category naming convention.

Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-31 08:48:56 +01:00

139 lines
3.3 KiB
Go

package auth
import (
"encoding/json"
"sync"
"time"
core "dappco.re/go/core"
coreerr "dappco.re/go/core/log"
"forge.lthn.ai/core/go-store"
)
const sessionGroup = "sessions"
// SQLiteSessionStore is a SessionStore backed by go-store (SQLite KV).
// A mutex serialises all operations because SQLite is single-writer.
type SQLiteSessionStore struct {
mu sync.Mutex
store *store.Store
}
// NewSQLiteSessionStore creates a new SQLite-backed session store.
//
// sessionStore, err := auth.NewSQLiteSessionStore("/var/lib/agent/sessions.db")
// authenticator := auth.New(medium, auth.WithSessionStore(sessionStore))
func NewSQLiteSessionStore(databasePath string) (*SQLiteSessionStore, error) {
s, err := store.New(databasePath)
if err != nil {
return nil, err
}
return &SQLiteSessionStore{store: s}, nil
}
// Get retrieves a session by token from SQLite.
func (s *SQLiteSessionStore) Get(token string) (*Session, error) {
s.mu.Lock()
defer s.mu.Unlock()
value, err := s.store.Get(sessionGroup, token)
if err != nil {
if core.Is(err, store.ErrNotFound) {
return nil, ErrSessionNotFound
}
return nil, err
}
var session Session
if err := json.Unmarshal([]byte(value), &session); err != nil {
return nil, coreerr.E("auth.SQLiteSessionStore.Get", "failed to unmarshal session", err)
}
return &session, nil
}
// Set stores a session in SQLite, keyed by its token.
func (s *SQLiteSessionStore) Set(session *Session) error {
s.mu.Lock()
defer s.mu.Unlock()
data, err := json.Marshal(session)
if err != nil {
return coreerr.E("auth.SQLiteSessionStore.Set", "failed to marshal session", err)
}
return s.store.Set(sessionGroup, session.Token, string(data))
}
// Delete removes a session by token from SQLite.
func (s *SQLiteSessionStore) Delete(token string) error {
s.mu.Lock()
defer s.mu.Unlock()
// Check existence first to return ErrSessionNotFound
_, err := s.store.Get(sessionGroup, token)
if err != nil {
if core.Is(err, store.ErrNotFound) {
return ErrSessionNotFound
}
return err
}
return s.store.Delete(sessionGroup, token)
}
// DeleteByUser removes all sessions belonging to the given user.
func (s *SQLiteSessionStore) DeleteByUser(userID string) error {
s.mu.Lock()
defer s.mu.Unlock()
all, err := s.store.GetAll(sessionGroup)
if err != nil {
return err
}
for token, value := range all {
var session Session
if err := json.Unmarshal([]byte(value), &session); err != nil {
continue // Skip malformed entries
}
if session.UserID == userID {
if err := s.store.Delete(sessionGroup, token); err != nil {
return err
}
}
}
return nil
}
// Cleanup removes all expired sessions and returns the count removed.
func (s *SQLiteSessionStore) Cleanup() (int, error) {
s.mu.Lock()
defer s.mu.Unlock()
all, err := s.store.GetAll(sessionGroup)
if err != nil {
return 0, err
}
now := time.Now()
count := 0
for token, value := range all {
var session Session
if err := json.Unmarshal([]byte(value), &session); err != nil {
continue // Skip malformed entries
}
if now.After(session.ExpiresAt) {
if err := s.store.Delete(sessionGroup, token); err != nil {
return count, err
}
count++
}
}
return count, nil
}
// Close closes the underlying SQLite store.
func (s *SQLiteSessionStore) Close() error {
s.mu.Lock()
defer s.mu.Unlock()
return s.store.Close()
}