Files
MC_Report/public/js/api.js
canglan 188e083719 refactor: auth expiry - sync JWT exp check, remove async validation
Previous async validateAuth() approach caused: recursion loop,
wrong /me path (404 -> session cleared), init() fetching auth-required
settings without token (cleared session every load).

New synchronous approach:
- Auth.expired(): decodes JWT payload locally, checks exp timestamp
- Auth.logged(): token+user exist AND not expired; auto-clears stale
  session on expiry (no network, no async, no loops)
- route(): simple sync guard - if !Auth.logged() reset + redirect to
  #/login; home page stays public (clears stale cookie only)
- API.req: 401 response -> Auth.reset() + redirect to login unless on
  public page (fallback for server-side revocation/invalid signature)
- Removed validateAuth/_validating/_doRoute entirely
2026-08-21 21:01:30 +08:00

72 lines
2.7 KiB
JavaScript

/*
* MC Report System
* Copyright (C) 2026 Sea Network Technology Studio
* Author: CangLan <admin@sea-studio.top>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
const API = {
base: '/api',
_key: window.__API_KEY__ || '',
async req(method, path, data) {
const token = Auth.token();
const headers = {};
if (token) headers['Authorization'] = `Bearer ${token}`;
if (this._key) headers['x-api-key'] = this._key;
const opts = { method, headers };
if (data instanceof FormData) opts.body = data;
else if (data && method !== 'GET') { headers['Content-Type'] = 'application/json'; opts.body = JSON.stringify(data); }
opts.headers = headers;
const res = await fetch(this.base + path, opts);
const ct = res.headers.get('content-type');
if (ct && ct.includes('application/json')) {
const json = await res.json();
if (res.status === 401) {
// 服务端判定未登录/过期: 清除本地登录态, 非公开页跳转登录
Auth.reset();
const page = (location.hash || '#/home').replace('#/', '').split('/')[0];
if (!['home', 'login', 'register', 'forgot', 'reset', 'verify', 'install'].includes(page)) location.hash = '#/login';
}
if (!res.ok) throw new Error(json.error || '请求失败');
return json;
}
if (!res.ok) throw new Error('请求失败');
return res;
},
get(p) { return this.req('GET', p); },
post(p, d) { return this.req('POST', p, d); },
put(p, d) { return this.req('PUT', p, d); },
delete(p) { return this.req('DELETE', p); },
async download(path) {
const token = Auth.token();
const h = {};
if (token) h['Authorization'] = `Bearer ${token}`;
if (this._key) h['x-api-key'] = this._key;
const res = await fetch(this.base + path, { headers: h });
if (!res.ok) throw new Error('下载失败');
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
const disp = res.headers.get('content-disposition');
a.download = disp?.match(/filename=(.+)/)?.[1] || 'export.csv';
a.click();
URL.revokeObjectURL(url);
}
};