新增空闲锁屏、修复401等问题,新增文件版本管理、共享库等占位符
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
import { ElMessage } from "element-plus";
|
||||
import router from "../router";
|
||||
import { useAuthStore } from "../store/auth";
|
||||
import { useSessionStore } from "../store/session";
|
||||
import { getToken, setToken, clearToken } from "../utils/auth";
|
||||
import { extendToken, unlockSession } from "../api/authClient";
|
||||
import { parseJwtExp } from "./jwt";
|
||||
|
||||
export const IDLE_TIMEOUT_MINUTES = 30;
|
||||
export const EXTEND_EARLY_SECONDS = 120;
|
||||
export const EXTEND_MIN_INTERVAL_SECONDS = 60;
|
||||
export const UNLOCK_MAX_ATTEMPTS = 5;
|
||||
|
||||
type BroadcastMessage =
|
||||
| { type: "ACTIVE"; at: number }
|
||||
| { type: "NETWORK"; at: number }
|
||||
| { type: "LOCK"; reason: string }
|
||||
| { type: "TOKEN_UPDATED"; token: string }
|
||||
| { type: "LOGOUT" };
|
||||
|
||||
let idleTimer: number | null = null;
|
||||
let extendPromise: Promise<{ token: string | null; authFailed: boolean }> | null = null;
|
||||
let initialized = false;
|
||||
const channel = typeof BroadcastChannel !== "undefined" ? new BroadcastChannel("ctms-auth") : null;
|
||||
|
||||
const broadcast = (message: BroadcastMessage) => {
|
||||
if (channel) {
|
||||
channel.postMessage(message);
|
||||
}
|
||||
try {
|
||||
localStorage.setItem("ctms_auth_broadcast", JSON.stringify({ ...message, ts: Date.now() }));
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
};
|
||||
|
||||
const handleBroadcast = (message: BroadcastMessage) => {
|
||||
const session = useSessionStore();
|
||||
const auth = useAuthStore();
|
||||
if (message.type === "ACTIVE") {
|
||||
session.recordUserActivity(message.at);
|
||||
scheduleIdleCheck();
|
||||
}
|
||||
if (message.type === "NETWORK") {
|
||||
session.recordNetworkActivity(message.at);
|
||||
scheduleIdleCheck();
|
||||
}
|
||||
if (message.type === "LOCK") {
|
||||
const reason = message.reason as "idle" | "extend_failed" | "token_invalid";
|
||||
session.lock(reason || "idle");
|
||||
}
|
||||
if (message.type === "TOKEN_UPDATED") {
|
||||
auth.setToken(message.token);
|
||||
setToken(message.token);
|
||||
session.unlock();
|
||||
}
|
||||
if (message.type === "LOGOUT") {
|
||||
forceLogout();
|
||||
}
|
||||
};
|
||||
|
||||
const scheduleIdleCheck = () => {
|
||||
if (idleTimer) {
|
||||
window.clearTimeout(idleTimer);
|
||||
}
|
||||
const session = useSessionStore();
|
||||
const timeoutMs = IDLE_TIMEOUT_MINUTES * 60 * 1000;
|
||||
const nextCheck =
|
||||
Math.min(session.lastUserActiveAt, session.lastNetworkActiveAt) + timeoutMs - Date.now();
|
||||
if (nextCheck <= 0) {
|
||||
checkIdle();
|
||||
return;
|
||||
}
|
||||
idleTimer = window.setTimeout(checkIdle, nextCheck);
|
||||
};
|
||||
|
||||
const checkIdle = () => {
|
||||
const session = useSessionStore();
|
||||
const timeoutMs = IDLE_TIMEOUT_MINUTES * 60 * 1000;
|
||||
const now = Date.now();
|
||||
if (now - session.lastUserActiveAt > timeoutMs && now - session.lastNetworkActiveAt > timeoutMs) {
|
||||
lockSession("idle");
|
||||
return;
|
||||
}
|
||||
scheduleIdleCheck();
|
||||
};
|
||||
|
||||
export const initSessionManager = () => {
|
||||
if (initialized) return;
|
||||
initialized = true;
|
||||
const events = ["mousemove", "mousedown", "keydown", "touchstart", "scroll"];
|
||||
let lastSignal = 0;
|
||||
const onUserActivity = () => {
|
||||
const now = Date.now();
|
||||
if (now - lastSignal < 1000) return;
|
||||
lastSignal = now;
|
||||
markUserActive(now);
|
||||
};
|
||||
events.forEach((event) => window.addEventListener(event, onUserActivity, { passive: true }));
|
||||
if (channel) {
|
||||
channel.onmessage = (event) => handleBroadcast(event.data as BroadcastMessage);
|
||||
}
|
||||
window.addEventListener("storage", (event) => {
|
||||
if (event.key !== "ctms_auth_broadcast" || !event.newValue) return;
|
||||
try {
|
||||
const message = JSON.parse(event.newValue) as BroadcastMessage;
|
||||
handleBroadcast(message);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
});
|
||||
scheduleIdleCheck();
|
||||
startTokenKeepAlive();
|
||||
};
|
||||
|
||||
export const markUserActive = (at: number = Date.now()) => {
|
||||
const session = useSessionStore();
|
||||
session.recordUserActivity(at);
|
||||
broadcast({ type: "ACTIVE", at });
|
||||
scheduleIdleCheck();
|
||||
};
|
||||
|
||||
export const markNetworkActive = (at: number = Date.now()) => {
|
||||
const session = useSessionStore();
|
||||
session.recordNetworkActivity(at);
|
||||
broadcast({ type: "NETWORK", at });
|
||||
scheduleIdleCheck();
|
||||
};
|
||||
|
||||
export const lockSession = (reason: "idle" | "extend_failed" | "token_invalid") => {
|
||||
const session = useSessionStore();
|
||||
if (session.locked) return;
|
||||
session.lock(reason);
|
||||
broadcast({ type: "LOCK", reason });
|
||||
};
|
||||
|
||||
export const forceLogout = () => {
|
||||
const auth = useAuthStore();
|
||||
const session = useSessionStore();
|
||||
auth.logout();
|
||||
session.unlock();
|
||||
clearToken();
|
||||
broadcast({ type: "LOGOUT" });
|
||||
router.replace("/login");
|
||||
};
|
||||
|
||||
const updateToken = (token: string) => {
|
||||
const auth = useAuthStore();
|
||||
auth.setToken(token);
|
||||
setToken(token);
|
||||
broadcast({ type: "TOKEN_UPDATED", token });
|
||||
};
|
||||
|
||||
export const extendAccessToken = async (reason: "early" | "response-401") => {
|
||||
const session = useSessionStore();
|
||||
const token = getToken();
|
||||
if (!token) return { token: null, authFailed: false };
|
||||
if (reason === "early") {
|
||||
const now = Date.now();
|
||||
if (now - session.lastExtendAt < EXTEND_MIN_INTERVAL_SECONDS * 1000) {
|
||||
return { token: null, authFailed: false };
|
||||
}
|
||||
}
|
||||
if (extendPromise) return extendPromise;
|
||||
extendPromise = (async () => {
|
||||
let authFailed = false;
|
||||
try {
|
||||
const { data } = await extendToken(token);
|
||||
session.setLastExtendAt(Date.now());
|
||||
updateToken(data.accessToken);
|
||||
return { token: data.accessToken, authFailed: false };
|
||||
} catch (err: any) {
|
||||
const status = err?.response?.status;
|
||||
if (status === 401) {
|
||||
lockSession("extend_failed");
|
||||
authFailed = true;
|
||||
} else if (status === 403) {
|
||||
forceLogout();
|
||||
authFailed = true;
|
||||
}
|
||||
return { token: null, authFailed };
|
||||
} finally {
|
||||
extendPromise = null;
|
||||
}
|
||||
})();
|
||||
return extendPromise;
|
||||
};
|
||||
|
||||
export const startTokenKeepAlive = () => {
|
||||
window.setInterval(() => {
|
||||
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() - session.lastUserActiveAt < timeoutMs || Date.now() - session.lastNetworkActiveAt < timeoutMs;
|
||||
if (active && remaining < EXTEND_EARLY_SECONDS * 1000) {
|
||||
void extendAccessToken("early");
|
||||
}
|
||||
}, 10000);
|
||||
};
|
||||
|
||||
export const unlockWithPassword = async (email: string, password: string) => {
|
||||
const session = useSessionStore();
|
||||
const { data } = await unlockSession({ email, password });
|
||||
updateToken(data.accessToken);
|
||||
session.unlock();
|
||||
markUserActive();
|
||||
};
|
||||
Reference in New Issue
Block a user