Files
plumber/internal/store/sessions.go
T
codegirl007 29b0536215 Address production-readiness review: clearer errors, safer votes, and ops hardening.
Distinguish auth/lookup failures, make votes idempotent on visible questions, bound shutdown, page admin users, LRU throttle, trusted-proxy CIDRs, avatar cleanup, versioned migrations, and session cleanup logging.
2026-08-22 12:16:59 -07:00

127 lines
2.9 KiB
Go

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