R69 完成业务验收与会话稳定性修复
这个提交包含在:
@@ -24,6 +24,7 @@ import {
|
||||
loginWithPassword,
|
||||
logoutSession,
|
||||
refreshSession,
|
||||
resetRefreshSessionCache,
|
||||
} from "./authService";
|
||||
import { resolvePermissionsForRoles } from "./permissions";
|
||||
|
||||
@@ -246,6 +247,7 @@ export function AuthProvider({ children }: PropsWithChildren) {
|
||||
}, [clearProactiveRefreshTimer]);
|
||||
|
||||
const clearSession = useCallback(() => {
|
||||
resetRefreshSessionCache();
|
||||
clearSessionHint();
|
||||
markUnauthenticatedReady();
|
||||
void clearPwaRuntimeCaches();
|
||||
@@ -427,6 +429,7 @@ export function AuthProvider({ children }: PropsWithChildren) {
|
||||
const changePassword = useCallback(
|
||||
async (oldPassword: string, newPassword: string) => {
|
||||
const result = await changePasswordApi({ oldPassword, newPassword });
|
||||
resetRefreshSessionCache();
|
||||
const me = await fetchCurrentEmployee(result.accessToken);
|
||||
commitAuthenticatedUser(result.accessToken, me, false);
|
||||
scheduleProactiveRefresh(result.expiresIn);
|
||||
|
||||
@@ -6,7 +6,20 @@ const JSON_HEADERS = {
|
||||
"content-type": "application/json",
|
||||
};
|
||||
|
||||
const REFRESH_SUCCESS_REPLAY_MS = 1000;
|
||||
|
||||
let refreshSessionPromise: Promise<LoginResponse> | null = null;
|
||||
let refreshSessionReplay: { data: LoginResponse; expiresAt: number } | null = null;
|
||||
let refreshSessionGeneration = 0;
|
||||
|
||||
export function resetRefreshSessionCache() {
|
||||
refreshSessionGeneration += 1;
|
||||
refreshSessionPromise = null;
|
||||
refreshSessionReplay = null;
|
||||
}
|
||||
|
||||
export async function loginWithPassword(payload: LoginRequest) {
|
||||
resetRefreshSessionCache();
|
||||
return apiRequestData<LoginResponse>("/api/auth/login", {
|
||||
method: "POST",
|
||||
auth: false,
|
||||
@@ -19,7 +32,7 @@ export async function loginWithPassword(payload: LoginRequest) {
|
||||
});
|
||||
}
|
||||
|
||||
export async function refreshSession() {
|
||||
async function requestRefreshSession() {
|
||||
return apiRequestData<LoginResponse>("/api/auth/refresh", {
|
||||
method: "POST",
|
||||
auth: false,
|
||||
@@ -30,12 +43,54 @@ export async function refreshSession() {
|
||||
});
|
||||
}
|
||||
|
||||
export async function refreshSession() {
|
||||
const now = Date.now();
|
||||
if (refreshSessionReplay && refreshSessionReplay.expiresAt > now) {
|
||||
return refreshSessionReplay.data;
|
||||
}
|
||||
|
||||
if (!refreshSessionPromise) {
|
||||
const generation = refreshSessionGeneration;
|
||||
const requestPromise = requestRefreshSession()
|
||||
.then((data) => {
|
||||
if (generation === refreshSessionGeneration) {
|
||||
refreshSessionReplay = {
|
||||
data,
|
||||
expiresAt: Date.now() + REFRESH_SUCCESS_REPLAY_MS,
|
||||
};
|
||||
}
|
||||
return data;
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
if (generation === refreshSessionGeneration) {
|
||||
refreshSessionReplay = null;
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
|
||||
refreshSessionPromise = requestPromise;
|
||||
void requestPromise
|
||||
.finally(() => {
|
||||
if (refreshSessionPromise === requestPromise) {
|
||||
refreshSessionPromise = null;
|
||||
}
|
||||
})
|
||||
.catch(() => undefined);
|
||||
}
|
||||
|
||||
return refreshSessionPromise;
|
||||
}
|
||||
|
||||
export async function logoutSession() {
|
||||
return apiRequestData<Record<string, never>>("/api/auth/logout", {
|
||||
method: "POST",
|
||||
auth: false,
|
||||
retryOn401: false,
|
||||
});
|
||||
try {
|
||||
return await apiRequestData<Record<string, never>>("/api/auth/logout", {
|
||||
method: "POST",
|
||||
auth: false,
|
||||
retryOn401: false,
|
||||
});
|
||||
} finally {
|
||||
resetRefreshSessionCache();
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchCurrentEmployee(accessToken: string) {
|
||||
|
||||
在新工单中引用
屏蔽一个用户