import Apis from '@/shared/api' import { getLocale, translate, type MessageKey } from '@/shared/i18n' import type { AppResponseData_ForgotPasswordResp, AppResponseData_LoginResp, AppResponseData_PowChallengeResp, AppResponseData_RegisterResp, AppResponseData_ResendCodeResp, AppResponseData_VerifyEmailResp, EmailVerificationPurpose, ForgotPasswordCmd, LoginCmd, RegisterCmd, ResendCodeCmd, ResetPasswordCmd, TwoFactorLoginCmd, VerifyEmailCmd, } from '@/shared/api' export type { EmailVerificationPurpose } interface Envelope { code: number message: string data?: T | null error?: string | null error_code?: string | null } export class ApiError extends Error { readonly code: number readonly errorCode?: string | null constructor(message: string, code: number, errorCode?: string | null) { super(message) this.name = 'ApiError' this.code = code this.errorCode = errorCode } } /** * Localized user-facing error text. The server's own wording is the last * resort: `error_code` and status are stable machine-readable handles that the * dictionary can translate, but a validation message built from user input is * not translatable and must pass through unchanged. */ export function toUserError(err: unknown): string { if (err instanceof ApiError) { const key = err.errorCode ? (`error.code.${err.errorCode}` as MessageKey) : null if (key && key in ERROR_KEYS) { return translate(getLocale(), key) } const byStatus = `error.status.${err.code}` as MessageKey if (byStatus in ERROR_KEYS) return translate(getLocale(), byStatus) return err.message } if (err instanceof TypeError) return translate(getLocale(), 'auth.error.network') return translate(getLocale(), 'auth.error.unknown') } /** * Key membership probe: the dictionaries are `Record` at the * boundary, so an unknown server code must be detected rather than assumed. */ const ERROR_KEYS = { 'error.code.invalid_credentials': true, 'error.code.pow_failed': true, 'error.code.unauthorized': true, 'error.code.forbidden': true, 'error.code.not_found': true, 'error.code.conflict': true, 'error.code.rate_limited': true, 'error.code.bad_request': true, 'error.code.invalid_request_body': true, 'error.code.merge_blocked': true, 'error.code.import_disabled': true, 'error.code.import_upstream_failed': true, 'error.code.password_change_required': true, 'error.status.400': true, 'error.status.401': true, 'error.status.403': true, 'error.status.404': true, 'error.status.409': true, 'error.status.429': true, 'error.status.500': true, 'error.status.502': true, 'error.status.503': true, 'error.status.504': true, 'error.status.0': true, 'error.status.unknown': true, } as const satisfies Partial> async function unwrap(method: PromiseLike>): Promise { const res = await method if (res.code !== 200) { throw new ApiError(res.error || res.message || 'Request failed', res.code, res.error_code) } return res.data ?? null } // Challenges are single-use server-side, but alova caches GET responses for // 5 min by default: a retried submit would replay the already-consumed nonce // and fail with "pow challenge missing". A cache controller that always // returns undefined skips both the cache read (even entries stored by older // builds) and the cache write, so every submit fetches a fresh challenge. export const powChallenge = () => unwrap(Apis.auth.pow_challenge({ cacheFor: () => undefined })) export const login = (cmd: LoginCmd) => unwrap(Apis.auth.login({ data: cmd })) export const login2fa = (cmd: TwoFactorLoginCmd) => unwrap(Apis.auth.login_2fa({ data: cmd })) export const register = (cmd: RegisterCmd) => unwrap(Apis.auth.register({ data: cmd })) export const forgotPassword = (cmd: ForgotPasswordCmd) => unwrap(Apis.auth.forgot_password({ data: cmd })) export const resetPassword = (cmd: ResetPasswordCmd) => unwrap(Apis.auth.reset_password({ data: cmd })) export const verifyEmail = (cmd: VerifyEmailCmd) => unwrap(Apis.auth.verify_email({ data: cmd })) export const resendCode = (cmd: ResendCodeCmd) => unwrap(Apis.auth.resend_code({ data: cmd })) export type PowChallenge = NonNullable export type LoginResult = NonNullable export type RegisterResult = NonNullable export type ForgotPasswordResult = NonNullable export type VerifyEmailResult = NonNullable export type ResendCodeResult = NonNullable