Files
plumber/internal/store/sessions.go
T
codegirl007 247fb05281 Replace scs postgresstore with a sqlc-backed SessionStore.
Keep scs for cookies and session API while sessions DDL and queries live in the same sqlc stack as the rest of Postgres.
2026-08-22 07:11:47 -07:00

115 lines
2.6 KiB
Go

package store
import (
"context"
"database/sql"
"errors"
"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()
for {
select {
case <-ticker.C:
_ = s.q.DeleteExpiredSessions(context.Background())
case <-s.stop:
return
}
}
}