Answers the open fork on #2527's last slice: automatic, not a button. Until now missing_since was a dead end -- reconcile marks it, every selection path skips it, the admin surface lists it, and there it sits. Two decisions carry most of the safety, both at the design level rather than as rate limits bolted on afterwards. The unit is the ALBUM, not the track. Lidarr acquires releases; there is no meaningful "fetch me one track", and a track-kind request needs a recording MBID plenty of files lack. Grouping means the loss that produced #2523 -- three reorganised albums, ~40 missing files -- becomes three requests instead of forty. The flood problem mostly dissolves. And nothing is requested until a file has been missing longer than the grace window (24h default). A filesystem lies transiently: an unmounted volume, a container that started before its media mount attached, a NAS mid-reboot. Every one of those resolves itself well inside a day at no cost. missing_since is never re-stamped (#2523), so it is a true "gone since" clock to measure against, not "when we last noticed". This is the difference between automatic and trigger-happy. Then the backoff proper: 6h -> 12h -> 24h -> 48h per album, clamped to a week, three attempts before giving up, and a per-pass ceiling so a genuinely large loss trickles instead of dumping hundreds of rows into the queue. Giving up is stamped as a timestamp rather than inferred from attempts >= max, so the verdict survives an operator later raising the maximum and the surface can say when. A sweeper, not a hook inside reconcile. Reconcile runs inside a scan and has no business deciding to talk to a third-party service; it also re-runs often, which would make "attempt once, then back off" awkward to express. A worker paces itself, survives a restart, and retries without needing another scan. Recovered albums have their state deleted rather than reset -- a future loss is a new problem, not a continuation. Requests are attributed to the oldest admin: lidarr_requests.user_id is NOT NULL and a re-acquisition has no requesting human, so this keeps the row auditable and in the same queue as everything else without inventing a synthetic principal the schema would have to understand. Auto-approve defaults ON. Requests are created pending and nothing reaches Lidarr until approval, so with it off this would be a notification rather than an attempt. Lidarr disabled leaves the request pending rather than counting a failure -- the record of intent is still right and becomes actionable the moment Lidarr is configured. Albums with no MBID are counted, not silently skipped: nothing can be asked of Lidarr for a release MusicBrainz cannot name, and quietly doing nothing would read as the feature being broken. Settings are DB-backed per rule #25 with CHECK-guarded ranges, validated in Go as well so the API answers 400 rather than surfacing a constraint violation. The admin card and the state on the missing-files page are next; this is the engine.
747 lines
21 KiB
Go
747 lines
21 KiB
Go
// Code generated by sqlc. DO NOT EDIT.
|
|
// versions:
|
|
// sqlc v1.31.1
|
|
// source: users.sql
|
|
|
|
package dbq
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
)
|
|
|
|
const changeUserPassword = `-- name: ChangeUserPassword :exec
|
|
UPDATE users SET password_hash = $2 WHERE id = $1
|
|
`
|
|
|
|
type ChangeUserPasswordParams struct {
|
|
ID pgtype.UUID
|
|
PasswordHash string
|
|
}
|
|
|
|
// 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).
|
|
func (q *Queries) ChangeUserPassword(ctx context.Context, arg ChangeUserPasswordParams) error {
|
|
_, err := q.db.Exec(ctx, changeUserPassword, arg.ID, arg.PasswordHash)
|
|
return err
|
|
}
|
|
|
|
const countAdmins = `-- name: CountAdmins :one
|
|
SELECT count(*) FROM users WHERE is_admin = true
|
|
`
|
|
|
|
func (q *Queries) CountAdmins(ctx context.Context) (int64, error) {
|
|
row := q.db.QueryRow(ctx, countAdmins)
|
|
var count int64
|
|
err := row.Scan(&count)
|
|
return count, err
|
|
}
|
|
|
|
const countUsers = `-- name: CountUsers :one
|
|
SELECT count(*) FROM users
|
|
`
|
|
|
|
func (q *Queries) CountUsers(ctx context.Context) (int64, error) {
|
|
row := q.db.QueryRow(ctx, countUsers)
|
|
var count int64
|
|
err := row.Scan(&count)
|
|
return count, err
|
|
}
|
|
|
|
const createUser = `-- name: CreateUser :one
|
|
INSERT INTO users (username, password_hash, api_token, is_admin, display_name)
|
|
VALUES ($1, $2, $3, $4, $5)
|
|
RETURNING id, username, password_hash, api_token, is_admin, created_at, subsonic_password, listenbrainz_token, listenbrainz_enabled, display_name, auto_approve_requests, email, timezone, timezone_updated_at, debug_mode_enabled
|
|
`
|
|
|
|
type CreateUserParams struct {
|
|
Username string
|
|
PasswordHash string
|
|
ApiToken string
|
|
IsAdmin bool
|
|
DisplayName *string
|
|
}
|
|
|
|
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) {
|
|
row := q.db.QueryRow(ctx, createUser,
|
|
arg.Username,
|
|
arg.PasswordHash,
|
|
arg.ApiToken,
|
|
arg.IsAdmin,
|
|
arg.DisplayName,
|
|
)
|
|
var i User
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Username,
|
|
&i.PasswordHash,
|
|
&i.ApiToken,
|
|
&i.IsAdmin,
|
|
&i.CreatedAt,
|
|
&i.SubsonicPassword,
|
|
&i.ListenbrainzToken,
|
|
&i.ListenbrainzEnabled,
|
|
&i.DisplayName,
|
|
&i.AutoApproveRequests,
|
|
&i.Email,
|
|
&i.Timezone,
|
|
&i.TimezoneUpdatedAt,
|
|
&i.DebugModeEnabled,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const createUserAdmin = `-- name: CreateUserAdmin :one
|
|
INSERT INTO users (username, password_hash, api_token, is_admin, display_name)
|
|
VALUES ($1, $2, $3, $4, $5)
|
|
RETURNING id, username, password_hash, api_token, is_admin, created_at, subsonic_password, listenbrainz_token, listenbrainz_enabled, display_name, auto_approve_requests, email, timezone, timezone_updated_at, debug_mode_enabled
|
|
`
|
|
|
|
type CreateUserAdminParams struct {
|
|
Username string
|
|
PasswordHash string
|
|
ApiToken string
|
|
IsAdmin bool
|
|
DisplayName *string
|
|
}
|
|
|
|
// 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.
|
|
func (q *Queries) CreateUserAdmin(ctx context.Context, arg CreateUserAdminParams) (User, error) {
|
|
row := q.db.QueryRow(ctx, createUserAdmin,
|
|
arg.Username,
|
|
arg.PasswordHash,
|
|
arg.ApiToken,
|
|
arg.IsAdmin,
|
|
arg.DisplayName,
|
|
)
|
|
var i User
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Username,
|
|
&i.PasswordHash,
|
|
&i.ApiToken,
|
|
&i.IsAdmin,
|
|
&i.CreatedAt,
|
|
&i.SubsonicPassword,
|
|
&i.ListenbrainzToken,
|
|
&i.ListenbrainzEnabled,
|
|
&i.DisplayName,
|
|
&i.AutoApproveRequests,
|
|
&i.Email,
|
|
&i.Timezone,
|
|
&i.TimezoneUpdatedAt,
|
|
&i.DebugModeEnabled,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const createUserFirstAdminRace = `-- name: CreateUserFirstAdminRace :one
|
|
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 id, username, password_hash, api_token, is_admin, created_at, subsonic_password, listenbrainz_token, listenbrainz_enabled, display_name, auto_approve_requests, email, timezone, timezone_updated_at, debug_mode_enabled
|
|
`
|
|
|
|
type CreateUserFirstAdminRaceParams struct {
|
|
Username string
|
|
PasswordHash string
|
|
ApiToken string
|
|
DisplayName *string
|
|
}
|
|
|
|
// 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).
|
|
func (q *Queries) CreateUserFirstAdminRace(ctx context.Context, arg CreateUserFirstAdminRaceParams) (User, error) {
|
|
row := q.db.QueryRow(ctx, createUserFirstAdminRace,
|
|
arg.Username,
|
|
arg.PasswordHash,
|
|
arg.ApiToken,
|
|
arg.DisplayName,
|
|
)
|
|
var i User
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Username,
|
|
&i.PasswordHash,
|
|
&i.ApiToken,
|
|
&i.IsAdmin,
|
|
&i.CreatedAt,
|
|
&i.SubsonicPassword,
|
|
&i.ListenbrainzToken,
|
|
&i.ListenbrainzEnabled,
|
|
&i.DisplayName,
|
|
&i.AutoApproveRequests,
|
|
&i.Email,
|
|
&i.Timezone,
|
|
&i.TimezoneUpdatedAt,
|
|
&i.DebugModeEnabled,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const deleteUser = `-- name: DeleteUser :exec
|
|
DELETE FROM users WHERE id = $1
|
|
`
|
|
|
|
// 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.
|
|
func (q *Queries) DeleteUser(ctx context.Context, id pgtype.UUID) error {
|
|
_, err := q.db.Exec(ctx, deleteUser, id)
|
|
return err
|
|
}
|
|
|
|
const getListenBrainzConfig = `-- name: GetListenBrainzConfig :one
|
|
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
|
|
`
|
|
|
|
type GetListenBrainzConfigRow struct {
|
|
ListenbrainzToken *string
|
|
ListenbrainzEnabled bool
|
|
LastScrobbledAt pgtype.Timestamptz
|
|
}
|
|
|
|
// Returns the user's LB token + enabled flag and the most recent
|
|
// play_events.scrobbled_at for last-scrobbled-at status.
|
|
func (q *Queries) GetListenBrainzConfig(ctx context.Context, id pgtype.UUID) (GetListenBrainzConfigRow, error) {
|
|
row := q.db.QueryRow(ctx, getListenBrainzConfig, id)
|
|
var i GetListenBrainzConfigRow
|
|
err := row.Scan(&i.ListenbrainzToken, &i.ListenbrainzEnabled, &i.LastScrobbledAt)
|
|
return i, err
|
|
}
|
|
|
|
const getOldestAdmin = `-- name: GetOldestAdmin :one
|
|
SELECT id, username, password_hash, api_token, is_admin, created_at, subsonic_password, listenbrainz_token, listenbrainz_enabled, display_name, auto_approve_requests, email, timezone, timezone_updated_at, debug_mode_enabled FROM users
|
|
WHERE is_admin = true
|
|
ORDER BY created_at, id
|
|
LIMIT 1
|
|
`
|
|
|
|
// The account a system-initiated action is attributed to (milestone #290).
|
|
// lidarr_requests.user_id is NOT NULL and a re-acquisition has no requesting
|
|
// human, so the row is owned by the longest-standing admin: it keeps the
|
|
// request auditable and puts it in the same admin queue as everything else,
|
|
// without inventing a synthetic principal the rest of the schema would have
|
|
// to understand. Ordered by id as a tiebreak so the choice is stable across
|
|
// calls rather than depending on scan order.
|
|
func (q *Queries) GetOldestAdmin(ctx context.Context) (User, error) {
|
|
row := q.db.QueryRow(ctx, getOldestAdmin)
|
|
var i User
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Username,
|
|
&i.PasswordHash,
|
|
&i.ApiToken,
|
|
&i.IsAdmin,
|
|
&i.CreatedAt,
|
|
&i.SubsonicPassword,
|
|
&i.ListenbrainzToken,
|
|
&i.ListenbrainzEnabled,
|
|
&i.DisplayName,
|
|
&i.AutoApproveRequests,
|
|
&i.Email,
|
|
&i.Timezone,
|
|
&i.TimezoneUpdatedAt,
|
|
&i.DebugModeEnabled,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getUserByAPIToken = `-- name: GetUserByAPIToken :one
|
|
SELECT id, username, password_hash, api_token, is_admin, created_at, subsonic_password, listenbrainz_token, listenbrainz_enabled, display_name, auto_approve_requests, email, timezone, timezone_updated_at, debug_mode_enabled FROM users WHERE api_token = $1
|
|
`
|
|
|
|
func (q *Queries) GetUserByAPIToken(ctx context.Context, apiToken string) (User, error) {
|
|
row := q.db.QueryRow(ctx, getUserByAPIToken, apiToken)
|
|
var i User
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Username,
|
|
&i.PasswordHash,
|
|
&i.ApiToken,
|
|
&i.IsAdmin,
|
|
&i.CreatedAt,
|
|
&i.SubsonicPassword,
|
|
&i.ListenbrainzToken,
|
|
&i.ListenbrainzEnabled,
|
|
&i.DisplayName,
|
|
&i.AutoApproveRequests,
|
|
&i.Email,
|
|
&i.Timezone,
|
|
&i.TimezoneUpdatedAt,
|
|
&i.DebugModeEnabled,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getUserByEmail = `-- name: GetUserByEmail :one
|
|
SELECT id, username, password_hash, api_token, is_admin, created_at, subsonic_password, listenbrainz_token, listenbrainz_enabled, display_name, auto_approve_requests, email, timezone, timezone_updated_at, debug_mode_enabled FROM users WHERE lower(email) = lower($1)
|
|
`
|
|
|
|
// Used by forgot-password lookup. Lowercase comparison both sides
|
|
// to keep the unique index happy and to make the lookup
|
|
// case-insensitive.
|
|
func (q *Queries) GetUserByEmail(ctx context.Context, lower string) (User, error) {
|
|
row := q.db.QueryRow(ctx, getUserByEmail, lower)
|
|
var i User
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Username,
|
|
&i.PasswordHash,
|
|
&i.ApiToken,
|
|
&i.IsAdmin,
|
|
&i.CreatedAt,
|
|
&i.SubsonicPassword,
|
|
&i.ListenbrainzToken,
|
|
&i.ListenbrainzEnabled,
|
|
&i.DisplayName,
|
|
&i.AutoApproveRequests,
|
|
&i.Email,
|
|
&i.Timezone,
|
|
&i.TimezoneUpdatedAt,
|
|
&i.DebugModeEnabled,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getUserByID = `-- name: GetUserByID :one
|
|
SELECT id, username, password_hash, api_token, is_admin, created_at, subsonic_password, listenbrainz_token, listenbrainz_enabled, display_name, auto_approve_requests, email, timezone, timezone_updated_at, debug_mode_enabled FROM users WHERE id = $1
|
|
`
|
|
|
|
func (q *Queries) GetUserByID(ctx context.Context, id pgtype.UUID) (User, error) {
|
|
row := q.db.QueryRow(ctx, getUserByID, id)
|
|
var i User
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Username,
|
|
&i.PasswordHash,
|
|
&i.ApiToken,
|
|
&i.IsAdmin,
|
|
&i.CreatedAt,
|
|
&i.SubsonicPassword,
|
|
&i.ListenbrainzToken,
|
|
&i.ListenbrainzEnabled,
|
|
&i.DisplayName,
|
|
&i.AutoApproveRequests,
|
|
&i.Email,
|
|
&i.Timezone,
|
|
&i.TimezoneUpdatedAt,
|
|
&i.DebugModeEnabled,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getUserByUsername = `-- name: GetUserByUsername :one
|
|
SELECT id, username, password_hash, api_token, is_admin, created_at, subsonic_password, listenbrainz_token, listenbrainz_enabled, display_name, auto_approve_requests, email, timezone, timezone_updated_at, debug_mode_enabled FROM users WHERE username = $1
|
|
`
|
|
|
|
func (q *Queries) GetUserByUsername(ctx context.Context, username string) (User, error) {
|
|
row := q.db.QueryRow(ctx, getUserByUsername, username)
|
|
var i User
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Username,
|
|
&i.PasswordHash,
|
|
&i.ApiToken,
|
|
&i.IsAdmin,
|
|
&i.CreatedAt,
|
|
&i.SubsonicPassword,
|
|
&i.ListenbrainzToken,
|
|
&i.ListenbrainzEnabled,
|
|
&i.DisplayName,
|
|
&i.AutoApproveRequests,
|
|
&i.Email,
|
|
&i.Timezone,
|
|
&i.TimezoneUpdatedAt,
|
|
&i.DebugModeEnabled,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const listActiveUsersWithTimezones = `-- name: ListActiveUsersWithTimezones :many
|
|
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'
|
|
)
|
|
`
|
|
|
|
type ListActiveUsersWithTimezonesRow struct {
|
|
ID pgtype.UUID
|
|
Timezone string
|
|
}
|
|
|
|
// 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.
|
|
func (q *Queries) ListActiveUsersWithTimezones(ctx context.Context) ([]ListActiveUsersWithTimezonesRow, error) {
|
|
rows, err := q.db.Query(ctx, listActiveUsersWithTimezones)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []ListActiveUsersWithTimezonesRow
|
|
for rows.Next() {
|
|
var i ListActiveUsersWithTimezonesRow
|
|
if err := rows.Scan(&i.ID, &i.Timezone); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listUsers = `-- name: ListUsers :many
|
|
SELECT id, username, display_name, is_admin, auto_approve_requests,
|
|
debug_mode_enabled, created_at
|
|
FROM users
|
|
ORDER BY created_at DESC
|
|
`
|
|
|
|
type ListUsersRow struct {
|
|
ID pgtype.UUID
|
|
Username string
|
|
DisplayName *string
|
|
IsAdmin bool
|
|
AutoApproveRequests bool
|
|
DebugModeEnabled bool
|
|
CreatedAt pgtype.Timestamptz
|
|
}
|
|
|
|
// Admin user-management list. Sort newest-first.
|
|
func (q *Queries) ListUsers(ctx context.Context) ([]ListUsersRow, error) {
|
|
rows, err := q.db.Query(ctx, listUsers)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []ListUsersRow
|
|
for rows.Next() {
|
|
var i ListUsersRow
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.Username,
|
|
&i.DisplayName,
|
|
&i.IsAdmin,
|
|
&i.AutoApproveRequests,
|
|
&i.DebugModeEnabled,
|
|
&i.CreatedAt,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const regenerateApiToken = `-- name: RegenerateApiToken :one
|
|
UPDATE users SET api_token = $2 WHERE id = $1
|
|
RETURNING id, username, password_hash, api_token, is_admin, created_at, subsonic_password, listenbrainz_token, listenbrainz_enabled, display_name, auto_approve_requests, email, timezone, timezone_updated_at, debug_mode_enabled
|
|
`
|
|
|
|
type RegenerateApiTokenParams struct {
|
|
ID pgtype.UUID
|
|
ApiToken string
|
|
}
|
|
|
|
// Self-service: caller wants a new API token. Used by the /settings
|
|
// API Token card's "Regenerate" button.
|
|
func (q *Queries) RegenerateApiToken(ctx context.Context, arg RegenerateApiTokenParams) (User, error) {
|
|
row := q.db.QueryRow(ctx, regenerateApiToken, arg.ID, arg.ApiToken)
|
|
var i User
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Username,
|
|
&i.PasswordHash,
|
|
&i.ApiToken,
|
|
&i.IsAdmin,
|
|
&i.CreatedAt,
|
|
&i.SubsonicPassword,
|
|
&i.ListenbrainzToken,
|
|
&i.ListenbrainzEnabled,
|
|
&i.DisplayName,
|
|
&i.AutoApproveRequests,
|
|
&i.Email,
|
|
&i.Timezone,
|
|
&i.TimezoneUpdatedAt,
|
|
&i.DebugModeEnabled,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const resetUserPassword = `-- name: ResetUserPassword :exec
|
|
UPDATE users
|
|
SET password_hash = $2
|
|
WHERE id = $1
|
|
`
|
|
|
|
type ResetUserPasswordParams struct {
|
|
ID pgtype.UUID
|
|
PasswordHash string
|
|
}
|
|
|
|
// 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).
|
|
func (q *Queries) ResetUserPassword(ctx context.Context, arg ResetUserPasswordParams) error {
|
|
_, err := q.db.Exec(ctx, resetUserPassword, arg.ID, arg.PasswordHash)
|
|
return err
|
|
}
|
|
|
|
const setDebugMode = `-- name: SetDebugMode :one
|
|
UPDATE users
|
|
SET debug_mode_enabled = $2
|
|
WHERE id = $1
|
|
RETURNING id, username, password_hash, api_token, is_admin, created_at, subsonic_password, listenbrainz_token, listenbrainz_enabled, display_name, auto_approve_requests, email, timezone, timezone_updated_at, debug_mode_enabled
|
|
`
|
|
|
|
type SetDebugModeParams struct {
|
|
ID pgtype.UUID
|
|
DebugModeEnabled bool
|
|
}
|
|
|
|
// 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.
|
|
func (q *Queries) SetDebugMode(ctx context.Context, arg SetDebugModeParams) (User, error) {
|
|
row := q.db.QueryRow(ctx, setDebugMode, arg.ID, arg.DebugModeEnabled)
|
|
var i User
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Username,
|
|
&i.PasswordHash,
|
|
&i.ApiToken,
|
|
&i.IsAdmin,
|
|
&i.CreatedAt,
|
|
&i.SubsonicPassword,
|
|
&i.ListenbrainzToken,
|
|
&i.ListenbrainzEnabled,
|
|
&i.DisplayName,
|
|
&i.AutoApproveRequests,
|
|
&i.Email,
|
|
&i.Timezone,
|
|
&i.TimezoneUpdatedAt,
|
|
&i.DebugModeEnabled,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const setListenBrainzEnabled = `-- name: SetListenBrainzEnabled :exec
|
|
UPDATE users
|
|
SET listenbrainz_enabled = $2
|
|
WHERE id = $1
|
|
`
|
|
|
|
type SetListenBrainzEnabledParams struct {
|
|
ID pgtype.UUID
|
|
ListenbrainzEnabled bool
|
|
}
|
|
|
|
func (q *Queries) SetListenBrainzEnabled(ctx context.Context, arg SetListenBrainzEnabledParams) error {
|
|
_, err := q.db.Exec(ctx, setListenBrainzEnabled, arg.ID, arg.ListenbrainzEnabled)
|
|
return err
|
|
}
|
|
|
|
const setListenBrainzToken = `-- name: SetListenBrainzToken :exec
|
|
UPDATE users
|
|
SET listenbrainz_token = $2,
|
|
listenbrainz_enabled = CASE WHEN COALESCE($2, '') = '' THEN FALSE ELSE listenbrainz_enabled END
|
|
WHERE id = $1
|
|
`
|
|
|
|
type SetListenBrainzTokenParams struct {
|
|
ID pgtype.UUID
|
|
ListenbrainzToken *string
|
|
}
|
|
|
|
func (q *Queries) SetListenBrainzToken(ctx context.Context, arg SetListenBrainzTokenParams) error {
|
|
_, err := q.db.Exec(ctx, setListenBrainzToken, arg.ID, arg.ListenbrainzToken)
|
|
return err
|
|
}
|
|
|
|
const setSubsonicPassword = `-- name: SetSubsonicPassword :exec
|
|
UPDATE users SET subsonic_password = $2 WHERE id = $1
|
|
`
|
|
|
|
type SetSubsonicPasswordParams struct {
|
|
ID pgtype.UUID
|
|
SubsonicPassword *string
|
|
}
|
|
|
|
// 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.
|
|
func (q *Queries) SetSubsonicPassword(ctx context.Context, arg SetSubsonicPasswordParams) error {
|
|
_, err := q.db.Exec(ctx, setSubsonicPassword, arg.ID, arg.SubsonicPassword)
|
|
return err
|
|
}
|
|
|
|
const updateUserAdmin = `-- name: UpdateUserAdmin :one
|
|
UPDATE users
|
|
SET is_admin = $2
|
|
WHERE id = $1
|
|
RETURNING id, username, password_hash, api_token, is_admin, created_at, subsonic_password, listenbrainz_token, listenbrainz_enabled, display_name, auto_approve_requests, email, timezone, timezone_updated_at, debug_mode_enabled
|
|
`
|
|
|
|
type UpdateUserAdminParams struct {
|
|
ID pgtype.UUID
|
|
IsAdmin bool
|
|
}
|
|
|
|
// Sets is_admin to the given value. Returns the updated row so the
|
|
// handler can echo it back to the caller.
|
|
func (q *Queries) UpdateUserAdmin(ctx context.Context, arg UpdateUserAdminParams) (User, error) {
|
|
row := q.db.QueryRow(ctx, updateUserAdmin, arg.ID, arg.IsAdmin)
|
|
var i User
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Username,
|
|
&i.PasswordHash,
|
|
&i.ApiToken,
|
|
&i.IsAdmin,
|
|
&i.CreatedAt,
|
|
&i.SubsonicPassword,
|
|
&i.ListenbrainzToken,
|
|
&i.ListenbrainzEnabled,
|
|
&i.DisplayName,
|
|
&i.AutoApproveRequests,
|
|
&i.Email,
|
|
&i.Timezone,
|
|
&i.TimezoneUpdatedAt,
|
|
&i.DebugModeEnabled,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const updateUserAutoApprove = `-- name: UpdateUserAutoApprove :one
|
|
UPDATE users
|
|
SET auto_approve_requests = $2
|
|
WHERE id = $1
|
|
RETURNING id, username, password_hash, api_token, is_admin, created_at, subsonic_password, listenbrainz_token, listenbrainz_enabled, display_name, auto_approve_requests, email, timezone, timezone_updated_at, debug_mode_enabled
|
|
`
|
|
|
|
type UpdateUserAutoApproveParams struct {
|
|
ID pgtype.UUID
|
|
AutoApproveRequests bool
|
|
}
|
|
|
|
// Toggle the per-user auto_approve_requests flag.
|
|
func (q *Queries) UpdateUserAutoApprove(ctx context.Context, arg UpdateUserAutoApproveParams) (User, error) {
|
|
row := q.db.QueryRow(ctx, updateUserAutoApprove, arg.ID, arg.AutoApproveRequests)
|
|
var i User
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Username,
|
|
&i.PasswordHash,
|
|
&i.ApiToken,
|
|
&i.IsAdmin,
|
|
&i.CreatedAt,
|
|
&i.SubsonicPassword,
|
|
&i.ListenbrainzToken,
|
|
&i.ListenbrainzEnabled,
|
|
&i.DisplayName,
|
|
&i.AutoApproveRequests,
|
|
&i.Email,
|
|
&i.Timezone,
|
|
&i.TimezoneUpdatedAt,
|
|
&i.DebugModeEnabled,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const updateUserProfile = `-- name: UpdateUserProfile :one
|
|
UPDATE users
|
|
SET display_name = $2,
|
|
email = $3
|
|
WHERE id = $1
|
|
RETURNING id, username, password_hash, api_token, is_admin, created_at, subsonic_password, listenbrainz_token, listenbrainz_enabled, display_name, auto_approve_requests, email, timezone, timezone_updated_at, debug_mode_enabled
|
|
`
|
|
|
|
type UpdateUserProfileParams struct {
|
|
ID pgtype.UUID
|
|
DisplayName *string
|
|
Email *string
|
|
}
|
|
|
|
// 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.
|
|
func (q *Queries) UpdateUserProfile(ctx context.Context, arg UpdateUserProfileParams) (User, error) {
|
|
row := q.db.QueryRow(ctx, updateUserProfile, arg.ID, arg.DisplayName, arg.Email)
|
|
var i User
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Username,
|
|
&i.PasswordHash,
|
|
&i.ApiToken,
|
|
&i.IsAdmin,
|
|
&i.CreatedAt,
|
|
&i.SubsonicPassword,
|
|
&i.ListenbrainzToken,
|
|
&i.ListenbrainzEnabled,
|
|
&i.DisplayName,
|
|
&i.AutoApproveRequests,
|
|
&i.Email,
|
|
&i.Timezone,
|
|
&i.TimezoneUpdatedAt,
|
|
&i.DebugModeEnabled,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const updateUserTimezone = `-- name: UpdateUserTimezone :exec
|
|
UPDATE users
|
|
SET timezone = $2,
|
|
timezone_updated_at = now()
|
|
WHERE id = $1
|
|
`
|
|
|
|
type UpdateUserTimezoneParams struct {
|
|
ID pgtype.UUID
|
|
Timezone string
|
|
}
|
|
|
|
// 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.
|
|
func (q *Queries) UpdateUserTimezone(ctx context.Context, arg UpdateUserTimezoneParams) error {
|
|
_, err := q.db.Exec(ctx, updateUserTimezone, arg.ID, arg.Timezone)
|
|
return err
|
|
}
|