Files
Nexus/web/app/api.js
T
Your Name 56993e4f98 fix: auth logout, Nginx configs, missing dependency, CORS
- auth: logout endpoint now accepts refresh_token (was admin_id which
  frontend can't know) — added logout_by_token() to AuthService
- api.js: sendBeacon uses Blob with application/json content-type
  (was sending text/plain which FastAPI couldn't parse)
- Nginx: fixed SPA fallback to 404 (not SPA), added install.php
  PHP-FPM handler, added production HTTPS config for api.synaglobal.vip
- requirements: added cryptography==44.0.0 (used by crypto.py but
  was missing from requirements.txt)
- models: removed unused Fernet import from domain models
- CORS: added http://api.synaglobal.vip origin

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 00:30:28 +08:00

105 lines
2.8 KiB
JavaScript

// Nexus — Shared API auth module (JWT Bearer + auto-refresh)
// Include before page-specific scripts: <script src="/app/api.js"></script>
const API = window.location.origin + '/api';
function _getTokens() {
return {
access: localStorage.getItem('access_token') || '',
refresh: localStorage.getItem('refresh_token') || '',
expires: parseInt(localStorage.getItem('token_expires') || '0'),
};
}
function _isTokenExpiring() {
// Refresh if token expires within 2 minutes
return Date.now() > _getTokens().expires - 120000;
}
async function _refreshTokens() {
const { refresh } = _getTokens();
if (!refresh) return false;
try {
const res = await fetch(API + '/auth/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refresh_token: refresh }),
});
if (!res.ok) return false;
const data = await res.json();
if (!data.success) return false;
localStorage.setItem('access_token', data.access_token);
localStorage.setItem('refresh_token', data.refresh_token || refresh);
localStorage.setItem('token_expires', String(Date.now() + (data.expires_in || 1800) * 1000));
return true;
} catch {
return false;
}
}
function apiHeaders() {
const { access } = _getTokens();
return { 'Authorization': 'Bearer ' + access };
}
function apiHeadersJSON() {
return { ...apiHeaders(), 'Content-Type': 'application/json' };
}
async function apiFetch(url, options = {}) {
// Auto-refresh if token is about to expire
if (_isTokenExpiring()) {
const refreshed = await _refreshTokens();
if (!refreshed) {
_logout();
return null;
}
}
// Add auth headers
options.headers = { ...apiHeaders(), ...(options.headers || {}) };
const res = await fetch(url, options);
// If 401, try refresh once
if (res.status === 401) {
const refreshed = await _refreshTokens();
if (refreshed) {
options.headers = { ...apiHeaders(), ...(options.headers || {}) };
return fetch(url, options);
}
_logout();
return null;
}
return res;
}
function _logout() {
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
localStorage.removeItem('token_expires');
localStorage.removeItem('admin');
window.location.href = '/app/login.html';
}
function doLogout() {
// Notify backend (best-effort, don't block)
const { access, refresh } = _getTokens();
if (access) {
try {
const blob = new Blob(
[JSON.stringify({ refresh_token: refresh })],
{ type: 'application/json' }
);
navigator.sendBeacon(API + '/auth/logout', blob);
} catch {}
}
_logout();
}
// Backward-compatible alias
const token = localStorage.getItem('access_token') || '';
if (!token) window.location.href = '/app/login.html';
function ah() { return apiHeaders(); }