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 // SessionCreatedAt is when this session signed in, in ISOLayout.
16 SessionCreatedAt string
17}
18
19func (d *DB) CreateSession(ctx context.Context, id string, userID int64, expiresAt, createdAt string) error {
20 _, err := d.ExecContext(ctx,
21 `INSERT INTO sessions (id, user_id, expires_at, created_at) VALUES (?, ?, ?, ?)`,
22 id, userID, expiresAt, createdAt)
23 return err
24}
25
26func (d *DB) DeleteSession(ctx context.Context, id string) error {
27 _, err := d.ExecContext(ctx, `DELETE FROM sessions WHERE id = ?`, id)
28 return err
29}
30
31// DeleteOtherSessions signs a user out everywhere except the session keepID.
32func (d *DB) DeleteOtherSessions(ctx context.Context, userID int64, keepID string) error {
33 _, err := d.ExecContext(ctx, `DELETE FROM sessions WHERE user_id = ? AND id != ?`, userID, keepID)
34 return err
35}
36
37func (d *DB) DeleteExpiredSessions(ctx context.Context) error {
38 // expires_at is stored in the ISO form NowISO writes. datetime('now') uses
39 // a space instead of the T, which compares wrong for the same day.
40 _, err := d.ExecContext(ctx, `DELETE FROM sessions WHERE expires_at < ?`, NowISO())
41 return err
42}
43
44// SessionUser resolves a session cookie. Expired sessions and pending users
45// resolve to nil.
46func (d *DB) SessionUser(ctx context.Context, sessionID, now string) (*SessionUser, error) {
47 return d.sessionUser(ctx, sessionID, now, false)
48}
49
50// SessionUserAllowPending also resolves the session of an account that is
51// still awaiting approval. Only the passkey registration ceremony uses it:
52// in queue mode the account exists but must not be usable yet.
53func (d *DB) SessionUserAllowPending(ctx context.Context, sessionID, now string) (*SessionUser, error) {
54 return d.sessionUser(ctx, sessionID, now, true)
55}
56
57func (d *DB) sessionUser(ctx context.Context, sessionID, now string, allowPending bool) (*SessionUser, error) {
58 query := `SELECT users.id, users.username, users.avatar_version, sessions.created_at
59 FROM sessions
60 JOIN users ON users.id = sessions.user_id
61 WHERE sessions.id = ? AND sessions.expires_at > ?`
62 if !allowPending {
63 query += ` AND users.is_pending = 0`
64 }
65 var u SessionUser
66 err := d.QueryRowContext(ctx, query, sessionID, now).Scan(&u.ID, &u.Username, &u.AvatarVersion, &u.SessionCreatedAt)
67 if errors.Is(err, sql.ErrNoRows) {
68 return nil, nil
69 }
70 if err != nil {
71 return nil, err
72 }
73 u.IsAdmin = u.Username == AdminUsername
74 return &u, nil
75}
76