/** * 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 { ApiFailure, settingsKey, unwrapApi } from '@app/lib' import type { ChangePasswordCmd, UpdatePreferencesCmd, UpdateProfileCmd } from '@app/services/api' import Apis, { trackFetch } from '@app/services/api' import { invalidateCache, writeCache } from '@app/services/storage' import type { 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<{ items?: DeviceRow[] }>(Apis.account.list_devices({ params: { limit: 100 } })).then( (page) => page?.items ?? [], ) /* ---- 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 }) /* ---- ssh public keys ------------------------------------------------------ */ export const listSshKeys = async (): Promise => unwrapApi(Apis.auth.auth_ssh_key_list({ params: { limit: 100 } })) export const addSshKey = async (name: string, content: string) => unwrapApi(Apis.auth.auth_ssh_key_add({ data: { name, public_key: content } })) export const deleteSshKey = (keyId: string) => unwrapApi(Apis.auth.auth_ssh_key_delete({ pathParams: { key_id: keyId } })) /* ---- account status ------------------------------------------------------- */ /** The endpoint answers with a bare array, not the paged envelope. The section * is a punishment *history*, so revoked entries are included too. */ export const listSuspensions = async (): Promise => { const rows = await unwrapApi( Apis.user.my_suspensions({ params: { include_revoked: true } }), ) return rows ?? [] }