优化后台界面与无操作退出体验
This commit is contained in:
@@ -2,22 +2,20 @@ import router from "../router";
|
||||
import { useAuthStore } from "../store/auth";
|
||||
import { useSessionStore } from "../store/session";
|
||||
import { getToken, setToken, clearToken } from "../utils/auth";
|
||||
import { extendToken, getLoginKey, unlockSession } from "../api/authClient";
|
||||
import { encryptLoginPayload } from "../utils/loginCrypto";
|
||||
import { extendToken } from "../api/authClient";
|
||||
import { parseJwtExp } from "./jwt";
|
||||
|
||||
export const IDLE_TIMEOUT_MINUTES = 30;
|
||||
export const AUTO_LOGOUT_TIMEOUT_HOURS = 2;
|
||||
export const TIMEOUT_WARNING_SECONDS = 60;
|
||||
export const EXTEND_EARLY_SECONDS = 120;
|
||||
export const EXTEND_MIN_INTERVAL_SECONDS = 60;
|
||||
export const UNLOCK_MAX_ATTEMPTS = 5;
|
||||
export const LOGOUT_REASON_TIMEOUT = "timeout";
|
||||
export const LOGOUT_REASON_MANUAL = "manual";
|
||||
export const LOGOUT_REASON_AUTH_EXPIRED = "auth_expired";
|
||||
const LOGOUT_REASON_STORAGE_KEY = "ctms_logout_reason";
|
||||
|
||||
type BroadcastMessage =
|
||||
| { type: "ACTIVE"; at: number }
|
||||
| { type: "NETWORK"; at: number }
|
||||
| { type: "LOCK"; reason: string; email?: string; deadlineAt?: number }
|
||||
| { type: "TOKEN_UPDATED"; token: string }
|
||||
| { type: "LOGOUT"; reason?: string };
|
||||
|
||||
@@ -26,23 +24,15 @@ let extendPromise: Promise<{ token: string | null; authFailed: boolean }> | null
|
||||
let initialized = false;
|
||||
const channel = typeof BroadcastChannel !== "undefined" ? new BroadcastChannel("ctms-auth") : null;
|
||||
|
||||
const getLastActiveAt = (session: ReturnType<typeof useSessionStore>) =>
|
||||
Math.max(session.lastUserActiveAt, session.lastNetworkActiveAt);
|
||||
const getTimeoutAt = (session: ReturnType<typeof useSessionStore>) =>
|
||||
session.lastUserActiveAt + IDLE_TIMEOUT_MINUTES * 60 * 1000;
|
||||
|
||||
const reconcileSessionState = (now: number = Date.now()) => {
|
||||
const session = useSessionStore();
|
||||
if (session.locked) return false;
|
||||
const idleTimeoutMs = IDLE_TIMEOUT_MINUTES * 60 * 1000;
|
||||
const autoLogoutMs = AUTO_LOGOUT_TIMEOUT_HOURS * 60 * 60 * 1000;
|
||||
const lastActiveAt = getLastActiveAt(session);
|
||||
if (now - lastActiveAt > autoLogoutMs) {
|
||||
if (now >= getTimeoutAt(session)) {
|
||||
forceLogout(LOGOUT_REASON_TIMEOUT);
|
||||
return false;
|
||||
}
|
||||
if (now - lastActiveAt > idleTimeoutMs) {
|
||||
lockSession("idle");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -61,24 +51,13 @@ const handleBroadcast = (message: BroadcastMessage) => {
|
||||
const session = useSessionStore();
|
||||
const auth = useAuthStore();
|
||||
if (message.type === "ACTIVE") {
|
||||
if (session.locked) return;
|
||||
session.recordUserActivity(message.at);
|
||||
scheduleIdleCheck();
|
||||
}
|
||||
if (message.type === "NETWORK") {
|
||||
if (session.locked) return;
|
||||
session.recordNetworkActivity(message.at);
|
||||
scheduleIdleCheck();
|
||||
}
|
||||
if (message.type === "LOCK") {
|
||||
const reason = message.reason as "idle" | "extend_failed" | "token_invalid";
|
||||
session.lock(reason || "idle", message.email, message.deadlineAt);
|
||||
scheduleIdleCheck();
|
||||
}
|
||||
if (message.type === "TOKEN_UPDATED") {
|
||||
auth.setToken(message.token);
|
||||
setToken(message.token);
|
||||
session.unlock();
|
||||
session.resetActivity();
|
||||
}
|
||||
if (message.type === "LOGOUT") {
|
||||
performLogout(message.reason);
|
||||
@@ -91,39 +70,22 @@ const scheduleIdleCheck = () => {
|
||||
}
|
||||
const session = useSessionStore();
|
||||
const now = Date.now();
|
||||
if (session.locked) {
|
||||
const remain = (session.lockAutoLogoutDeadlineAt || 0) - now;
|
||||
if (remain <= 0) {
|
||||
checkIdle();
|
||||
return;
|
||||
}
|
||||
idleTimer = window.setTimeout(checkIdle, remain);
|
||||
return;
|
||||
}
|
||||
const idleTimeoutMs = IDLE_TIMEOUT_MINUTES * 60 * 1000;
|
||||
const autoLogoutMs = AUTO_LOGOUT_TIMEOUT_HOURS * 60 * 60 * 1000;
|
||||
const lastActiveAt = getLastActiveAt(session);
|
||||
const idleRemain = lastActiveAt + idleTimeoutMs - now;
|
||||
const logoutRemain = lastActiveAt + autoLogoutMs - now;
|
||||
const nextCheck = Math.min(idleRemain, logoutRemain);
|
||||
if (nextCheck <= 0) {
|
||||
const timeoutAt = getTimeoutAt(session);
|
||||
const warningAt = timeoutAt - TIMEOUT_WARNING_SECONDS * 1000;
|
||||
if (timeoutAt <= now) {
|
||||
checkIdle();
|
||||
return;
|
||||
}
|
||||
idleTimer = window.setTimeout(checkIdle, nextCheck);
|
||||
if (warningAt <= now) {
|
||||
session.showTimeoutWarning(timeoutAt);
|
||||
idleTimer = window.setTimeout(checkIdle, timeoutAt - now);
|
||||
return;
|
||||
}
|
||||
session.clearTimeoutWarning();
|
||||
idleTimer = window.setTimeout(checkIdle, warningAt - now);
|
||||
};
|
||||
|
||||
const checkIdle = () => {
|
||||
const session = useSessionStore();
|
||||
if (session.locked) {
|
||||
const deadlineAt = session.lockAutoLogoutDeadlineAt || 0;
|
||||
if (deadlineAt > 0 && Date.now() >= deadlineAt) {
|
||||
forceLogout(LOGOUT_REASON_TIMEOUT);
|
||||
return;
|
||||
}
|
||||
scheduleIdleCheck();
|
||||
return;
|
||||
}
|
||||
if (!reconcileSessionState(Date.now())) return;
|
||||
scheduleIdleCheck();
|
||||
};
|
||||
@@ -166,34 +128,12 @@ export const initSessionManager = () => {
|
||||
|
||||
export const markUserActive = (at: number = Date.now()) => {
|
||||
const session = useSessionStore();
|
||||
if (session.locked) return;
|
||||
if (!reconcileSessionState(at)) return;
|
||||
session.recordUserActivity(at);
|
||||
broadcast({ type: "ACTIVE", at });
|
||||
scheduleIdleCheck();
|
||||
};
|
||||
|
||||
export const markNetworkActive = (at: number = Date.now()) => {
|
||||
const session = useSessionStore();
|
||||
if (session.locked) return;
|
||||
if (!reconcileSessionState(at)) return;
|
||||
session.recordNetworkActivity(at);
|
||||
broadcast({ type: "NETWORK", at });
|
||||
scheduleIdleCheck();
|
||||
};
|
||||
|
||||
export const lockSession = (reason: "idle" | "extend_failed" | "token_invalid") => {
|
||||
const session = useSessionStore();
|
||||
const auth = useAuthStore();
|
||||
if (session.locked) return;
|
||||
const email = auth.user?.email || "";
|
||||
const autoLogoutMs = AUTO_LOGOUT_TIMEOUT_HOURS * 60 * 60 * 1000;
|
||||
const deadlineAt = getLastActiveAt(session) + autoLogoutMs;
|
||||
session.lock(reason, email, deadlineAt);
|
||||
broadcast({ type: "LOCK", reason, email, deadlineAt });
|
||||
scheduleIdleCheck();
|
||||
};
|
||||
|
||||
const setLogoutReason = (reason?: string) => {
|
||||
try {
|
||||
if (!reason) {
|
||||
@@ -221,7 +161,7 @@ const performLogout = (reason?: string) => {
|
||||
const auth = useAuthStore();
|
||||
const session = useSessionStore();
|
||||
auth.logout();
|
||||
session.unlock();
|
||||
session.resetActivity();
|
||||
clearToken();
|
||||
router.replace("/login");
|
||||
};
|
||||
@@ -259,10 +199,10 @@ export const extendAccessToken = async (reason: "early" | "response-401") => {
|
||||
} catch (err: any) {
|
||||
const status = err?.response?.status;
|
||||
if (status === 401) {
|
||||
lockSession("extend_failed");
|
||||
forceLogout(LOGOUT_REASON_AUTH_EXPIRED);
|
||||
authFailed = true;
|
||||
} else if (status === 403) {
|
||||
forceLogout();
|
||||
forceLogout(LOGOUT_REASON_AUTH_EXPIRED);
|
||||
authFailed = true;
|
||||
}
|
||||
return { token: null, authFailed };
|
||||
@@ -278,40 +218,13 @@ export const startTokenKeepAlive = () => {
|
||||
const token = getToken();
|
||||
if (!token) return;
|
||||
const session = useSessionStore();
|
||||
if (session.locked) return;
|
||||
const expAt = parseJwtExp(token);
|
||||
if (!expAt) return;
|
||||
const remaining = expAt - Date.now();
|
||||
const timeoutMs = IDLE_TIMEOUT_MINUTES * 60 * 1000;
|
||||
const active = Date.now() - getLastActiveAt(session) < timeoutMs;
|
||||
const active = Date.now() - session.lastUserActiveAt < timeoutMs;
|
||||
if (active && remaining < EXTEND_EARLY_SECONDS * 1000) {
|
||||
void extendAccessToken("early");
|
||||
}
|
||||
}, 10000);
|
||||
};
|
||||
|
||||
export const unlockWithPassword = async (email: string, password: string) => {
|
||||
const session = useSessionStore();
|
||||
const auth = useAuthStore();
|
||||
const { data: loginKey } = await getLoginKey();
|
||||
const ciphertext = await encryptLoginPayload(loginKey.public_key, {
|
||||
email,
|
||||
password,
|
||||
challenge: loginKey.challenge,
|
||||
});
|
||||
const { data } = await unlockSession({
|
||||
key_id: loginKey.key_id,
|
||||
challenge: loginKey.challenge,
|
||||
ciphertext,
|
||||
});
|
||||
updateToken(data.accessToken);
|
||||
session.unlock();
|
||||
try {
|
||||
await auth.fetchMe();
|
||||
} catch {
|
||||
// token已更新但用户信息拉取失败时,避免界面进入无角色状态
|
||||
forceLogout();
|
||||
throw new Error("无法获取用户信息,请重新登录");
|
||||
}
|
||||
markUserActive();
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user