package db import ( "context" "database/sql" "errors" ) // SessionUser is the identity attached to a request by the session cookie. type SessionUser struct { ID int64 Username string IsAdmin bool AvatarVersion int64 // SessionCreatedAt is when this session signed in, in ISOLayout. SessionCreatedAt string } func (d *DB) CreateSession(ctx context.Context, id string, userID int64, expiresAt, createdAt string) error { _, err := d.ExecContext(ctx, `INSERT INTO sessions (id, user_id, expires_at, created_at) VALUES (?, ?, ?, ?)`, id, userID, expiresAt, createdAt) return err } func (d *DB) DeleteSession(ctx context.Context, id string) error { _, err := d.ExecContext(ctx, `DELETE FROM sessions WHERE id = ?`, id) return err } // DeleteOtherSessions signs a user out everywhere except the session keepID. func (d *DB) DeleteOtherSessions(ctx context.Context, userID int64, keepID string) error { _, err := d.ExecContext(ctx, `DELETE FROM sessions WHERE user_id = ? AND id != ?`, userID, keepID) return err } func (d *DB) DeleteExpiredSessions(ctx context.Context) error { // expires_at is stored in the ISO form NowISO writes. datetime('now') uses // a space instead of the T, which compares wrong for the same day. _, err := d.ExecContext(ctx, `DELETE FROM sessions WHERE expires_at < ?`, NowISO()) return err } // SessionUser resolves a session cookie. Expired sessions and pending users // resolve to nil. func (d *DB) SessionUser(ctx context.Context, sessionID, now string) (*SessionUser, error) { return d.sessionUser(ctx, sessionID, now, false) } // SessionUserAllowPending also resolves the session of an account that is // still awaiting approval. Only the passkey registration ceremony uses it: // in queue mode the account exists but must not be usable yet. func (d *DB) SessionUserAllowPending(ctx context.Context, sessionID, now string) (*SessionUser, error) { return d.sessionUser(ctx, sessionID, now, true) } func (d *DB) sessionUser(ctx context.Context, sessionID, now string, allowPending bool) (*SessionUser, error) { query := `SELECT users.id, users.username, users.avatar_version, sessions.created_at FROM sessions JOIN users ON users.id = sessions.user_id WHERE sessions.id = ? AND sessions.expires_at > ?` if !allowPending { query += ` AND users.is_pending = 0` } var u SessionUser err := d.QueryRowContext(ctx, query, sessionID, now).Scan(&u.ID, &u.Username, &u.AvatarVersion, &u.SessionCreatedAt) if errors.Is(err, sql.ErrNoRows) { return nil, nil } if err != nil { return nil, err } u.IsAdmin = u.Username == AdminUsername return &u, nil }