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 here: 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) { expires := time.Now().Add(ttl) _, err := s.db.Exec(`INSERT INTO sessions (id, reader_id, expires_at) VALUES ($1, $2, $3)`, id, readerID, expires) if err != nil { return Session{}, err } _, err = s.db.Exec(`DELETE FROM sessions WHERE expires_at < now()`) if 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) { _, _ = 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 }