internal/store/sessions.go

v1.8.0
gitbay/internal/store/sessions.go history · blame · raw

131 lines · 3954 bytes

  1package store
  2
  3import (
  4	"crypto/rand"
  5	"crypto/sha256"
  6	"database/sql"
  7	"encoding/hex"
  8	"errors"
  9	"time"
 10)
 11
 12// NewToken returns a fresh random token and its storage hash. Only the hash
 13// is persisted; the token itself goes to the user once.
 14func NewToken() (token, hash string, err error) {
 15	var b [32]byte
 16	if _, err := rand.Read(b[:]); err != nil {
 17		return "", "", err
 18	}
 19	token = hex.EncodeToString(b[:])
 20	return token, HashToken(token), nil
 21}
 22
 23func HashToken(token string) string {
 24	sum := sha256.Sum256([]byte(token))
 25	return hex.EncodeToString(sum[:])
 26}
 27
 28func fmtTime(t time.Time) string { return t.UTC().Format("2006-01-02T15:04:05.000Z") }
 29
 30// CreateLoginToken stores a one-time login token hash.
 31func (s *Store) CreateLoginToken(userID int64, hash string, ttl time.Duration) error {
 32	_, err := s.DB.Exec(
 33		"INSERT INTO login_tokens (token_hash, user_id, expires_at) VALUES (?, ?, ?)",
 34		hash, userID, fmtTime(time.Now().Add(ttl)))
 35	return err
 36}
 37
 38// ConsumeLoginToken redeems a token exactly once; expired or used tokens
 39// fail identically.
 40func (s *Store) ConsumeLoginToken(hash string) (int64, error) {
 41	res, err := s.DB.Exec(`
 42		UPDATE login_tokens SET used_at = strftime('%Y-%m-%dT%H:%M:%fZ','now')
 43		WHERE token_hash = ? AND used_at IS NULL AND expires_at > ?`,
 44		hash, fmtTime(time.Now()))
 45	if err != nil {
 46		return 0, err
 47	}
 48	if n, _ := res.RowsAffected(); n == 0 {
 49		return 0, ErrNotFound
 50	}
 51	var userID int64
 52	err = s.DB.QueryRow("SELECT user_id FROM login_tokens WHERE token_hash = ?", hash).Scan(&userID)
 53	return userID, err
 54}
 55
 56func (s *Store) CreateWebSession(hash string, userID int64, ttl time.Duration) error {
 57	_, err := s.DB.Exec(
 58		"INSERT INTO web_sessions (token_hash, user_id, expires_at) VALUES (?, ?, ?)",
 59		hash, userID, fmtTime(time.Now().Add(ttl)))
 60	return err
 61}
 62
 63// WebSessionUser resolves a session cookie hash to its user.
 64func (s *Store) WebSessionUser(hash string) (User, error) {
 65	var userID int64
 66	err := s.DB.QueryRow(
 67		"SELECT user_id FROM web_sessions WHERE token_hash = ? AND expires_at > ?",
 68		hash, fmtTime(time.Now())).Scan(&userID)
 69	if errors.Is(err, sql.ErrNoRows) {
 70		return User{}, ErrNotFound
 71	}
 72	if err != nil {
 73		return User{}, err
 74	}
 75	return s.UserByID(userID)
 76}
 77
 78func (s *Store) DeleteWebSession(hash string) error {
 79	_, err := s.DB.Exec("DELETE FROM web_sessions WHERE token_hash = ?", hash)
 80	return err
 81}
 82
 83// WebSession is one browser session as its owner lists it. ID is the first
 84// twelve hex digits of the stored token hash: enough to name it, and a
 85// hash of the cookie rather than the cookie.
 86type WebSession struct {
 87	ID        string `json:"id"`
 88	CreatedAt string `json:"created_at"`
 89	ExpiresAt string `json:"expires_at"`
 90}
 91
 92// ListWebSessions lists the user's unexpired browser sessions, newest first.
 93func (s *Store) ListWebSessions(userID int64) ([]WebSession, error) {
 94	rows, err := s.DB.Query(`SELECT substr(token_hash, 1, 12), created_at, expires_at
 95		FROM web_sessions WHERE user_id = ? AND expires_at > ? ORDER BY created_at DESC`,
 96		userID, fmtTime(time.Now()))
 97	if err != nil {
 98		return nil, err
 99	}
100	defer rows.Close()
101	var out []WebSession
102	for rows.Next() {
103		var ws WebSession
104		if err := rows.Scan(&ws.ID, &ws.CreatedAt, &ws.ExpiresAt); err != nil {
105			return nil, err
106		}
107		out = append(out, ws)
108	}
109	return out, rows.Err()
110}
111
112// RevokeWebSession ends one of the user's sessions by its listed id.
113func (s *Store) RevokeWebSession(userID int64, id string) error {
114	res, err := s.DB.Exec("DELETE FROM web_sessions WHERE user_id = ? AND substr(token_hash, 1, 12) = ?", userID, id)
115	if err != nil {
116		return err
117	}
118	if n, _ := res.RowsAffected(); n == 0 {
119		return ErrNotFound
120	}
121	return nil
122}
123
124// RevokeAllWebSessions ends every browser session the user has.
125func (s *Store) RevokeAllWebSessions(userID int64) (int64, error) {
126	res, err := s.DB.Exec("DELETE FROM web_sessions WHERE user_id = ?", userID)
127	if err != nil {
128		return 0, err
129	}
130	return res.RowsAffected()
131}