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.
127 lines
2.9 KiB
Go
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
|
|
}
|
|
}
|
|
}
|