sessions.go
⎇
Raw
1package db
2
3import (
4 "context"
5 "database/sql"
6 "errors"
7)
8
9// SessionUser is the identity attached to a request by the session cookie.
10type SessionUser struct {
11 ID int64
12 Username string
13 IsAdmin bool
14 AvatarVersion int64
15}
16
17func (d *DB) CreateSession(ctx context.Context, id string, userID int64, expiresAt, createdAt string) error {
18 _, err := d.ExecContext(ctx,
19 `INSERT INTO sessions (id, user_id, expires_at, created_at) VALUES (?, ?, ?, ?)`,
20 id, userID, expiresAt, createdAt)
21 return err
22}
23
24func (d *DB) DeleteSession(ctx context.Context, id string) error {
25 _, err := d.ExecContext(ctx, `DELETE FROM sessions WHERE id = ?`, id)
26 return err
27}
28
29func (d *DB) DeleteExpiredSessions(ctx context.Context) error {
30 // expires_at is stored in the ISO form NowISO writes. datetime('now') uses
31 // a space instead of the T, which compares wrong for the same day.
32 _, err := d.ExecContext(ctx, `DELETE FROM sessions WHERE expires_at < ?`, NowISO())
33 return err
34}
35
36// SessionUser resolves a session cookie. Expired sessions and pending users
37// resolve to nil.
38func (d *DB) SessionUser(ctx context.Context, sessionID, now string) (*SessionUser, error) {
39 return d.sessionUser(ctx, sessionID, now, false)
40}
41
42// SessionUserAllowPending also resolves the session of an account that is
43// still awaiting approval. Only the passkey registration ceremony uses it:
44// in queue mode the account exists but must not be usable yet.
45func (d *DB) SessionUserAllowPending(ctx context.Context, sessionID, now string) (*SessionUser, error) {
46 return d.sessionUser(ctx, sessionID, now, true)
47}
48
49func (d *DB) sessionUser(ctx context.Context, sessionID, now string, allowPending bool) (*SessionUser, error) {
50 query := `SELECT users.id, users.username, users.avatar_version
51 FROM sessions
52 JOIN users ON users.id = sessions.user_id
53 WHERE sessions.id = ? AND sessions.expires_at > ?`
54 if !allowPending {
55 query += ` AND users.is_pending = 0`
56 }
57 var u SessionUser
58 err := d.QueryRowContext(ctx, query, sessionID, now).Scan(&u.ID, &u.Username, &u.AvatarVersion)
59 if errors.Is(err, sql.ErrNoRows) {
60 return nil, nil
61 }
62 if err != nil {
63 return nil, err
64 }
65 u.IsAdmin = u.Username == AdminUsername
66 return &u, nil
67}
68