| @@ -64,25 +64,40 @@ func (s *Store) ConsumeLoginToken(hash string) (int64, error) { |
| 64 | return userID, err |
64 | return userID, err |
| 65 | } |
65 | } |
| 66 | |
66 | |
| |
67 | // WebSessionIdle is how long a browser session lasts without a request. |
| |
68 | // Each use moves its expiry this far ahead, never past the cap it was |
| |
69 | // created with. Migration 0062 repeats the value for sessions it |
| |
70 | // converts. |
| |
71 | const WebSessionIdle = 12 * time.Hour |
| |
72 | |
| |
73 | // CreateWebSession stores a session that lapses after WebSessionIdle |
| |
74 | // without use, and after ttl regardless. |
| 67 | func (s *Store) CreateWebSession(hash string, userID int64, ttl time.Duration) error { |
75 | func (s *Store) CreateWebSession(hash string, userID int64, ttl time.Duration) error { |
| |
76 | now := time.Now() |
| 68 | _, err := s.DB.Exec( |
77 | _, err := s.DB.Exec( |
| 69 | "INSERT INTO web_sessions (token_hash, user_id, expires_at) VALUES (?, ?, ?)", |
78 | "INSERT INTO web_sessions (token_hash, user_id, expires_at, absolute_expires_at, last_used_at) VALUES (?, ?, ?, ?, ?)", |
| 70 | hash, userID, fmtTime(time.Now().Add(ttl))) |
79 | hash, userID, fmtTime(now.Add(min(ttl, WebSessionIdle))), fmtTime(now.Add(ttl)), fmtTime(now)) |
| 71 | return err |
80 | return err |
| 72 | } |
81 | } |
| 73 | |
82 | |
| 74 | // WebSessionUser resolves a session cookie hash to its user. |
83 | // WebSessionUser resolves a session cookie hash to its user and renews |
| |
84 | // the session's idle expiry. A session is written at most once a |
| |
85 | // minute, so a burst of requests costs one UPDATE. |
| 75 | func (s *Store) WebSessionUser(hash string) (User, error) { |
86 | func (s *Store) WebSessionUser(hash string) (User, error) { |
| |
87 | now := time.Now() |
| 76 | var userID int64 |
88 | var userID int64 |
| 77 | err := s.DB.QueryRow( |
89 | err := s.DB.QueryRow( |
| 78 | "SELECT user_id FROM web_sessions WHERE token_hash = ? AND expires_at > ?", |
90 | "SELECT user_id FROM web_sessions WHERE token_hash = ? AND expires_at > ?", |
| 79 | hash, fmtTime(time.Now())).Scan(&userID) |
91 | hash, fmtTime(now)).Scan(&userID) |
| 80 | if errors.Is(err, sql.ErrNoRows) { |
92 | if errors.Is(err, sql.ErrNoRows) { |
| 81 | return User{}, ErrNotFound |
93 | return User{}, ErrNotFound |
| 82 | } |
94 | } |
| 83 | if err != nil { |
95 | if err != nil { |
| 84 | return User{}, err |
96 | return User{}, err |
| 85 | } |
97 | } |
| |
98 | s.DB.Exec(`UPDATE web_sessions SET last_used_at = ?, expires_at = min(absolute_expires_at, ?) |
| |
99 | WHERE token_hash = ? AND last_used_at < ?`, |
| |
100 | fmtTime(now), fmtTime(now.Add(WebSessionIdle)), hash, fmtTime(now.Add(-time.Minute))) |
| 86 | return s.UserByID(userID) |
101 | return s.UserByID(userID) |
| 87 | } |
102 | } |
| 88 | |
103 | |
| @@ -95,14 +110,15 @@ func (s *Store) DeleteWebSession(hash string) error { |
| 95 | // twelve hex digits of the stored token hash: enough to name it, and a |
110 | // twelve hex digits of the stored token hash: enough to name it, and a |
| 96 | // hash of the cookie rather than the cookie. |
111 | // hash of the cookie rather than the cookie. |
| 97 | type WebSession struct { |
112 | type WebSession struct { |
| 98 | ID string `json:"id"` |
113 | ID string `json:"id"` |
| 99 | CreatedAt string `json:"created_at"` |
114 | CreatedAt string `json:"created_at"` |
| 100 | ExpiresAt string `json:"expires_at"` |
115 | ExpiresAt string `json:"expires_at"` |
| |
116 | LastUsedAt string `json:"last_used_at"` |
| 101 | } |
117 | } |
| 102 | |
118 | |
| 103 | // ListWebSessions lists the user's unexpired browser sessions, newest first. |
119 | // ListWebSessions lists the user's unexpired browser sessions, newest first. |
| 104 | func (s *Store) ListWebSessions(userID int64) ([]WebSession, error) { |
120 | func (s *Store) ListWebSessions(userID int64) ([]WebSession, error) { |
| 105 | rows, err := s.DB.Query(`SELECT substr(token_hash, 1, 12), created_at, expires_at |
121 | rows, err := s.DB.Query(`SELECT substr(token_hash, 1, 12), created_at, expires_at, COALESCE(last_used_at, created_at) |
| 106 | FROM web_sessions WHERE user_id = ? AND expires_at > ? ORDER BY created_at DESC`, |
122 | FROM web_sessions WHERE user_id = ? AND expires_at > ? ORDER BY created_at DESC`, |
| 107 | userID, fmtTime(time.Now())) |
123 | userID, fmtTime(time.Now())) |
| 108 | if err != nil { |
124 | if err != nil { |
| @@ -112,7 +128,7 @@ func (s *Store) ListWebSessions(userID int64) ([]WebSession, error) { |
| 112 | var out []WebSession |
128 | var out []WebSession |
| 113 | for rows.Next() { |
129 | for rows.Next() { |
| 114 | var ws WebSession |
130 | var ws WebSession |
| 115 | if err := rows.Scan(&ws.ID, &ws.CreatedAt, &ws.ExpiresAt); err != nil { |
131 | if err := rows.Scan(&ws.ID, &ws.CreatedAt, &ws.ExpiresAt, &ws.LastUsedAt); err != nil { |
| 116 | return nil, err |
132 | return nil, err |
| 117 | } |
133 | } |
| 118 | out = append(out, ws) |
134 | out = append(out, ws) |