fix:无法设置token的问题
This commit is contained in:
@@ -7,9 +7,9 @@
|
||||
import { ofetch, type $Fetch, type FetchHook } from "ofetch";
|
||||
import { ApiError, ErrorCode } from "./api_result";
|
||||
import { getApiConfig } from "../../../core/net/config/api_config";
|
||||
import { authRefreshInterceptor } from "./interceptor/auth_refresh_interceptor";
|
||||
import { loggingInterceptor } from "./interceptor/logging_interceptor";
|
||||
import { tokenInterceptor } from "./interceptor/token_interceptor";
|
||||
import { authRefreshInterceptor } from "../../../core/net/interceptor/auth_refresh_interceptor";
|
||||
import { loggingInterceptor } from "../../../core/net/interceptor/logging_interceptor";
|
||||
import { tokenInterceptor } from "../../../core/net/interceptor/token_interceptor";
|
||||
|
||||
/**
|
||||
* 合并多个同类型 hook 为单个 hook(按顺序执行)
|
||||
|
||||
@@ -10,6 +10,6 @@ export * from "./http_client";
|
||||
export * from "./metrics_api";
|
||||
export * from "./response_helper";
|
||||
export * from "./user_api";
|
||||
export * from "./interceptor/auth_refresh_interceptor";
|
||||
export * from "./interceptor/logging_interceptor";
|
||||
export * from "./interceptor/token_interceptor";
|
||||
export * from "../../../core/net/interceptor/auth_refresh_interceptor";
|
||||
export * from "../../../core/net/interceptor/logging_interceptor";
|
||||
export * from "../../../core/net/interceptor/token_interceptor";
|
||||
|
||||
@@ -1,186 +0,0 @@
|
||||
/**
|
||||
* 认证刷新拦截器
|
||||
*
|
||||
* 处理 401 错误,自动刷新 token 并重试原请求。
|
||||
* 原始 Dart: lib/core/net/interceptor/auth_interceptor.dart
|
||||
*/
|
||||
import type { FetchHook } from "ofetch";
|
||||
import { ApiPath } from "../api_path";
|
||||
import { ApiError, ErrorCode } from "../api_result";
|
||||
import { AuthStorage } from "@/data/storage/auth/auth_storage";
|
||||
import { deviceIdentifier } from "@/utils/device_identifier";
|
||||
import { Result } from "@/utils/result";
|
||||
|
||||
/**
|
||||
* 不需要 auth token 刷新的路径
|
||||
* (这些路径返回 401 时不触发刷新)
|
||||
*/
|
||||
const NO_AUTH_REFRESH_PATHS: readonly string[] = [
|
||||
ApiPath.sendCode,
|
||||
ApiPath.emailLogin,
|
||||
ApiPath.register,
|
||||
ApiPath.guestLogin,
|
||||
ApiPath.refresh,
|
||||
];
|
||||
|
||||
/**
|
||||
* 是否正在刷新 token(防止并发刷新)
|
||||
*/
|
||||
let isRefreshing = false;
|
||||
|
||||
/**
|
||||
* 等待中的刷新 Promise
|
||||
*/
|
||||
let refreshPromise: Promise<void> | null = null;
|
||||
|
||||
/**
|
||||
* 获取当前可用的 auth token(登录 token 优先,其次游客 token)
|
||||
*/
|
||||
async function getAuthTokenAsync(): Promise<string | null> {
|
||||
const storage = AuthStorage.getInstance();
|
||||
const loginResult = await storage.getLoginToken();
|
||||
if (Result.isOk(loginResult) && loginResult.data && loginResult.data.length > 0) {
|
||||
return loginResult.data;
|
||||
}
|
||||
const guestResult = await storage.getGuestToken();
|
||||
if (Result.isOk(guestResult) && guestResult.data && guestResult.data.length > 0) {
|
||||
return guestResult.data;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 实际刷新 token(被拦截器调用)
|
||||
*/
|
||||
async function refreshToken(
|
||||
isGuestMode: boolean,
|
||||
baseUrl: string
|
||||
): Promise<void> {
|
||||
if (isGuestMode) {
|
||||
await refreshGuestToken(baseUrl);
|
||||
} else {
|
||||
await refreshLoginToken(baseUrl);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新登录 token
|
||||
*/
|
||||
async function refreshLoginToken(baseUrl: string): Promise<void> {
|
||||
const storage = AuthStorage.getInstance();
|
||||
const refreshResult = await storage.getRefreshToken();
|
||||
if (!Result.isOk(refreshResult) || !refreshResult.data) {
|
||||
throw new Error("No refresh token available");
|
||||
}
|
||||
|
||||
const response = await fetch(`${baseUrl}${ApiPath.refresh}`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ refreshToken: refreshResult.data }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Refresh failed: ${response.status}`);
|
||||
}
|
||||
|
||||
const json = (await response.json()) as {
|
||||
success?: boolean;
|
||||
data?: { token?: string; refreshToken?: string };
|
||||
};
|
||||
|
||||
if (json.success && json.data) {
|
||||
if (json.data.token) await storage.setLoginToken(json.data.token);
|
||||
if (json.data.refreshToken) await storage.setRefreshToken(json.data.refreshToken);
|
||||
} else {
|
||||
throw new Error("Refresh response invalid");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新游客 token
|
||||
*/
|
||||
async function refreshGuestToken(baseUrl: string): Promise<void> {
|
||||
const storage = AuthStorage.getInstance();
|
||||
const deviceId = await deviceIdentifier.getDeviceId();
|
||||
|
||||
const response = await fetch(`${baseUrl}${ApiPath.guestLogin}`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ deviceId }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Guest refresh failed: ${response.status}`);
|
||||
}
|
||||
|
||||
const json = (await response.json()) as {
|
||||
success?: boolean;
|
||||
data?: { token?: string; deviceId?: string };
|
||||
};
|
||||
|
||||
if (json.success && json.data) {
|
||||
if (json.data.token) await storage.setGuestToken(json.data.token);
|
||||
if (json.data.deviceId) await storage.setDeviceId(json.data.deviceId);
|
||||
} else {
|
||||
throw new Error("Guest refresh response invalid");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 401 → 刷新 token 并重试
|
||||
*/
|
||||
export const onResponseErrorAuthRefresh: FetchHook = async (ctx) => {
|
||||
const status = ctx.response?.status;
|
||||
|
||||
// 只处理 401
|
||||
if (status !== ErrorCode.httpUnauthorized) return;
|
||||
|
||||
// 跳过不需要刷新的路径
|
||||
const requestUrl =
|
||||
ctx.request instanceof Request ? ctx.request.url : ctx.request;
|
||||
const path = new URL(requestUrl).pathname;
|
||||
if (NO_AUTH_REFRESH_PATHS.some((p) => path.includes(p))) return;
|
||||
|
||||
// 已登录则刷新登录 token,否则刷新游客 token
|
||||
const isGuestMode = !AuthStorage.getInstance().hasLoginToken();
|
||||
|
||||
// 并发刷新:等待或启动
|
||||
if (isRefreshing && refreshPromise) {
|
||||
await refreshPromise;
|
||||
} else {
|
||||
isRefreshing = true;
|
||||
const baseUrl = new URL(requestUrl).origin;
|
||||
refreshPromise = (async () => {
|
||||
try {
|
||||
await refreshToken(isGuestMode, baseUrl);
|
||||
} finally {
|
||||
isRefreshing = false;
|
||||
refreshPromise = null;
|
||||
}
|
||||
})();
|
||||
await refreshPromise;
|
||||
}
|
||||
|
||||
// 刷新失败 → 清除数据并抛出
|
||||
const newToken = await getAuthTokenAsync();
|
||||
if (!newToken) {
|
||||
AuthStorage.getInstance().clearAuthData();
|
||||
throw new ApiError(
|
||||
ErrorCode.unknownError,
|
||||
"Token refresh failed",
|
||||
status
|
||||
);
|
||||
}
|
||||
|
||||
// 刷新成功:更新请求头的 token
|
||||
if (ctx.request instanceof Request) {
|
||||
ctx.request.headers.set("Authorization", `Bearer ${newToken}`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 兼容旧接口的 FetchHooks 形式
|
||||
*/
|
||||
export const authRefreshInterceptor = {
|
||||
onResponseError: onResponseErrorAuthRefresh,
|
||||
};
|
||||
@@ -1,85 +0,0 @@
|
||||
/**
|
||||
* 日志拦截器
|
||||
*
|
||||
* 在开发模式下记录请求/响应/错误日志。
|
||||
* 原始 Dart: lib/core/net/interceptor/app_interceptor.dart 中的日志部分。
|
||||
*/
|
||||
import type { FetchHook } from "ofetch";
|
||||
|
||||
const TAG = "[API]";
|
||||
|
||||
function isDev(): boolean {
|
||||
return process.env.NODE_ENV !== "production";
|
||||
}
|
||||
|
||||
function getRequestUrl(request: Request | string): string {
|
||||
return typeof request === "string" ? request : request.url;
|
||||
}
|
||||
|
||||
function getRequestMethod(
|
||||
request: Request | string,
|
||||
options: { method?: string }
|
||||
): string {
|
||||
return request instanceof Request ? request.method : options.method ?? "GET";
|
||||
}
|
||||
|
||||
function formatBody(body: unknown): string {
|
||||
if (body === undefined || body === null) return "";
|
||||
try {
|
||||
if (body instanceof FormData) {
|
||||
const entries: string[] = [];
|
||||
body.forEach((value, key) => {
|
||||
if (value instanceof File) {
|
||||
entries.push(`${key}=File(${value.name})`);
|
||||
} else {
|
||||
entries.push(`${key}=${String(value)}`);
|
||||
}
|
||||
});
|
||||
return `FormData{${entries.join(", ")}}`;
|
||||
}
|
||||
return JSON.stringify(body);
|
||||
} catch {
|
||||
return String(body);
|
||||
}
|
||||
}
|
||||
|
||||
export const onRequestLogging: FetchHook = async (ctx) => {
|
||||
if (!isDev()) return;
|
||||
const method = getRequestMethod(ctx.request, ctx.options);
|
||||
const url = getRequestUrl(ctx.request);
|
||||
console.log(
|
||||
`${TAG} → ${method} ${url}`,
|
||||
ctx.options.body !== undefined ? formatBody(ctx.options.body) : ""
|
||||
);
|
||||
};
|
||||
|
||||
export const onResponseLogging: FetchHook = async (ctx) => {
|
||||
if (!isDev() || !ctx.response) return;
|
||||
const method = getRequestMethod(ctx.request, ctx.options);
|
||||
const url = getRequestUrl(ctx.request);
|
||||
console.log(
|
||||
`${TAG} ← ${ctx.response.status} ${method} ${url}`,
|
||||
formatBody(ctx.response._data)
|
||||
);
|
||||
};
|
||||
|
||||
export const onErrorLogging: FetchHook = async (ctx) => {
|
||||
if (!isDev()) return;
|
||||
const status = ctx.response?.status ?? "N/A";
|
||||
const method = getRequestMethod(ctx.request, ctx.options);
|
||||
const url = getRequestUrl(ctx.request);
|
||||
console.error(
|
||||
`${TAG} ✕ ${status} ${method} ${url}`,
|
||||
ctx.error?.message ?? "Unknown error"
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 兼容旧接口的 FetchHooks 形式(保留以备兼容)
|
||||
*/
|
||||
export const loggingInterceptor = {
|
||||
onRequest: onRequestLogging,
|
||||
onResponse: onResponseLogging,
|
||||
onResponseError: onErrorLogging,
|
||||
onRequestError: onErrorLogging,
|
||||
};
|
||||
@@ -1,74 +0,0 @@
|
||||
/**
|
||||
* Token 拦截器
|
||||
*
|
||||
* 在请求头中注入 Authorization: Bearer <token>。
|
||||
* 跳过不需要 token 的路径(登录、注册、发送验证码、刷新等)。
|
||||
* 原始 Dart: lib/core/net/interceptor/token_interceptor.dart + token_paths.dart
|
||||
*/
|
||||
import type { FetchHook } from "ofetch";
|
||||
import { ApiPath } from "../api_path";
|
||||
import { AuthStorage } from "../../../storage/auth/auth_storage";
|
||||
|
||||
/**
|
||||
* 不需要 token 的路径
|
||||
*/
|
||||
const NO_TOKEN_PATHS: readonly string[] = [
|
||||
ApiPath.sendCode,
|
||||
ApiPath.emailLogin,
|
||||
ApiPath.register,
|
||||
ApiPath.guestLogin,
|
||||
];
|
||||
|
||||
/**
|
||||
* 获取请求路径
|
||||
*/
|
||||
function getRequestPath(request: Request | string): string {
|
||||
if (typeof request === "string") {
|
||||
try {
|
||||
return new URL(request).pathname;
|
||||
} catch {
|
||||
return request;
|
||||
}
|
||||
}
|
||||
return new URL(request.url).pathname;
|
||||
}
|
||||
|
||||
/**
|
||||
* 路径是否需要 token
|
||||
*/
|
||||
function needsToken(path: string): boolean {
|
||||
return !NO_TOKEN_PATHS.some((noTokenPath) => path.includes(noTokenPath));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前可用的 auth token(登录 token 优先,其次游客 token)
|
||||
*/
|
||||
async function getAuthToken(): Promise<string | null> {
|
||||
const storage = AuthStorage.getInstance();
|
||||
const loginR = await storage.getLoginToken();
|
||||
if (loginR.success && loginR.data && loginR.data.length > 0) {
|
||||
return loginR.data;
|
||||
}
|
||||
const guestR = await storage.getGuestToken();
|
||||
if (guestR.success && guestR.data && guestR.data.length > 0) {
|
||||
return guestR.data;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export const onRequestToken: FetchHook = async (ctx) => {
|
||||
const path = getRequestPath(ctx.request);
|
||||
if (!needsToken(path)) return;
|
||||
|
||||
const token = await getAuthToken();
|
||||
if (token && ctx.request instanceof Request) {
|
||||
ctx.request.headers.set("Authorization", `Bearer ${token}`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 兼容旧接口的 FetchHooks 形式
|
||||
*/
|
||||
export const tokenInterceptor = {
|
||||
onRequest: onRequestToken,
|
||||
};
|
||||
Reference in New Issue
Block a user