759ae5e
/**
* User-settings API: wrappers for every user-account endpoint (profile,
* security, sessions, devices, emails, preferences, access keys, ssh keys,
* suspensions). Workspace navigation data lives in the page slice's
* shell-api; space mutations in features/space-management.
*/
import Apis, { trackFetch } from '@/shared/api'
import { ApiFailure, settingsKey, unwrapApi } from '@/shared/lib'
import { invalidateCache, writeCache } from '@/shared/storage'
import type {
ChangePasswordCmd,
CreateAccessKeyCmd,
UpdateProfileCmd,
UpdatePreferencesCmd,
} from '@/shared/api'
import type {
AccessKey,
AccessKeyPage,
AvatarInfo,
CountResult,
DeviceRow,
RecoveryCodes,
SessionPage,
SshKey,
SshKeyPage,
SuspensionRow,
TotpSetup,
TotpStatus,
UserPreferences,
UserPublic,
} from './user-settings'
/* ---- account & profile ---------------------------------------------------- */
export const updateProfile = (cmd: UpdateProfileCmd) =>
unwrapApi<UserPublic>(Apis.account.update_profile({ data: cmd }))
export const updateUsername = (username: string) =>
unwrapApi<UserPublic>(Apis.account.update_username({ data: { username } }))
/** Binary upload — the generated client types the body as number[], so PUT
* the raw bytes with fetch and reuse the standard envelope contract. */
export async function uploadAvatar(file: File): Promise<AvatarInfo> {
const res = await trackFetch('/api/v1/me/avatar', {
method: 'PUT',
body: file,
headers: { 'content-type': file.type || 'application/octet-stream' },
})
const env = (await res.json()) as {
code: number
message: string
data?: AvatarInfo | null
error?: string | null
}
if (env.code !== 200) {
throw new ApiFailure(env.error || env.message || 'avatar upload failed', env.code)
}
return env.data as AvatarInfo
}
export const deleteAvatar = () => unwrapApi<null>(Apis.user.delete_avatar())
/* ---- password & 2FA ------------------------------------------------------- */
export const changePassword = (cmd: ChangePasswordCmd) =>
unwrapApi<null>(Apis.auth.change_password({ data: cmd }))
export const totpStatus = () => unwrapApi<TotpStatus>(Apis.auth.totp_status())
export const totpSetup = () => unwrapApi<TotpSetup>(Apis.auth.totp_setup())
/** Enabling 2FA hands back the one-time recovery codes. */
export const totpEnable = (code: string, password: string) =>
unwrapApi<RecoveryCodes>(Apis.auth.totp_enable({ data: { code, password } }))
export const totpDisable = (code: string) =>
unwrapApi<null>(Apis.auth.totp_disable({ data: { code } }))
export const recoveryCodes = () =>
unwrapApi<RecoveryCodes>(Apis.auth.recovery_codes())
/** Rotation requires the current password. */
export const regenerateRecoveryCodes = (password: string) =>
unwrapApi<RecoveryCodes>(Apis.auth.regenerate_recovery_codes({ data: { password } }))
/* ---- sessions & devices --------------------------------------------------- */
export const listSessions = () =>
unwrapApi<SessionPage>(Apis.user.list_sessions({ params: { limit: 25 } }))
export const revokeSession = (sessionId: string) =>
unwrapApi<null>(Apis.user.revoke_session({ pathParams: { session_id: sessionId } }))
export const revokeAllSessions = () =>
unwrapApi<CountResult>(Apis.user.revoke_all_sessions())
export const listDevices = () => unwrapApi<DeviceRow[]>(Apis.account.list_devices({ params: {} }))
/* ---- preferences ---------------------------------------------------------- */
/** Canonical home of the preference endpoints; features/space-management keeps
* its own four-line copies for the personal privacy dialog (features may not
* import each other). */
export const fetchPreferences = () =>
unwrapApi<UserPreferences>(Apis.account.get_preferences())
export const updatePreferences = (cmd: UpdatePreferencesCmd) =>
unwrapApi<UserPreferences>(Apis.account.update_preferences({ data: cmd })).then(prefs => {
// The response is the canonical row: store it so every preferences
// consumer sees the change immediately, and drop the derived diff-style
// read the commit view caches under its own key.
writeCache(settingsKey('preferences'), prefs)
invalidateCache(settingsKey('diff-style'))
return prefs
})
/* ---- access keys ---------------------------------------------------------- */
export const listAccessKeys = () =>
unwrapApi<AccessKeyPage>(Apis.security.list_access_keys({ params: { limit: 25 } }))
export const createAccessKey = (cmd: CreateAccessKeyCmd) =>
unwrapApi<{ key: AccessKey; token: string }>(Apis.security.create_access_key({ data: cmd }))
export const rotateAccessKey = (keyId: string) =>
unwrapApi<{ key: AccessKey; token: string }>(
Apis.auth.rotate_access_key({ pathParams: { key_id: keyId } }),
)
export const deleteAccessKey = (keyId: string) =>
unwrapApi<null>(Apis.security.delete_access_key({ pathParams: { key_id: keyId } }))
/* ---- ssh public keys (git over SSH) -------------------------------------- */
export const listSshKeys = () =>
unwrapApi<SshKeyPage>(Apis.security.list_ssh_keys({ params: { limit: 50 } }))
export const addSshKey = (name: string, content: string) =>
unwrapApi<SshKey>(Apis.security.add_ssh_key({ data: { name, content } }))
export const deleteSshKey = (keyId: string) =>
unwrapApi<null>(Apis.security.delete_ssh_key({ pathParams: { key_id: keyId } }))
/* ---- account status ------------------------------------------------------- */
export const listSuspensions = () =>
unwrapApi<SuspensionRow[]>(Apis.user.my_suspensions({ params: {} }))