228 lines
6.1 KiB
JavaScript
228 lines
6.1 KiB
JavaScript
// public/assets/auth.js
|
|
/* English comments: auth token helpers and login UI */
|
|
|
|
(function () {
|
|
const LS_REFRESH = 'scmedia_refresh_token';
|
|
const LS_CLIENT = 'scmedia_client_type';
|
|
const ACCESS_COOKIE = 'scmedia_access_token';
|
|
let refreshPromise = null;
|
|
let refreshTimer = null;
|
|
|
|
function getAccessToken() {
|
|
const parts = document.cookie.split(';').map((p) => p.trim());
|
|
for (const part of parts) {
|
|
if (!part) continue;
|
|
const idx = part.indexOf('=');
|
|
if (idx === -1) continue;
|
|
const key = part.slice(0, idx);
|
|
if (key === ACCESS_COOKIE) {
|
|
return decodeURIComponent(part.slice(idx + 1));
|
|
}
|
|
}
|
|
return '';
|
|
}
|
|
|
|
function getRefreshToken() {
|
|
return localStorage.getItem(LS_REFRESH) || sessionStorage.getItem(LS_REFRESH) || '';
|
|
}
|
|
|
|
function getClientType() {
|
|
return localStorage.getItem(LS_CLIENT) || sessionStorage.getItem(LS_CLIENT) || 'web';
|
|
}
|
|
|
|
function setTokens(tokens, clientType = 'web', remember = true) {
|
|
const storage = remember ? localStorage : sessionStorage;
|
|
if (tokens?.refresh_token) storage.setItem(LS_REFRESH, tokens.refresh_token);
|
|
storage.setItem(LS_CLIENT, clientType);
|
|
const other = remember ? sessionStorage : localStorage;
|
|
other.removeItem(LS_REFRESH);
|
|
other.removeItem(LS_CLIENT);
|
|
localStorage.removeItem('scmedia_access_token');
|
|
sessionStorage.removeItem('scmedia_access_token');
|
|
}
|
|
|
|
function clearTokens() {
|
|
localStorage.removeItem(LS_REFRESH);
|
|
localStorage.removeItem(LS_CLIENT);
|
|
sessionStorage.removeItem(LS_REFRESH);
|
|
sessionStorage.removeItem(LS_CLIENT);
|
|
localStorage.removeItem('scmedia_access_token');
|
|
sessionStorage.removeItem('scmedia_access_token');
|
|
}
|
|
|
|
function parseJwt(token) {
|
|
if (!token) return null;
|
|
const parts = token.split('.');
|
|
if (parts.length !== 3) return null;
|
|
try {
|
|
const payload = JSON.parse(atob(parts[1].replace(/-/g, '+').replace(/_/g, '/')));
|
|
return payload || null;
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function getRoles() {
|
|
const payload = parseJwt(getAccessToken());
|
|
return Array.isArray(payload?.roles) ? payload.roles : [];
|
|
}
|
|
|
|
function isAccessExpired(leewaySeconds = 30) {
|
|
const payload = parseJwt(getAccessToken());
|
|
const exp = Number(payload?.exp || 0);
|
|
if (!exp) return true;
|
|
const now = Math.floor(Date.now() / 1000);
|
|
return exp <= (now + leewaySeconds);
|
|
}
|
|
|
|
function isAdmin() {
|
|
return getRoles().includes('admin');
|
|
}
|
|
|
|
function redirectToLogin() {
|
|
if (window.location.pathname !== '/login') {
|
|
window.location.href = '/login';
|
|
}
|
|
}
|
|
|
|
function requireAuth() {
|
|
const access = getAccessToken();
|
|
const refresh = getRefreshToken();
|
|
if (!access && !refresh) {
|
|
redirectToLogin();
|
|
return;
|
|
}
|
|
if (!access && refresh) {
|
|
refreshTokens().then((ok) => {
|
|
if (!ok) redirectToLogin();
|
|
});
|
|
}
|
|
}
|
|
|
|
async function refreshTokens() {
|
|
if (refreshPromise) return refreshPromise;
|
|
const refresh = getRefreshToken();
|
|
if (!refresh) return false;
|
|
const clientType = getClientType();
|
|
refreshPromise = (async () => {
|
|
const tryRefresh = async (token) => {
|
|
const res = await fetch('/api/auth/refresh', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ refresh_token: token, client_type: clientType }),
|
|
});
|
|
if (!res.ok) return null;
|
|
const data = await res.json();
|
|
if (!data?.ok) return null;
|
|
return data?.data || {};
|
|
};
|
|
|
|
let data = await tryRefresh(refresh);
|
|
if (!data) {
|
|
const current = getRefreshToken();
|
|
if (current && current !== refresh) {
|
|
data = await tryRefresh(current);
|
|
}
|
|
}
|
|
if (!data) {
|
|
clearTokens();
|
|
return false;
|
|
}
|
|
setTokens(data, clientType);
|
|
return true;
|
|
})();
|
|
try {
|
|
return await refreshPromise;
|
|
} finally {
|
|
refreshPromise = null;
|
|
}
|
|
}
|
|
|
|
async function logout() {
|
|
const refresh = getRefreshToken();
|
|
if (window.UI?.stopSseClient) {
|
|
window.UI.stopSseClient();
|
|
}
|
|
clearTokens();
|
|
try {
|
|
await fetch('/api/auth/logout', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(refresh ? { refresh_token: refresh } : {}),
|
|
});
|
|
} catch (e) {
|
|
// Ignore logout network errors.
|
|
}
|
|
redirectToLogin();
|
|
}
|
|
|
|
function initSessionRefresh() {
|
|
if (refreshTimer) {
|
|
clearTimeout(refreshTimer);
|
|
refreshTimer = null;
|
|
}
|
|
const token = getAccessToken();
|
|
const payload = parseJwt(token);
|
|
const exp = Number(payload?.exp || 0);
|
|
if (!exp) return;
|
|
const now = Math.floor(Date.now() / 1000);
|
|
const lead = 45;
|
|
const delayMs = Math.max(1000, (exp - now - lead) * 1000);
|
|
refreshTimer = setTimeout(async () => {
|
|
const ok = await refreshTokens();
|
|
if (ok) {
|
|
initSessionRefresh();
|
|
return;
|
|
}
|
|
clearTokens();
|
|
redirectToLogin();
|
|
}, delayMs);
|
|
}
|
|
|
|
function initHeaderControls() {
|
|
const logoutBtn = document.getElementById('btnLogout');
|
|
if (logoutBtn) {
|
|
logoutBtn.addEventListener('click', () => {
|
|
logout();
|
|
});
|
|
}
|
|
const adminBtn = document.getElementById('btnAdmin');
|
|
if (adminBtn) {
|
|
adminBtn.classList.toggle('is-hidden', !isAdmin());
|
|
}
|
|
loadHeaderAvatar().catch(() => {});
|
|
}
|
|
|
|
async function loadHeaderAvatar() {
|
|
const img = document.getElementById('headerAvatar');
|
|
if (!img) return;
|
|
const token = getAccessToken();
|
|
if (!token) return;
|
|
const res = await fetch('/api/account/avatar', {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
});
|
|
if (!res.ok) return;
|
|
const blob = await res.blob();
|
|
const url = URL.createObjectURL(blob);
|
|
img.onload = () => URL.revokeObjectURL(url);
|
|
img.src = url;
|
|
}
|
|
|
|
window.Auth = {
|
|
getAccessToken,
|
|
getRefreshToken,
|
|
setTokens,
|
|
clearTokens,
|
|
parseJwt,
|
|
getRoles,
|
|
isAdmin,
|
|
isAccessExpired,
|
|
requireAuth,
|
|
refreshTokens,
|
|
logout,
|
|
redirectToLogin,
|
|
initHeaderControls,
|
|
initSessionRefresh,
|
|
};
|
|
})();
|