Add session management features: implement user session revocation on password change, enhance session validation, and update UI for password change functionality
This commit is contained in:
@ -4,6 +4,95 @@ function getToken() {
|
||||
return localStorage.getItem('qdb_token');
|
||||
}
|
||||
|
||||
const WEBSITE_ROLES = ['admin', 'supervisor'];
|
||||
|
||||
let sessionCheckPromise = null;
|
||||
let sessionCheckToken = null;
|
||||
|
||||
/** Clear cached session validation (after logout or token change). */
|
||||
export function invalidateSessionCheck() {
|
||||
sessionCheckPromise = null;
|
||||
sessionCheckToken = null;
|
||||
}
|
||||
|
||||
/** Clear stored session and open the login page. */
|
||||
export function redirectToLogin() {
|
||||
invalidateSessionCheck();
|
||||
localStorage.removeItem('qdb_token');
|
||||
localStorage.removeItem('qdb_user');
|
||||
localStorage.removeItem('qdb_role');
|
||||
window.location.hash = '#/login';
|
||||
}
|
||||
|
||||
/** @returns {boolean} true if the response means the session is no longer valid */
|
||||
function isSessionInvalidResponse(res, json) {
|
||||
if (res.status === 401) return true;
|
||||
const errMsg = json.error?.message || json.error || '';
|
||||
return res.status === 403 && (
|
||||
errMsg === 'Invalid or expired token'
|
||||
|| errMsg === 'Missing Bearer token'
|
||||
|| errMsg === 'Password change required before access'
|
||||
|| json.error?.code === 'UNAUTHORIZED'
|
||||
|| json.error?.code === 'PASSWORD_CHANGE_REQUIRED'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the stored token is still valid on the server. Redirects to login when not.
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
export async function ensureValidSession() {
|
||||
const token = getToken();
|
||||
if (!token) {
|
||||
redirectToLogin();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sessionCheckPromise && sessionCheckToken === token) {
|
||||
return sessionCheckPromise;
|
||||
}
|
||||
|
||||
sessionCheckToken = token;
|
||||
sessionCheckPromise = (async () => {
|
||||
try {
|
||||
const data = await apiGet('session');
|
||||
if (!data.success || !data.valid) {
|
||||
redirectToLogin();
|
||||
return false;
|
||||
}
|
||||
if (data.mustChangePassword) {
|
||||
redirectToLogin();
|
||||
return false;
|
||||
}
|
||||
if (!WEBSITE_ROLES.includes(data.role)) {
|
||||
redirectToLogin();
|
||||
return false;
|
||||
}
|
||||
if (data.user) localStorage.setItem('qdb_user', data.user);
|
||||
if (data.role) localStorage.setItem('qdb_role', data.role);
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (e.message !== 'Session expired') {
|
||||
redirectToLogin();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
})();
|
||||
|
||||
return sessionCheckPromise;
|
||||
}
|
||||
|
||||
async function handleAuthResponse(res) {
|
||||
if (res.status !== 401 && res.status !== 403) return;
|
||||
const json = await res.json().catch(() => ({}));
|
||||
if (isSessionInvalidResponse(res, json)) {
|
||||
redirectToLogin();
|
||||
throw new Error('Session expired');
|
||||
}
|
||||
const errMsg = json.error?.message || json.error || '';
|
||||
throw new Error(errMsg || 'Permission denied');
|
||||
}
|
||||
|
||||
async function apiFetch(endpoint, options = {}) {
|
||||
const token = getToken();
|
||||
const headers = options.headers || {};
|
||||
@ -18,16 +107,7 @@ async function apiFetch(endpoint, options = {}) {
|
||||
const res = await fetch(`${API_BASE}/${cleanEndpoint}`, { ...options, headers });
|
||||
|
||||
if (res.status === 401 || res.status === 403) {
|
||||
const json = await res.json().catch(() => ({}));
|
||||
const errMsg = json.error?.message || json.error || '';
|
||||
if (res.status === 401 || errMsg === 'Invalid or expired token') {
|
||||
localStorage.removeItem('qdb_token');
|
||||
localStorage.removeItem('qdb_user');
|
||||
localStorage.removeItem('qdb_role');
|
||||
window.location.hash = '#/login';
|
||||
throw new Error('Session expired');
|
||||
}
|
||||
throw new Error(errMsg || 'Permission denied');
|
||||
await handleAuthResponse(res);
|
||||
}
|
||||
|
||||
return res;
|
||||
@ -98,5 +178,13 @@ export async function apiDownloadFetch(url, options = {}) {
|
||||
const token = getToken();
|
||||
const headers = { ...(options.headers || {}), 'X-QDB-Client': 'web' };
|
||||
if (token) headers['Authorization'] = `Bearer ${token}`;
|
||||
return fetch(url, { ...options, headers });
|
||||
const res = await fetch(url, { ...options, headers });
|
||||
const ct = res.headers.get('Content-Type') || '';
|
||||
if (ct.includes('application/json')) {
|
||||
await handleAuthResponse(res);
|
||||
} else if (res.status === 401 || res.status === 403) {
|
||||
redirectToLogin();
|
||||
throw new Error('Session expired');
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user