56993e4f98
- 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>
105 lines
2.8 KiB
JavaScript
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(); }
|