/** * 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(Apis.account.update_profile({ data: cmd })) export const updateUsername = (username: string) => unwrapApi(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 { 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(Apis.user.delete_avatar()) /* ---- password & 2FA ------------------------------------------------------- */ export const changePassword = (cmd: ChangePasswordCmd) => unwrapApi(Apis.auth.change_password({ data: cmd })) export const totpStatus = () => unwrapApi(Apis.auth.totp_status()) export const totpSetup = () => unwrapApi(Apis.auth.totp_setup()) /** Enabling 2FA hands back the one-time recovery codes. */ export const totpEnable = (code: string, password: string) => unwrapApi(Apis.auth.totp_enable({ data: { code, password } })) export const totpDisable = (code: string) => unwrapApi(Apis.auth.totp_disable({ data: { code } })) export const recoveryCodes = () => unwrapApi(Apis.auth.recovery_codes()) /** Rotation requires the current password. */ export const regenerateRecoveryCodes = (password: string) => unwrapApi(Apis.auth.regenerate_recovery_codes({ data: { password } })) /* ---- sessions & devices --------------------------------------------------- */ export const listSessions = () => unwrapApi(Apis.user.list_sessions({ params: { limit: 25 } })) export const revokeSession = (sessionId: string) => unwrapApi(Apis.user.revoke_session({ pathParams: { session_id: sessionId } })) export const revokeAllSessions = () => unwrapApi(Apis.user.revoke_all_sessions()) export const listDevices = () => unwrapApi(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(Apis.account.get_preferences()) export const updatePreferences = (cmd: UpdatePreferencesCmd) => unwrapApi(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(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(Apis.security.delete_access_key({ pathParams: { key_id: keyId } })) /* ---- ssh public keys (git over SSH) -------------------------------------- */ export const listSshKeys = () => unwrapApi(Apis.security.list_ssh_keys({ params: { limit: 50 } })) export const addSshKey = (name: string, content: string) => unwrapApi(Apis.security.add_ssh_key({ data: { name, content } })) export const deleteSshKey = (keyId: string) => unwrapApi(Apis.security.delete_ssh_key({ pathParams: { key_id: keyId } })) /* ---- account status ------------------------------------------------------- */ export const listSuspensions = () => unwrapApi(Apis.user.my_suspensions({ params: {} }))