Enforce trading authorization and suitability checks

This commit is contained in:
张胜宇
2026-09-13 19:24:59 +08:00
parent b9aafce120
commit e3c316aafb
7 changed files with 149 additions and 28 deletions
+64 -4
View File
@@ -4,7 +4,9 @@ const SESSION_KEY = 'portalAuthContext';
const IDENTITY_COOKIE = 'portal_auth_user';
const CONTEXT_COOKIE = 'portal_auth_context';
const AUTH_CHANNEL_NAME = 'portal-auth-session';
const AUTH_EVENT_TYPES = new Set(['signed-in', 'signed-out', 'account-switched']);
const AUTH_EVENT_TYPES = new Set(['signed-in', 'signed-out', 'account-switched', 'session-expired']);
const SESSION_IDLE_TIMEOUT_MS = 30 * 60 * 1000;
const SESSION_CHECK_INTERVAL_MS = 15 * 1000;
let authChannel = null;
let authSyncStarted = false;
@@ -38,13 +40,59 @@ function writeSharedContext(context, maxAge) {
}
function hasMatchingIdentity(token, context) {
const expiresAt = tokenExpiry(token);
return Boolean(
token
&& context
&& readCookie(IDENTITY_COOKIE) === String(context.userId),
&& readCookie(IDENTITY_COOKIE) === String(context.userId)
&& (!expiresAt || Date.now() < expiresAt),
);
}
function tokenExpiry(token) {
try {
const payload = token.split('.')[1];
if (!payload) return 0;
const normalized = payload.replace(/-/g, '+').replace(/_/g, '/').padEnd(Math.ceil(payload.length / 4) * 4, '=');
const value = JSON.parse(decodeURIComponent(atob(normalized).split('').map((char) => `%${`00${char.charCodeAt(0).toString(16)}`.slice(-2)}`).join('')));
return Number(value.exp || 0) * 1000;
} catch {
return 0;
}
}
function sessionExpired() {
clearAuthSession({ eventType: 'session-expired' });
if (isProtectedPortalPage()) {
window.location.replace(`${loginPathForCurrentPortal()}?reason=session-expired`);
}
}
function checkSessionLifetime() {
const token = readCookie('auth_token');
const context = readSharedContext() || readSessionContext();
if (!token || !context || !hasMatchingIdentity(token, context)) return;
const expiresAt = Number(context.expiresAt || tokenExpiry(token));
const lastActivityAt = Number(context.lastActivityAt || Date.now());
if ((expiresAt && Date.now() >= expiresAt) || Date.now() - lastActivityAt >= SESSION_IDLE_TIMEOUT_MS) {
sessionExpired();
}
}
function touchActivity() {
const token = readCookie('auth_token');
const context = readSharedContext() || readSessionContext();
if (!token || !context || !hasMatchingIdentity(token, context)) return;
const now = Date.now();
const expiresAt = Number(context.expiresAt || tokenExpiry(token));
if (expiresAt && now >= expiresAt) { sessionExpired(); return; }
if (now - Number(context.lastActivityAt || 0) < 60_000) return;
const next = { ...context, lastActivityAt: now, expiresAt };
const remaining = expiresAt ? Math.max(0, Math.ceil((expiresAt - now) / 1000)) : 1800;
sessionStorage.setItem(SESSION_KEY, JSON.stringify(next));
writeSharedContext(next, remaining);
}
function openAuthChannel() {
if (!('BroadcastChannel' in window)) return null;
if (authChannel) return authChannel;
@@ -80,7 +128,8 @@ function redirectForExternalAuthChange(type) {
window.location.replace('/portal/guest/home/?reason=signed-out');
return;
}
window.location.replace(`${loginPathForCurrentPortal()}?reason=account-switched`);
const reason = type === 'session-expired' ? 'session-expired' : 'account-switched';
window.location.replace(`${loginPathForCurrentPortal()}?reason=${reason}`);
}
function handleExternalAuthChange(type) {
@@ -108,6 +157,11 @@ export function startAuthSync() {
});
window.addEventListener('focus', reconcileSharedSession);
window.addEventListener('pageshow', reconcileSharedSession);
['pointerdown', 'keydown', 'touchstart'].forEach((eventName) => {
window.addEventListener(eventName, touchActivity, { passive: true });
});
window.setInterval(checkSessionLifetime, SESSION_CHECK_INTERVAL_MS);
checkSessionLifetime();
}
export function getAccessToken() {
@@ -130,6 +184,8 @@ export function setAuthSession(loginData, username) {
roles: Array.isArray(loginData.roles) ? loginData.roles : [],
dataScope: loginData.data_scope || 'self',
permissions: Array.isArray(loginData.permissions) ? loginData.permissions : [],
expiresAt: Date.now() + maxAge * 1000,
lastActivityAt: Date.now(),
};
document.cookie = `${IDENTITY_COOKIE}=${encodeURIComponent(context.userId)}; path=/; max-age=${maxAge}; SameSite=Strict`;
writeSharedContext(context, maxAge);
@@ -190,8 +246,12 @@ export function updateAuthPermissions(permissions, dataScope) {
permissions: Array.isArray(permissions) ? [...permissions] : [],
dataScope: dataScope || context.dataScope,
};
const expiresAt = Number(next.expiresAt || tokenExpiry(readCookie('auth_token')));
const remaining = expiresAt ? Math.max(0, Math.ceil((expiresAt - Date.now()) / 1000)) : 1800;
next.expiresAt = expiresAt;
next.lastActivityAt = Date.now();
sessionStorage.setItem(SESSION_KEY, JSON.stringify(next));
writeSharedContext(next, 1800);
writeSharedContext(next, remaining);
return next;
}