-- name: CreateUser :one INSERT INTO users (username, password_hash, api_token, is_admin, display_name) VALUES ($1, $2, $3, $4, $5) RETURNING *; -- name: CreateUserFirstAdminRace :one -- Inserts a new user; sets is_admin=true ONLY when no users currently -- exist. The (SELECT NOT EXISTS ...) subquery is evaluated at INSERT -- time within the same statement, so the result reflects committed -- state at that moment. -- -- Race semantics: two concurrent calls in an empty-users state may -- both observe "no users" and both insert with is_admin=true. That's -- benign — having two admins from the gate is fine; what matters is -- that there's AT LEAST one. If the two calls happened to share the -- same username, the unique constraint on users.username arbitrates -- and the second caller's INSERT fails with a unique violation. The -- caller (registration handler) can retry as a regular non-admin in -- that case (or surface a "username taken" error to the user). INSERT INTO users (username, password_hash, api_token, is_admin, display_name) VALUES ( $1, $2, $3, (SELECT NOT EXISTS (SELECT 1 FROM users)), $4 ) RETURNING *; -- name: GetUserByUsername :one SELECT * FROM users WHERE username = $1; -- name: GetUserByAPIToken :one SELECT * FROM users WHERE api_token = $1; -- name: CountUsers :one SELECT count(*) FROM users; -- name: SetSubsonicPassword :exec -- Stores (or clears with NULL) the per-user Subsonic legacy credential used -- for t/s and p auth on /rest/*. Must be plaintext; see migration 0003. UPDATE users SET subsonic_password = $2 WHERE id = $1; -- name: GetUserByID :one SELECT * FROM users WHERE id = $1; -- name: SetListenBrainzToken :exec UPDATE users SET listenbrainz_token = $2, listenbrainz_enabled = CASE WHEN COALESCE($2, '') = '' THEN FALSE ELSE listenbrainz_enabled END WHERE id = $1; -- name: SetListenBrainzEnabled :exec UPDATE users SET listenbrainz_enabled = $2 WHERE id = $1; -- name: ListUsers :many -- Admin user-management list. Sort newest-first. SELECT id, username, display_name, is_admin, auto_approve_requests, debug_mode_enabled, created_at FROM users ORDER BY created_at DESC; -- name: CountAdmins :one SELECT count(*) FROM users WHERE is_admin = true; -- name: UpdateUserAdmin :one -- Sets is_admin to the given value. Returns the updated row so the -- handler can echo it back to the caller. UPDATE users SET is_admin = $2 WHERE id = $1 RETURNING *; -- name: GetListenBrainzConfig :one -- Returns the user's LB token + enabled flag and the most recent -- play_events.scrobbled_at for last-scrobbled-at status. SELECT u.listenbrainz_token, u.listenbrainz_enabled, (SELECT MAX(pe.scrobbled_at)::timestamptz FROM play_events pe WHERE pe.user_id = u.id) AS last_scrobbled_at FROM users u WHERE u.id = $1; -- name: CreateUserAdmin :one -- Admin-driven user creation. Distinct from CreateUser/CreateUserFirstAdminRace: -- the caller (an admin) supplies all five fields explicitly including is_admin, -- so an admin can promote on creation. Used by POST /api/admin/users. INSERT INTO users (username, password_hash, api_token, is_admin, display_name) VALUES ($1, $2, $3, $4, $5) RETURNING *; -- name: DeleteUser :exec -- Hard delete with cascade. The schema's ON DELETE CASCADE foreign keys -- handle plays/likes/sessions/etc. Caller must enforce the last-admin -- guard at the HTTP layer (count admins, refuse if target is the only -- admin). The lidarr_quarantine, general_likes*, play_events, -- sessions, and other user-FK tables all have ON DELETE CASCADE so -- this is a clean drop. DELETE FROM users WHERE id = $1; -- name: ResetUserPassword :exec -- Admin-driven password reset: caller writes a new hashed password to -- the target user's row. No knowledge of the old password is required -- (this is the admin path; self-service "knows current password" is -- a separate U3 query). UPDATE users SET password_hash = $2 WHERE id = $1; -- name: UpdateUserAutoApprove :one -- Toggle the per-user auto_approve_requests flag. UPDATE users SET auto_approve_requests = $2 WHERE id = $1 RETURNING *; -- name: SetDebugMode :one -- Toggle the per-account diagnostics/debug-reporting opt-in. Admin-driven -- from /admin (flip remotely while a bug is live) or self-driven OFF from -- the client. Returns the updated row so the handler can echo it. UPDATE users SET debug_mode_enabled = $2 WHERE id = $1 RETURNING *; -- name: ChangeUserPassword :exec -- Self-service password change. Caller (HTTP handler) verifies the -- current password before calling this. Distinct from -- ResetUserPassword which is admin-driven (no current-password -- check). UPDATE users SET password_hash = $2 WHERE id = $1; -- name: UpdateUserProfile :one -- Self-service: set display name and/or email. Both fields are -- nullable in the DB; pass NULL ptr to clear, non-NULL ptr to set. UPDATE users SET display_name = $2, email = $3 WHERE id = $1 RETURNING *; -- name: RegenerateApiToken :one -- Self-service: caller wants a new API token. Used by the /settings -- API Token card's "Regenerate" button. UPDATE users SET api_token = $2 WHERE id = $1 RETURNING *; -- name: GetUserByEmail :one -- Used by forgot-password lookup. Lowercase comparison both sides -- to keep the unique index happy and to make the lookup -- case-insensitive. SELECT * FROM users WHERE lower(email) = lower($1); -- name: UpdateUserTimezone :exec -- Sets the user's IANA timezone and bumps timezone_updated_at. The -- handler validates the timezone string via time.LoadLocation before -- calling this; the DB does not re-validate. UPDATE users SET timezone = $2, timezone_updated_at = now() WHERE id = $1; -- name: ListActiveUsersWithTimezones :many -- Returns (id, timezone) for every user with a play in the last 7 -- days. The system-playlist scheduler iterates this list at startup -- and during its hourly reconciliation to discover newly-active / -- no-longer-active users. SELECT u.id, u.timezone FROM users u WHERE EXISTS ( SELECT 1 FROM play_events pe WHERE pe.user_id = u.id AND pe.started_at > now() - INTERVAL '7 days' );