2687ace
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<T> {
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<string, string>` 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<Record<MessageKey, true>>
async function unwrap<T>(method: PromiseLike<Envelope<T>>): Promise<T | null> {
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<AppResponseData_PowChallengeResp['data']>
export type LoginResult = NonNullable<AppResponseData_LoginResp['data']>
export type RegisterResult = NonNullable<AppResponseData_RegisterResp['data']>
export type ForgotPasswordResult = NonNullable<AppResponseData_ForgotPasswordResp['data']>
export type VerifyEmailResult = NonNullable<AppResponseData_VerifyEmailResp['data']>
export type ResendCodeResult = NonNullable<AppResponseData_ResendCodeResp['data']>