package store import ( "context" "database/sql" "errors" "log" "sync" "time" "github.com/alexedwards/scs/v2" "plumber/internal/store/sqlc" ) // SessionStore persists scs sessions in Postgres via sqlc and optionally // deletes expired rows on an interval. type SessionStore struct { db *sql.DB q *sqlc.Queries stop chan struct{} stopped chan struct{} once sync.Once } // NewSessionStore creates a store backed by db. cleanupInterval > 0 starts a // background goroutine that deletes expired sessions; 0 disables cleanup. func NewSessionStore(db *sql.DB, cleanupInterval time.Duration) *SessionStore { s := &SessionStore{ db: db, q: sqlc.New(db), } if cleanupInterval > 0 { s.stop = make(chan struct{}) s.stopped = make(chan struct{}) go s.cleanupLoop(cleanupInterval) } return s } // Store returns the scs.Store implementation (s itself). func (s *SessionStore) Store() scs.Store { return s } // Find implements scs.Store. func (s *SessionStore) Find(token string) ([]byte, bool, error) { return s.FindCtx(context.Background(), token) } // Commit implements scs.Store. func (s *SessionStore) Commit(token string, data []byte, expiry time.Time) error { return s.CommitCtx(context.Background(), token, data, expiry) } // Delete implements scs.Store. func (s *SessionStore) Delete(token string) error { return s.DeleteCtx(context.Background(), token) } // FindCtx implements scs.CtxStore. func (s *SessionStore) FindCtx(ctx context.Context, token string) ([]byte, bool, error) { data, err := s.q.GetSession(ctx, token) if err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, false, nil } return nil, false, err } return data, true, nil } // CommitCtx implements scs.CtxStore. func (s *SessionStore) CommitCtx(ctx context.Context, token string, data []byte, expiry time.Time) error { return s.q.UpsertSession(ctx, sqlc.UpsertSessionParams{ Token: token, Data: data, Expiry: expiry, }) } // DeleteCtx implements scs.CtxStore. func (s *SessionStore) DeleteCtx(ctx context.Context, token string) error { return s.q.DeleteSession(ctx, token) } // StopCleanup stops the background expiry deleter. Safe to call multiple times. func (s *SessionStore) StopCleanup() { if s == nil || s.stop == nil { return } s.once.Do(func() { close(s.stop) <-s.stopped }) } // Close stops background session cleanup. func (s *SessionStore) Close() { s.StopCleanup() } func (s *SessionStore) cleanupLoop(interval time.Duration) { defer close(s.stopped) ticker := time.NewTicker(interval) defer ticker.Stop() var lastErrLog time.Time for { select { case <-ticker.C: n, err := s.q.DeleteExpiredSessions(context.Background()) if err != nil { if time.Since(lastErrLog) > time.Minute { log.Printf("session cleanup: %v", err) lastErrLog = time.Now() } continue } if n > 0 { log.Printf("session cleanup: deleted %d expired row(s)", n) } case <-s.stop: return } } }