package store import ( "database/sql" "fmt" "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 } // DeleteReaderSessions revokes every session one Reader holds — the owner's // remedy when a Reader's browser must be logged out everywhere at once. The // next request carrying any of those cookies finds no row and is rejected. func (s *Store) DeleteReaderSessions(readerID int64) error { if _, err := s.db.Exec(`DELETE FROM sessions WHERE reader_id = $1`, readerID); err != nil { return fmt.Errorf("delete sessions for reader %d: %w", readerID, err) } return nil }