fix(auth): handle logout and refresh results

Propagate storage Result failures through logout, guest restoration, token refresh, and user state actors. Clear invalid sessions after failed automatic refreshes and cover both business and guest flows.
This commit is contained in:
2026-07-13 17:23:54 +08:00
parent eba01fe637
commit 225d232763
5 changed files with 388 additions and 89 deletions
@@ -32,19 +32,17 @@ let isRefreshing = false;
let refreshPromise: Promise<void> | null = null;
/**
* 取当前可用的 auth token(登录 token 优先,其次游客 token
* 取当前刷新模式对应的 token,不跨登录态回退。
*/
async function getAuthTokenAsync(): Promise<string | null> {
async function getAuthTokenAsync(isGuestMode: boolean): 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;
const tokenResult = isGuestMode
? await storage.getGuestToken()
: await storage.getLoginToken();
if (Result.isErr(tokenResult)) throw tokenResult.error;
return tokenResult.data && tokenResult.data.length > 0
? tokenResult.data
: null;
}
/**
@@ -67,7 +65,8 @@ async function refreshToken(
async function refreshLoginToken(baseUrl: string): Promise<void> {
const storage = AuthStorage.getInstance();
const refreshResult = await storage.getRefreshToken();
if (!Result.isOk(refreshResult) || !refreshResult.data) {
if (Result.isErr(refreshResult)) throw refreshResult.error;
if (!refreshResult.data) {
throw new ApiError(
"HTTP_UNAUTHORIZED",
"No refresh token available",
@@ -95,12 +94,18 @@ async function refreshLoginToken(baseUrl: string): Promise<void> {
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 {
if (!json.success || !json.data?.token) {
throw new ApiError(ErrorCode.parseError, "Refresh response invalid");
}
const tokenResult = await storage.setLoginToken(json.data.token);
if (Result.isErr(tokenResult)) throw tokenResult.error;
if (json.data.refreshToken) {
const refreshTokenResult = await storage.setRefreshToken(
json.data.refreshToken,
);
if (Result.isErr(refreshTokenResult)) throw refreshTokenResult.error;
}
}
/**
@@ -130,12 +135,16 @@ async function refreshGuestToken(baseUrl: string): Promise<void> {
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 {
if (!json.success || !json.data?.token) {
throw new ApiError(ErrorCode.parseError, "Guest refresh response invalid");
}
const tokenResult = await storage.setGuestToken(json.data.token);
if (Result.isErr(tokenResult)) throw tokenResult.error;
if (json.data.deviceId) {
const deviceResult = await storage.setDeviceId(json.data.deviceId);
if (Result.isErr(deviceResult)) throw deviceResult.error;
}
}
function apiCodeForStatus(status: number): string {
@@ -179,41 +188,45 @@ export const onResponseErrorAuthRefresh: FetchHook = async (ctx) => {
// 已登录则刷新登录 token,否则刷新游客 token
const hasLoginTokenResult = await AuthStorage.getInstance().hasLoginToken();
const isGuestMode =
!Result.isOk(hasLoginTokenResult) || !hasLoginTokenResult.data;
if (Result.isErr(hasLoginTokenResult)) throw hasLoginTokenResult.error;
const isGuestMode = !hasLoginTokenResult.data;
// 并发刷新:等待或启动
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;
try {
// 并发刷新:等待或启动
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(isGuestMode);
if (!newToken) {
throw new ApiError(
ErrorCode.unknownError,
"Token refresh failed",
status,
);
}
// 刷新成功:更新当前 optionsretry 时 tokenInterceptor 也会重新读取最新 token。
const headers = new Headers(ctx.options.headers);
headers.set("Authorization", `Bearer ${newToken}`);
ctx.options.headers = headers;
} catch (error) {
const clearResult = await AuthStorage.getInstance().clearAuthData();
if (Result.isErr(clearResult)) throw clearResult.error;
throw error;
}
// 刷新失败 → 清除数据并抛出
const newToken = await getAuthTokenAsync();
if (!newToken) {
AuthStorage.getInstance().clearAuthData();
throw new ApiError(
ErrorCode.unknownError,
"Token refresh failed",
status
);
}
// 刷新成功:更新当前 optionsretry 时 tokenInterceptor 也会重新读取最新 token。
const headers = new Headers(ctx.options.headers);
headers.set("Authorization", `Bearer ${newToken}`);
ctx.options.headers = headers;
};
/**