feat(auth): store Subsonic API keys hashed; a new key is shown once (M462 #4983)
test-web / test (push) Successful in 2m4s
test-go / test (push) Successful in 2m23s
test-go / integration (push) Successful in 5m28s
release / Build signed APK (releases and dev) (push) Successful in 6m29s
release / Build + push container image (push) Successful in 29s
release / Verify release artifacts (tag releases only) (push) Skipped
test-web / test (push) Successful in 2m4s
test-go / test (push) Successful in 2m23s
test-go / integration (push) Successful in 5m28s
release / Build signed APK (releases and dev) (push) Successful in 6m29s
release / Build + push container image (push) Successful in 29s
release / Verify release artifacts (tag releases only) (push) Skipped
users.api_token held each user's apiKey in plaintext and was looked up by equality, so a leaked row or backup handed out working keys. Migration 0063 replaces it with api_token_hash (sha256, hex), computed in place from the existing keys so every Subsonic client keeps working. The key can no longer be read back: GET /api/me/api-token is gone, and POST returns the new key once. Settings shows it right after Regenerate with a copy button and a "won't be shown again" note. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -52,15 +52,15 @@ func (q *Queries) CountUsers(ctx context.Context) (int64, error) {
|
||||
}
|
||||
|
||||
const createUser = `-- name: CreateUser :one
|
||||
INSERT INTO users (username, password_hash, api_token, is_admin, display_name)
|
||||
INSERT INTO users (username, password_hash, api_token_hash, 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
|
||||
RETURNING id, username, password_hash, is_admin, created_at, subsonic_password, listenbrainz_token, listenbrainz_enabled, display_name, auto_approve_requests, email, timezone, timezone_updated_at, debug_mode_enabled, api_token_hash
|
||||
`
|
||||
|
||||
type CreateUserParams struct {
|
||||
Username string
|
||||
PasswordHash string
|
||||
ApiToken string
|
||||
ApiTokenHash string
|
||||
IsAdmin bool
|
||||
DisplayName *string
|
||||
}
|
||||
@@ -69,7 +69,7 @@ func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, e
|
||||
row := q.db.QueryRow(ctx, createUser,
|
||||
arg.Username,
|
||||
arg.PasswordHash,
|
||||
arg.ApiToken,
|
||||
arg.ApiTokenHash,
|
||||
arg.IsAdmin,
|
||||
arg.DisplayName,
|
||||
)
|
||||
@@ -78,7 +78,6 @@ func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, e
|
||||
&i.ID,
|
||||
&i.Username,
|
||||
&i.PasswordHash,
|
||||
&i.ApiToken,
|
||||
&i.IsAdmin,
|
||||
&i.CreatedAt,
|
||||
&i.SubsonicPassword,
|
||||
@@ -90,20 +89,21 @@ func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, e
|
||||
&i.Timezone,
|
||||
&i.TimezoneUpdatedAt,
|
||||
&i.DebugModeEnabled,
|
||||
&i.ApiTokenHash,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const createUserAdmin = `-- name: CreateUserAdmin :one
|
||||
INSERT INTO users (username, password_hash, api_token, is_admin, display_name)
|
||||
INSERT INTO users (username, password_hash, api_token_hash, 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
|
||||
RETURNING id, username, password_hash, is_admin, created_at, subsonic_password, listenbrainz_token, listenbrainz_enabled, display_name, auto_approve_requests, email, timezone, timezone_updated_at, debug_mode_enabled, api_token_hash
|
||||
`
|
||||
|
||||
type CreateUserAdminParams struct {
|
||||
Username string
|
||||
PasswordHash string
|
||||
ApiToken string
|
||||
ApiTokenHash string
|
||||
IsAdmin bool
|
||||
DisplayName *string
|
||||
}
|
||||
@@ -115,7 +115,7 @@ func (q *Queries) CreateUserAdmin(ctx context.Context, arg CreateUserAdminParams
|
||||
row := q.db.QueryRow(ctx, createUserAdmin,
|
||||
arg.Username,
|
||||
arg.PasswordHash,
|
||||
arg.ApiToken,
|
||||
arg.ApiTokenHash,
|
||||
arg.IsAdmin,
|
||||
arg.DisplayName,
|
||||
)
|
||||
@@ -124,7 +124,6 @@ func (q *Queries) CreateUserAdmin(ctx context.Context, arg CreateUserAdminParams
|
||||
&i.ID,
|
||||
&i.Username,
|
||||
&i.PasswordHash,
|
||||
&i.ApiToken,
|
||||
&i.IsAdmin,
|
||||
&i.CreatedAt,
|
||||
&i.SubsonicPassword,
|
||||
@@ -136,24 +135,25 @@ func (q *Queries) CreateUserAdmin(ctx context.Context, arg CreateUserAdminParams
|
||||
&i.Timezone,
|
||||
&i.TimezoneUpdatedAt,
|
||||
&i.DebugModeEnabled,
|
||||
&i.ApiTokenHash,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const createUserFirstAdminRace = `-- name: CreateUserFirstAdminRace :one
|
||||
INSERT INTO users (username, password_hash, api_token, is_admin, display_name)
|
||||
INSERT INTO users (username, password_hash, api_token_hash, 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
|
||||
RETURNING id, username, password_hash, is_admin, created_at, subsonic_password, listenbrainz_token, listenbrainz_enabled, display_name, auto_approve_requests, email, timezone, timezone_updated_at, debug_mode_enabled, api_token_hash
|
||||
`
|
||||
|
||||
type CreateUserFirstAdminRaceParams struct {
|
||||
Username string
|
||||
PasswordHash string
|
||||
ApiToken string
|
||||
ApiTokenHash string
|
||||
DisplayName *string
|
||||
}
|
||||
|
||||
@@ -174,7 +174,7 @@ func (q *Queries) CreateUserFirstAdminRace(ctx context.Context, arg CreateUserFi
|
||||
row := q.db.QueryRow(ctx, createUserFirstAdminRace,
|
||||
arg.Username,
|
||||
arg.PasswordHash,
|
||||
arg.ApiToken,
|
||||
arg.ApiTokenHash,
|
||||
arg.DisplayName,
|
||||
)
|
||||
var i User
|
||||
@@ -182,7 +182,6 @@ func (q *Queries) CreateUserFirstAdminRace(ctx context.Context, arg CreateUserFi
|
||||
&i.ID,
|
||||
&i.Username,
|
||||
&i.PasswordHash,
|
||||
&i.ApiToken,
|
||||
&i.IsAdmin,
|
||||
&i.CreatedAt,
|
||||
&i.SubsonicPassword,
|
||||
@@ -194,6 +193,7 @@ func (q *Queries) CreateUserFirstAdminRace(ctx context.Context, arg CreateUserFi
|
||||
&i.Timezone,
|
||||
&i.TimezoneUpdatedAt,
|
||||
&i.DebugModeEnabled,
|
||||
&i.ApiTokenHash,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -240,7 +240,7 @@ func (q *Queries) GetListenBrainzConfig(ctx context.Context, id pgtype.UUID) (Ge
|
||||
}
|
||||
|
||||
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
|
||||
SELECT id, username, password_hash, is_admin, created_at, subsonic_password, listenbrainz_token, listenbrainz_enabled, display_name, auto_approve_requests, email, timezone, timezone_updated_at, debug_mode_enabled, api_token_hash FROM users
|
||||
WHERE is_admin = true
|
||||
ORDER BY created_at, id
|
||||
LIMIT 1
|
||||
@@ -260,7 +260,6 @@ func (q *Queries) GetOldestAdmin(ctx context.Context) (User, error) {
|
||||
&i.ID,
|
||||
&i.Username,
|
||||
&i.PasswordHash,
|
||||
&i.ApiToken,
|
||||
&i.IsAdmin,
|
||||
&i.CreatedAt,
|
||||
&i.SubsonicPassword,
|
||||
@@ -272,22 +271,22 @@ func (q *Queries) GetOldestAdmin(ctx context.Context) (User, error) {
|
||||
&i.Timezone,
|
||||
&i.TimezoneUpdatedAt,
|
||||
&i.DebugModeEnabled,
|
||||
&i.ApiTokenHash,
|
||||
)
|
||||
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
|
||||
const getUserByAPITokenHash = `-- name: GetUserByAPITokenHash :one
|
||||
SELECT id, username, password_hash, is_admin, created_at, subsonic_password, listenbrainz_token, listenbrainz_enabled, display_name, auto_approve_requests, email, timezone, timezone_updated_at, debug_mode_enabled, api_token_hash FROM users WHERE api_token_hash = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetUserByAPIToken(ctx context.Context, apiToken string) (User, error) {
|
||||
row := q.db.QueryRow(ctx, getUserByAPIToken, apiToken)
|
||||
func (q *Queries) GetUserByAPITokenHash(ctx context.Context, apiTokenHash string) (User, error) {
|
||||
row := q.db.QueryRow(ctx, getUserByAPITokenHash, apiTokenHash)
|
||||
var i User
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Username,
|
||||
&i.PasswordHash,
|
||||
&i.ApiToken,
|
||||
&i.IsAdmin,
|
||||
&i.CreatedAt,
|
||||
&i.SubsonicPassword,
|
||||
@@ -299,12 +298,13 @@ func (q *Queries) GetUserByAPIToken(ctx context.Context, apiToken string) (User,
|
||||
&i.Timezone,
|
||||
&i.TimezoneUpdatedAt,
|
||||
&i.DebugModeEnabled,
|
||||
&i.ApiTokenHash,
|
||||
)
|
||||
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)
|
||||
SELECT id, username, password_hash, is_admin, created_at, subsonic_password, listenbrainz_token, listenbrainz_enabled, display_name, auto_approve_requests, email, timezone, timezone_updated_at, debug_mode_enabled, api_token_hash FROM users WHERE lower(email) = lower($1)
|
||||
`
|
||||
|
||||
// Used by forgot-password lookup. Lowercase comparison both sides
|
||||
@@ -317,7 +317,6 @@ func (q *Queries) GetUserByEmail(ctx context.Context, lower string) (User, error
|
||||
&i.ID,
|
||||
&i.Username,
|
||||
&i.PasswordHash,
|
||||
&i.ApiToken,
|
||||
&i.IsAdmin,
|
||||
&i.CreatedAt,
|
||||
&i.SubsonicPassword,
|
||||
@@ -329,12 +328,13 @@ func (q *Queries) GetUserByEmail(ctx context.Context, lower string) (User, error
|
||||
&i.Timezone,
|
||||
&i.TimezoneUpdatedAt,
|
||||
&i.DebugModeEnabled,
|
||||
&i.ApiTokenHash,
|
||||
)
|
||||
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
|
||||
SELECT id, username, password_hash, is_admin, created_at, subsonic_password, listenbrainz_token, listenbrainz_enabled, display_name, auto_approve_requests, email, timezone, timezone_updated_at, debug_mode_enabled, api_token_hash FROM users WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetUserByID(ctx context.Context, id pgtype.UUID) (User, error) {
|
||||
@@ -344,7 +344,6 @@ func (q *Queries) GetUserByID(ctx context.Context, id pgtype.UUID) (User, error)
|
||||
&i.ID,
|
||||
&i.Username,
|
||||
&i.PasswordHash,
|
||||
&i.ApiToken,
|
||||
&i.IsAdmin,
|
||||
&i.CreatedAt,
|
||||
&i.SubsonicPassword,
|
||||
@@ -356,12 +355,13 @@ func (q *Queries) GetUserByID(ctx context.Context, id pgtype.UUID) (User, error)
|
||||
&i.Timezone,
|
||||
&i.TimezoneUpdatedAt,
|
||||
&i.DebugModeEnabled,
|
||||
&i.ApiTokenHash,
|
||||
)
|
||||
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
|
||||
SELECT id, username, password_hash, is_admin, created_at, subsonic_password, listenbrainz_token, listenbrainz_enabled, display_name, auto_approve_requests, email, timezone, timezone_updated_at, debug_mode_enabled, api_token_hash FROM users WHERE username = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetUserByUsername(ctx context.Context, username string) (User, error) {
|
||||
@@ -371,7 +371,6 @@ func (q *Queries) GetUserByUsername(ctx context.Context, username string) (User,
|
||||
&i.ID,
|
||||
&i.Username,
|
||||
&i.PasswordHash,
|
||||
&i.ApiToken,
|
||||
&i.IsAdmin,
|
||||
&i.CreatedAt,
|
||||
&i.SubsonicPassword,
|
||||
@@ -383,6 +382,7 @@ func (q *Queries) GetUserByUsername(ctx context.Context, username string) (User,
|
||||
&i.Timezone,
|
||||
&i.TimezoneUpdatedAt,
|
||||
&i.DebugModeEnabled,
|
||||
&i.ApiTokenHash,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -471,39 +471,21 @@ func (q *Queries) ListUsers(ctx context.Context) ([]ListUsersRow, error) {
|
||||
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
|
||||
const regenerateApiToken = `-- name: RegenerateApiToken :exec
|
||||
UPDATE users SET api_token_hash = $2 WHERE id = $1
|
||||
`
|
||||
|
||||
type RegenerateApiTokenParams struct {
|
||||
ID pgtype.UUID
|
||||
ApiToken string
|
||||
ID pgtype.UUID
|
||||
ApiTokenHash 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
|
||||
// API Token card's "Regenerate" button. Only the hash is stored; the
|
||||
// handler returns the raw key once.
|
||||
func (q *Queries) RegenerateApiToken(ctx context.Context, arg RegenerateApiTokenParams) error {
|
||||
_, err := q.db.Exec(ctx, regenerateApiToken, arg.ID, arg.ApiTokenHash)
|
||||
return err
|
||||
}
|
||||
|
||||
const resetUserPassword = `-- name: ResetUserPassword :exec
|
||||
@@ -530,7 +512,7 @@ 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
|
||||
RETURNING id, username, password_hash, is_admin, created_at, subsonic_password, listenbrainz_token, listenbrainz_enabled, display_name, auto_approve_requests, email, timezone, timezone_updated_at, debug_mode_enabled, api_token_hash
|
||||
`
|
||||
|
||||
type SetDebugModeParams struct {
|
||||
@@ -548,7 +530,6 @@ func (q *Queries) SetDebugMode(ctx context.Context, arg SetDebugModeParams) (Use
|
||||
&i.ID,
|
||||
&i.Username,
|
||||
&i.PasswordHash,
|
||||
&i.ApiToken,
|
||||
&i.IsAdmin,
|
||||
&i.CreatedAt,
|
||||
&i.SubsonicPassword,
|
||||
@@ -560,6 +541,7 @@ func (q *Queries) SetDebugMode(ctx context.Context, arg SetDebugModeParams) (Use
|
||||
&i.Timezone,
|
||||
&i.TimezoneUpdatedAt,
|
||||
&i.DebugModeEnabled,
|
||||
&i.ApiTokenHash,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -617,7 +599,7 @@ 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
|
||||
RETURNING id, username, password_hash, is_admin, created_at, subsonic_password, listenbrainz_token, listenbrainz_enabled, display_name, auto_approve_requests, email, timezone, timezone_updated_at, debug_mode_enabled, api_token_hash
|
||||
`
|
||||
|
||||
type UpdateUserAdminParams struct {
|
||||
@@ -634,7 +616,6 @@ func (q *Queries) UpdateUserAdmin(ctx context.Context, arg UpdateUserAdminParams
|
||||
&i.ID,
|
||||
&i.Username,
|
||||
&i.PasswordHash,
|
||||
&i.ApiToken,
|
||||
&i.IsAdmin,
|
||||
&i.CreatedAt,
|
||||
&i.SubsonicPassword,
|
||||
@@ -646,6 +627,7 @@ func (q *Queries) UpdateUserAdmin(ctx context.Context, arg UpdateUserAdminParams
|
||||
&i.Timezone,
|
||||
&i.TimezoneUpdatedAt,
|
||||
&i.DebugModeEnabled,
|
||||
&i.ApiTokenHash,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -654,7 +636,7 @@ 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
|
||||
RETURNING id, username, password_hash, is_admin, created_at, subsonic_password, listenbrainz_token, listenbrainz_enabled, display_name, auto_approve_requests, email, timezone, timezone_updated_at, debug_mode_enabled, api_token_hash
|
||||
`
|
||||
|
||||
type UpdateUserAutoApproveParams struct {
|
||||
@@ -670,7 +652,6 @@ func (q *Queries) UpdateUserAutoApprove(ctx context.Context, arg UpdateUserAutoA
|
||||
&i.ID,
|
||||
&i.Username,
|
||||
&i.PasswordHash,
|
||||
&i.ApiToken,
|
||||
&i.IsAdmin,
|
||||
&i.CreatedAt,
|
||||
&i.SubsonicPassword,
|
||||
@@ -682,6 +663,7 @@ func (q *Queries) UpdateUserAutoApprove(ctx context.Context, arg UpdateUserAutoA
|
||||
&i.Timezone,
|
||||
&i.TimezoneUpdatedAt,
|
||||
&i.DebugModeEnabled,
|
||||
&i.ApiTokenHash,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -691,7 +673,7 @@ 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
|
||||
RETURNING id, username, password_hash, is_admin, created_at, subsonic_password, listenbrainz_token, listenbrainz_enabled, display_name, auto_approve_requests, email, timezone, timezone_updated_at, debug_mode_enabled, api_token_hash
|
||||
`
|
||||
|
||||
type UpdateUserProfileParams struct {
|
||||
@@ -709,7 +691,6 @@ func (q *Queries) UpdateUserProfile(ctx context.Context, arg UpdateUserProfilePa
|
||||
&i.ID,
|
||||
&i.Username,
|
||||
&i.PasswordHash,
|
||||
&i.ApiToken,
|
||||
&i.IsAdmin,
|
||||
&i.CreatedAt,
|
||||
&i.SubsonicPassword,
|
||||
@@ -721,6 +702,7 @@ func (q *Queries) UpdateUserProfile(ctx context.Context, arg UpdateUserProfilePa
|
||||
&i.Timezone,
|
||||
&i.TimezoneUpdatedAt,
|
||||
&i.DebugModeEnabled,
|
||||
&i.ApiTokenHash,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user