-- name: CreateAPIToken :execresult INSERT INTO api_tokens (user_id, token_hash, label, expires_at) VALUES (?, ?, ?, ?); -- name: GetActiveAPIToken :one -- Auth-hot lookup: returns the token only if it is neither revoked nor expired, -- so a resolved row is always usable. Matches the sessions never-expiring -- convention (expires_at IS NULL). SELECT id, user_id, token_hash, label, created_at, last_used_at, expires_at, revoked_at FROM api_tokens WHERE token_hash = ? AND revoked_at IS NULL AND (expires_at IS NULL OR strftime('%s', expires_at) > strftime('%s', 'now')); -- name: ListAPITokens :many -- Admin/CLI listing. Never selects token_hash (unrecoverable; only the raw -- token shown at creation is usable). SELECT t.id, t.user_id, COALESCE(u.username, '') AS username, t.label, t.created_at, t.last_used_at, t.expires_at, t.revoked_at FROM api_tokens t LEFT JOIN users u ON u.id = t.user_id ORDER BY t.id DESC LIMIT 200; -- name: RevokeAPIToken :execresult UPDATE api_tokens SET revoked_at = datetime('now') WHERE id = ? AND revoked_at IS NULL; -- name: RevokeAPITokenByLabel :execresult UPDATE api_tokens SET revoked_at = datetime('now') WHERE label = ? AND revoked_at IS NULL; -- name: TouchAPIToken :exec UPDATE api_tokens SET last_used_at = datetime('now') WHERE token_hash = ?; -- name: GetOwnerUser :one -- The highest-privilege account (role with the greatest position), used as the -- default identity for `token create`. FROM is users-only (role position is a -- correlated subquery, not a join) so the row maps through userFromGen exactly -- like GetUserByID, so keep this SELECT list identical to GetUserByID's. -- A :one query already reads a single row via QueryRow, so no LIMIT is needed -- (and an explicit LIMIT 1 is mis-emitted by sqlc here). ORDER BY puts the -- highest-position role first, so that first row is the owner. -- Banned users are excluded: account deletion anonymises the row and sets -- banned = 1 permanently, so without this filter a self-deleted Owner keeps -- outranking every live admin and becomes the default identity for token -- creation forever -- minting tokens the auth layer then 403s on every use. -- The ban_expires arm mirrors auth.IsEffectivelyBanned (and db.notBannedClause) -- so a lapsed temporary ban stays eligible; the replace() normalises the space -- separator form of ban_expires to 'T' before comparing, because ' ' sorts -- below 'T' and a same-day space-form expiry would otherwise read as lapsed. SELECT id, username, password, avatar, role_id, totp_secret, status, created_at, last_seen, banned, ban_reason, ban_expires, identity_public_key, display_name, about, custom_status FROM users WHERE banned = 0 OR (ban_expires IS NOT NULL AND replace(ban_expires, ' ', 'T') <= strftime('%Y-%m-%dT%H:%M:%SZ', 'now')) ORDER BY (SELECT r.position FROM roles r WHERE r.id = users.role_id) DESC, id ASC;