1a7e130f9f
- Drop the FIFO from oauthStates: consumed states left entries behind, so an unrate-limited start/cancel cycle grew the slice without bound. Evict by oldest expiry instead — the map alone now bounds memory. - CreateSession runs INSERT + expiry sweep in one transaction. - slices.Contains replaces a hand-rolled contains; APIBase typo fixed. - Stale comments and test paths updated; login hover uses --ember-ink.
70 lines
2.3 KiB
Go
70 lines
2.3 KiB
Go
package store
|
|
|
|
import (
|
|
"database/sql"
|
|
"time"
|
|
)
|
|
|
|
// Session is one browser login: an opaque id the cookie carries verbatim,
|
|
// the Reader it belongs to, and when it stops being valid.
|
|
type Session struct {
|
|
ID string
|
|
ReaderID int64
|
|
ExpiresAt time.Time
|
|
}
|
|
|
|
// CreateSession stores a new session row for reader. The id is generated by
|
|
// the caller (session.NewID) — the store only persists it. Expired rows that
|
|
// were never looked up are swept in the same transaction: this is the one
|
|
// write every login makes, so the table stays bounded without a background
|
|
// job.
|
|
func (s *Store) CreateSession(id string, readerID int64, ttl time.Duration) (Session, error) {
|
|
tx, err := s.db.Begin()
|
|
if err != nil {
|
|
return Session{}, err
|
|
}
|
|
defer tx.Rollback()
|
|
expires := time.Now().Add(ttl)
|
|
if _, err := tx.Exec(`INSERT INTO sessions (id, reader_id, expires_at) VALUES ($1, $2, $3)`,
|
|
id, readerID, expires); err != nil {
|
|
return Session{}, err
|
|
}
|
|
if _, err := tx.Exec(`DELETE FROM sessions WHERE expires_at < now()`); err != nil {
|
|
return Session{}, err
|
|
}
|
|
if err := tx.Commit(); err != nil {
|
|
return Session{}, err
|
|
}
|
|
return Session{ID: id, ReaderID: readerID, ExpiresAt: expires}, nil
|
|
}
|
|
|
|
// GetSession returns the live session row for id, or ok=false when the id is
|
|
// unknown or expired. An expired row is deleted on the way out, so the table
|
|
// never grows past sessions that are still valid.
|
|
func (s *Store) GetSession(id string, now time.Time) (Session, bool, error) {
|
|
var sess Session
|
|
err := s.db.QueryRow(
|
|
`SELECT id, reader_id, expires_at FROM sessions WHERE id = $1`, id,
|
|
).Scan(&sess.ID, &sess.ReaderID, &sess.ExpiresAt)
|
|
if err == sql.ErrNoRows {
|
|
return Session{}, false, nil
|
|
}
|
|
if err != nil {
|
|
return Session{}, false, err
|
|
}
|
|
if !sess.ExpiresAt.After(now) {
|
|
// Best-effort: the row is dead either way; failing the request over a
|
|
// cleanup delete would only hide the real error. CreateSession's
|
|
// sweep catches anything this misses.
|
|
_, _ = s.db.Exec(`DELETE FROM sessions WHERE id = $1`, id)
|
|
return Session{}, false, nil
|
|
}
|
|
return sess, true, nil
|
|
}
|
|
|
|
// DeleteSession revokes one session. Deleting an unknown id is not an error.
|
|
func (s *Store) DeleteSession(id string) error {
|
|
_, err := s.db.Exec(`DELETE FROM sessions WHERE id = $1`, id)
|
|
return err
|
|
}
|