feat: 优化web
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
import { getPublicApiOrigin } from '@/lib/env';
|
||||
import { emit401Unauthorized, emit403Forbidden } from '@/lib/notify/auth-events';
|
||||
import { refreshTokensShared } from '@/lib/api/refresh-flight';
|
||||
import { ApiError, type ApiEnvelope } from '@/lib/api/types';
|
||||
|
||||
type GetTokens = () => { accessToken: string | null; refreshToken: string | null };
|
||||
type SetTokens = (access: string | null, refresh: string | null) => void;
|
||||
|
||||
let tokenBridge: { get: GetTokens; set: SetTokens } | null = null;
|
||||
|
||||
/** 由 auth store 在客户端挂载时注册,避免循环依赖。 */
|
||||
export function registerTokenBridge(get: GetTokens, set: SetTokens) {
|
||||
tokenBridge = { get, set };
|
||||
}
|
||||
|
||||
let tenantIdGetter: () => string | null = () => null;
|
||||
|
||||
/** 由 tenant store 注册,请求 `/api/v1` 时附加 `X-Tenant-ID`(可选)。 */
|
||||
export function registerTenantHeaderBridge(get: () => string | null) {
|
||||
tenantIdGetter = get;
|
||||
}
|
||||
|
||||
function authHeader(): HeadersInit {
|
||||
const t = tokenBridge?.get().accessToken;
|
||||
return t ? { Authorization: `Bearer ${t}` } : {};
|
||||
}
|
||||
|
||||
function tenantHeader(): HeadersInit {
|
||||
const tid = tenantIdGetter?.();
|
||||
return tid ? { 'X-Tenant-ID': tid } : {};
|
||||
}
|
||||
|
||||
/** 业务 JSON:HTTP 200 时解析 envelope;401 时共享 refresh 后重试一次(登录请求勿走此逻辑)。 */
|
||||
export async function apiJson<T>(
|
||||
path: string,
|
||||
init: RequestInit & { skipRefreshRetry?: boolean } = {}
|
||||
): Promise<T> {
|
||||
const { skipRefreshRetry, ...req } = init;
|
||||
const url = `${getPublicApiOrigin()}${path.startsWith('/') ? path : `/${path}`}`;
|
||||
const method = (req.method || 'GET').toUpperCase();
|
||||
const hasBody = req.body != null && req.body !== '';
|
||||
const jsonHeaders: HeadersInit =
|
||||
method !== 'GET' && method !== 'HEAD' && hasBody ? { 'Content-Type': 'application/json' } : {};
|
||||
const baseHeaders = {
|
||||
...jsonHeaders,
|
||||
...authHeader(),
|
||||
...tenantHeader(),
|
||||
...(req.headers || {}),
|
||||
};
|
||||
|
||||
const first = await fetch(url, {
|
||||
...req,
|
||||
headers: baseHeaders,
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
if (first.status === 401 && !skipRefreshRetry && tokenBridge) {
|
||||
const ok = await refreshTokensShared(
|
||||
() => tokenBridge!.get().refreshToken,
|
||||
(a, r) => tokenBridge!.set(a, r)
|
||||
);
|
||||
if (ok) {
|
||||
const second = await fetch(url, {
|
||||
...req,
|
||||
headers: {
|
||||
...jsonHeaders,
|
||||
...authHeader(),
|
||||
...tenantHeader(),
|
||||
...(req.headers || {}),
|
||||
},
|
||||
credentials: 'include',
|
||||
});
|
||||
return handleEnvelope<T>(second);
|
||||
}
|
||||
}
|
||||
|
||||
return handleEnvelope<T>(first);
|
||||
}
|
||||
|
||||
async function handleEnvelope<T>(res: Response): Promise<T> {
|
||||
const text = await res.text();
|
||||
let body: unknown = null;
|
||||
try {
|
||||
body = text ? JSON.parse(text) : null;
|
||||
} catch {
|
||||
throw new ApiError('响应不是合法 JSON', res.status, text);
|
||||
}
|
||||
|
||||
if (res.status === 401) {
|
||||
const msg = (body as { msg?: string })?.msg || '未授权';
|
||||
emit401Unauthorized(msg);
|
||||
throw new ApiError(msg, 401, body);
|
||||
}
|
||||
if (res.status === 403) {
|
||||
const msg = (body as { msg?: string })?.msg || '禁止访问';
|
||||
emit403Forbidden(msg);
|
||||
throw new ApiError(msg, 403, body);
|
||||
}
|
||||
if (!res.ok) {
|
||||
throw new ApiError(`HTTP ${res.status}`, res.status, body);
|
||||
}
|
||||
|
||||
const env = body as ApiEnvelope<T>;
|
||||
if (typeof env?.code !== 'number') {
|
||||
return body as T;
|
||||
}
|
||||
if (env.code !== 200) {
|
||||
throw new ApiError(env.msg || '业务失败', res.status, env);
|
||||
}
|
||||
return env.data as T;
|
||||
}
|
||||
Reference in New Issue
Block a user