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.
This commit is contained in:
+93
-32
@@ -1,53 +1,114 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/alexedwards/scs/postgresstore"
|
||||
"github.com/alexedwards/scs/v2"
|
||||
|
||||
"plumber/internal/store/sqlc"
|
||||
)
|
||||
|
||||
const sessionsSchemaPostgres = `
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
token TEXT PRIMARY KEY,
|
||||
data BYTEA NOT NULL,
|
||||
expiry TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS sessions_expiry_idx ON sessions (expiry);
|
||||
`
|
||||
|
||||
// applySessionsSchema creates the scs sessions table if missing.
|
||||
func applySessionsSchema(db *sql.DB) error {
|
||||
return applySchema(db, sessionsSchemaPostgres)
|
||||
}
|
||||
|
||||
type sessionStopper interface {
|
||||
StopCleanup()
|
||||
}
|
||||
|
||||
// SessionStore wraps scs Postgres session persistence and cleanup.
|
||||
// SessionStore persists scs sessions in Postgres via sqlc and optionally
|
||||
// deletes expired rows on an interval.
|
||||
type SessionStore struct {
|
||||
store scs.Store
|
||||
stopper sessionStopper
|
||||
db *sql.DB
|
||||
q *sqlc.Queries
|
||||
stop chan struct{}
|
||||
stopped chan struct{}
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
// NewSessionStore starts a postgresstore with the given cleanup interval.
|
||||
// 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 {
|
||||
ps := postgresstore.NewWithCleanupInterval(db, cleanupInterval)
|
||||
return &SessionStore{store: ps, stopper: ps}
|
||||
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.
|
||||
// Store returns the scs.Store implementation (s itself).
|
||||
func (s *SessionStore) Store() scs.Store {
|
||||
return s.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() {
|
||||
if s == nil || s.stopper == nil {
|
||||
return
|
||||
}
|
||||
s.stopper.StopCleanup()
|
||||
s.stopper = nil
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user