178 lines
5.9 KiB
TypeScript
178 lines
5.9 KiB
TypeScript
export type Portal = 'admin' | 'client';
|
|
|
|
export type SessionUser = {
|
|
id: string;
|
|
tenantId?: string | null;
|
|
tenantName?: string | null;
|
|
username: string;
|
|
email?: string | null;
|
|
phone?: string | null;
|
|
displayName: string;
|
|
roles: string[];
|
|
};
|
|
|
|
export type LoginSession = {
|
|
portal: Portal;
|
|
user: SessionUser;
|
|
idleTimeoutSeconds: number;
|
|
lockRecoverySeconds: number;
|
|
absoluteExpiresAt: string;
|
|
lastActivityAt: string;
|
|
recentAuthenticationExpiresAt: string;
|
|
locked?: boolean;
|
|
};
|
|
|
|
export type SessionRecovery = {
|
|
returnUrl: string;
|
|
code?: string;
|
|
message?: string;
|
|
};
|
|
|
|
const legacySessionKey = 'cmpp-auth-session';
|
|
const sessionKey = (portal: Portal) => `cmpp-auth-session:${portal}`;
|
|
const recoveryKey = (portal: Portal) => `cmpp-session-recovery:${portal}`;
|
|
const sessionEventName = (portal: Portal, type: 'locked' | 'unlocked' | 'logout') => `cmpp-session-${portal}-${type}`;
|
|
|
|
export function portalFromPath(path: string): Portal | undefined {
|
|
if (/^\/?(?:api\/)?admin(?:\/|$)/.test(path)) return 'admin';
|
|
if (/^\/?(?:api\/)?client(?:\/|$)/.test(path)) return 'client';
|
|
return undefined;
|
|
}
|
|
|
|
export function readSession(portal: Portal): LoginSession | null {
|
|
try {
|
|
const raw = window.localStorage.getItem(sessionKey(portal));
|
|
if (raw) return JSON.parse(raw) as LoginSession;
|
|
|
|
// One-time migration for sessions created before portal storage was isolated.
|
|
const legacyRaw = window.localStorage.getItem(legacySessionKey);
|
|
if (!legacyRaw) return null;
|
|
const legacy = JSON.parse(legacyRaw) as LoginSession;
|
|
if (legacy.portal !== portal) return null;
|
|
window.localStorage.setItem(sessionKey(portal), legacyRaw);
|
|
window.localStorage.removeItem(legacySessionKey);
|
|
return legacy;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function writeSession(session: LoginSession) {
|
|
window.localStorage.setItem(sessionKey(session.portal), JSON.stringify(session));
|
|
window.localStorage.removeItem(legacySessionKey);
|
|
}
|
|
|
|
export function clearSession(portal: Portal) {
|
|
window.localStorage.removeItem(sessionKey(portal));
|
|
try {
|
|
const legacyRaw = window.localStorage.getItem(legacySessionKey);
|
|
if (legacyRaw && (JSON.parse(legacyRaw) as LoginSession).portal === portal) {
|
|
window.localStorage.removeItem(legacySessionKey);
|
|
}
|
|
} catch {
|
|
window.localStorage.removeItem(legacySessionKey);
|
|
}
|
|
}
|
|
|
|
export function updateSessionTiming(portal: Portal, timing: Partial<Omit<LoginSession, 'portal' | 'user'>>) {
|
|
const current = readSession(portal);
|
|
if (current) writeSession({ ...current, ...timing });
|
|
}
|
|
|
|
export function safeReturnUrl(portal: Portal, value: string) {
|
|
const normalized = value.trim();
|
|
if (!normalized.startsWith(`/${portal}`) || normalized.startsWith(`/${portal}/login`)) return undefined;
|
|
if (normalized.startsWith('//') || normalized.includes('\\')) return undefined;
|
|
try {
|
|
const parsed = new URL(normalized, window.location.origin);
|
|
if (parsed.origin !== window.location.origin || !parsed.pathname.startsWith(`/${portal}`)) return undefined;
|
|
return `${parsed.pathname}${parsed.search}${parsed.hash}`;
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
export function saveSessionRecovery(portal: Portal, recovery: SessionRecovery) {
|
|
const returnUrl = safeReturnUrl(portal, recovery.returnUrl);
|
|
if (!returnUrl) return;
|
|
window.sessionStorage.setItem(recoveryKey(portal), JSON.stringify({ ...recovery, returnUrl }));
|
|
}
|
|
|
|
export function readSessionRecovery(portal: Portal): SessionRecovery | null {
|
|
try {
|
|
const raw = window.sessionStorage.getItem(recoveryKey(portal));
|
|
if (!raw) return null;
|
|
const recovery = JSON.parse(raw) as SessionRecovery;
|
|
const returnUrl = safeReturnUrl(portal, recovery.returnUrl);
|
|
return returnUrl ? { ...recovery, returnUrl } : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function consumeSessionRecovery(portal: Portal) {
|
|
const recovery = readSessionRecovery(portal);
|
|
window.sessionStorage.removeItem(recoveryKey(portal));
|
|
return recovery;
|
|
}
|
|
|
|
export function clearSessionRecovery(portal: Portal) {
|
|
window.sessionStorage.removeItem(recoveryKey(portal));
|
|
}
|
|
|
|
export function currentRouteForPortal(portal: Portal) {
|
|
const hashRoute = window.location.hash.startsWith('#/') ? window.location.hash.slice(1) : '';
|
|
return safeReturnUrl(portal, hashRoute) ?? safeReturnUrl(portal, `${window.location.pathname}${window.location.search}${window.location.hash}`) ?? `/${portal}`;
|
|
}
|
|
|
|
export function redirectToPortalLogin(portal: Portal) {
|
|
window.location.assign(`${window.location.origin}/#/${portal}/login`);
|
|
}
|
|
|
|
let lastUserActivityAt = Date.now();
|
|
let reauthenticationHandler: (() => Promise<void>) | undefined;
|
|
|
|
export function markUserActivity() {
|
|
lastUserActivityAt = Date.now();
|
|
}
|
|
|
|
export function getLastUserActivityAt() {
|
|
return lastUserActivityAt;
|
|
}
|
|
|
|
export function hasRecentUserActivity() {
|
|
return Date.now() - lastUserActivityAt < 60_000;
|
|
}
|
|
|
|
export function setReauthenticationHandler(handler?: () => Promise<void>) {
|
|
reauthenticationHandler = handler;
|
|
}
|
|
|
|
export function requestReauthentication() {
|
|
if (!reauthenticationHandler) return Promise.reject(new Error('请重新验证当前密码后再操作'));
|
|
return reauthenticationHandler();
|
|
}
|
|
|
|
export function dispatchSessionEvent(portal: Portal, type: 'locked' | 'unlocked' | 'logout', detail?: Record<string, unknown>) {
|
|
window.dispatchEvent(new CustomEvent(sessionEventName(portal, type), { detail }));
|
|
try {
|
|
const channel = new BroadcastChannel(`cmpp-session:${portal}`);
|
|
channel.postMessage({ type, detail });
|
|
channel.close();
|
|
} catch {
|
|
// BroadcastChannel is an enhancement; the current tab still receives the DOM event.
|
|
}
|
|
}
|
|
|
|
export function sessionEvent(portal: Portal, type: 'locked' | 'unlocked' | 'logout') {
|
|
return sessionEventName(portal, type);
|
|
}
|
|
|
|
export function sessionChannel(portal: Portal) {
|
|
return `cmpp-session:${portal}`;
|
|
}
|
|
|
|
export function getSessionTenantId() {
|
|
return readSession('client')?.user.tenantId ?? undefined;
|
|
}
|