fix: auth expiry - validate token before showing protected pages

- Add validateAuth() method: calls /api/me to verify token validity,
  clears Auth state on 401/login error
- route() now calls validateAuth() async for ALL protected pages
  BEFORE showing layout/sidebar/rendering content
- Extract _doRoute() for actual page rendering (runs after validation)
- init() uses API.get for install check (proper auth handling)
- Added reset() to Auth: clears localStorage without redirecting

This fixes: expired token showing login-required pages, and no feedback
when session expires mid-session.
This commit is contained in:
2026-08-21 20:46:09 +08:00
parent 6ac8a5cd11
commit e232f3b02e

View File

@@ -37,12 +37,16 @@ const App = {
], ],
async init() { async init() {
// 获取站点名称(无论是否安装),使用原生 fetch 避免携带 Bearer token // 获取站点名称:先尝试带 token 的请求,失败则降级(安装阶段无需登录)
try { try {
const r = await fetch('/api/install/status'); const r = await API.get('/install/status');
const s = await r.json(); if (!r.installed) { location.hash = '#/install'; return; }
if (!s.installed) { location.hash = '#/install'; return; } } catch {
} catch { location.hash = '#/install'; return; } // 未安装或 API 不可用,尝试直接路由到安装页
location.hash = '#/install';
return;
}
// 安装完成后获取站点名称(需要 token,若未登录则忽略名称设置)
try { try {
const nameRes = await fetch('/api/settings/settings'); const nameRes = await fetch('/api/settings/settings');
if (nameRes.ok) { if (nameRes.ok) {
@@ -50,7 +54,7 @@ const App = {
if (settings.site_name && settings.site_name.value) window.__SITE_NAME__ = settings.site_name.value; if (settings.site_name && settings.site_name.value) window.__SITE_NAME__ = settings.site_name.value;
} }
} catch {} } catch {}
// 确保登录态同步:若 cookie 还在但 token 已过期,清除 // 确保登录态与服务器同步:若本地有 token 但 site_name 未加载(可能 token 已过期)
if (Auth.logged() && !window.__SITE_NAME__) Auth.reset(); if (Auth.logged() && !window.__SITE_NAME__) Auth.reset();
if (window.__SITE_NAME__) { if (window.__SITE_NAME__) {
document.getElementById('site-title').textContent = window.__SITE_NAME__; document.getElementById('site-title').textContent = window.__SITE_NAME__;
@@ -91,6 +95,21 @@ const App = {
closeModal() { document.getElementById('modal-overlay').classList.add('hidden'); }, closeModal() { document.getElementById('modal-overlay').classList.add('hidden'); },
navigate(page, param) { location.hash = `#/${page}` + (param ? `/${param}` : ''); }, navigate(page, param) { location.hash = `#/${page}` + (param ? `/${param}` : ''); },
// 验证 token 有效性(轻量级,失败则清除登录态)
async validateAuth() {
if (!Auth.logged()) return false;
try {
await API.get('/me');
return true;
} catch (e) {
if (e && e.message && e.message.includes('登录')) {
Auth.reset();
return false;
}
throw e;
}
},
route() { route() {
const hash = location.hash || '#/home'; const hash = location.hash || '#/home';
const parts = hash.replace('#/', '').split('/'); const parts = hash.replace('#/', '').split('/');
@@ -105,8 +124,22 @@ const App = {
if (page === 'install') { this.showPublic('install'); return; } if (page === 'install') { this.showPublic('install'); return; }
if (page === 'home') { this.showPublic('home', param); return; } if (page === 'home') { this.showPublic('home', param); return; }
if (page === 'login' || page === 'register' || page === 'forgot' || page === 'reset') { this.showPublic(page); return; } if (page === 'login' || page === 'register' || page === 'forgot' || page === 'reset') { this.showPublic(page); return; }
if (!Auth.logged()) { location.hash = '#/login'; return; }
// 访问受保护页面前先验证 token 有效性
if (Auth.logged()) {
this.validateAuth().then(valid => {
if (!valid) { location.hash = '#/login'; return; }
this._doRoute(page, param);
}).catch(() => {
Auth.reset();
location.hash = '#/login';
});
return;
}
location.hash = '#/login';
},
_doRoute(page, param) {
this.showLayout(); this.showLayout();
const sidebarPage = (page === 'tickets' && param.startsWith('create')) ? 'tickets-create' : page; const sidebarPage = (page === 'tickets' && param.startsWith('create')) ? 'tickets-create' : page;
this.renderSidebar(sidebarPage); this.renderSidebar(sidebarPage);