119 lines
3.7 KiB
TypeScript
119 lines
3.7 KiB
TypeScript
import { getApiConfig } from '../config/api';
|
|
import { beginApiWrite } from './api-activity';
|
|
import {
|
|
ApiRequestError,
|
|
requestApi,
|
|
type RequestOptions,
|
|
} from './api-transport';
|
|
import {
|
|
cachedServerUser,
|
|
clearSession,
|
|
ensureSession,
|
|
getAuthContext,
|
|
recoverSession,
|
|
rejectSession,
|
|
} from './auth-session';
|
|
export { ApiRequestError } from './api-transport';
|
|
|
|
const recoverable = new Set([
|
|
'AUTH_REQUIRED',
|
|
'AUTH_INVALID',
|
|
'AUTH_EXPIRED',
|
|
'AUTH_REVOKED',
|
|
]);
|
|
const blocked = new Set([
|
|
'ACCOUNT_NOT_FOUND',
|
|
'ACCOUNT_DISABLED',
|
|
'WECHAT_LOGIN_BLOCKED',
|
|
]);
|
|
|
|
/** 公开接口显式 auth=none;其余接口合并登录,最多恢复并重发一次,不跨账户补发。 */
|
|
export async function apiRequest<T>(
|
|
path: string,
|
|
options: RequestOptions & { auth?: 'required' | 'none' } = {},
|
|
): Promise<T> {
|
|
const finish =
|
|
options.upload || (options.method && options.method !== 'GET')
|
|
? beginApiWrite()
|
|
: undefined;
|
|
try {
|
|
return await performApiRequest<T>(path, options);
|
|
} finally {
|
|
finish?.();
|
|
}
|
|
}
|
|
|
|
async function performApiRequest<T>(
|
|
path: string,
|
|
options: RequestOptions & { auth?: 'required' | 'none' } = {},
|
|
): Promise<T> {
|
|
// Authorization 由会话层统一提供;公开请求不附带调用方误传的凭证。
|
|
const header = Object.fromEntries(
|
|
Object.entries(options.header || {}).filter(
|
|
([key]) => key.toLowerCase() !== 'authorization',
|
|
),
|
|
);
|
|
if (options.auth === 'none')
|
|
return requestApi(path, { ...options, header }, getApiConfig());
|
|
const context = getAuthContext();
|
|
const previousUserId = cachedServerUser()?.id;
|
|
const initial = await ensureSession(context);
|
|
if (previousUserId && initial.user.id !== previousUserId) {
|
|
clearSession(false, context);
|
|
throw new ApiRequestError(
|
|
'账户已变化,请重新操作',
|
|
401,
|
|
'ACCOUNT_CHANGED',
|
|
);
|
|
}
|
|
const send = async (accessToken: string) => {
|
|
if (getAuthContext().key !== context.key)
|
|
throw new ApiRequestError('接口环境已变化,请重新操作');
|
|
const result = await requestApi<T>(
|
|
path,
|
|
{
|
|
...options,
|
|
header: { ...header, Authorization: `Bearer ${accessToken}` },
|
|
},
|
|
context,
|
|
);
|
|
if (
|
|
getAuthContext().key !== context.key ||
|
|
cachedServerUser()?.id !== initial.user.id
|
|
)
|
|
throw new ApiRequestError('账户状态已变化,请重新操作');
|
|
return result;
|
|
};
|
|
try {
|
|
return await send(initial.accessToken);
|
|
} catch (error) {
|
|
if (!(error instanceof ApiRequestError) || error.statusCode !== 401)
|
|
throw error;
|
|
if (!recoverable.has(error.reason)) {
|
|
if (blocked.has(error.reason))
|
|
rejectSession(context, initial.accessToken, true);
|
|
throw error;
|
|
}
|
|
const renewed = await recoverSession(context, initial);
|
|
if (renewed.user.id !== initial.user.id) {
|
|
clearSession(false, context);
|
|
throw new ApiRequestError(
|
|
'账户已变化,请重新操作',
|
|
401,
|
|
'ACCOUNT_CHANGED',
|
|
);
|
|
}
|
|
try {
|
|
return await send(renewed.accessToken);
|
|
} catch (second) {
|
|
if (second instanceof ApiRequestError && second.statusCode === 401)
|
|
rejectSession(
|
|
context,
|
|
renewed.accessToken,
|
|
blocked.has(second.reason),
|
|
);
|
|
throw second;
|
|
}
|
|
}
|
|
}
|