class AuthManager {
constructor() {
this.isAuthenticated = false;
this.username = null;
this._resolveLogin = null;
this._overlayEl = null;
}
async initialize() {
const status = await this._checkStatus();
if (status.authenticated) {
this.isAuthenticated = true;
this.username = status.username;
return;
}
return this._showLoginOverlay();
}
async _checkStatus() {
try {
const res = await fetch('/api/auth/status');
return await res.json();
} catch {
return { authenticated: false };
}
}
async login(username, password) {
try {
const res = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const data = await res.json();
if (res.ok && data.success) {
this.isAuthenticated = true;
this.username = data.username;
this._hideLoginOverlay();
if (this._resolveLogin) this._resolveLogin();
return { success: true };
}
return { success: false, error: data.error || 'Invalid credentials' };
} catch {
return { success: false, error: 'Connection error' };
}
}
async logout() {
try {
await fetch('/api/auth/logout', { method: 'POST' });
} catch { /* ignore */ }
window.location.reload();
}
_showLoginOverlay() {
return new Promise(resolve => {
this._resolveLogin = resolve;
const overlay = document.createElement('div');
overlay.className = 'login-overlay aurora-bg aurora-static';
overlay.innerHTML = `
`;
document.body.appendChild(overlay);
this._overlayEl = overlay;
// Focus username after animation
setTimeout(() => {
document.getElementById('login-username')?.focus();
}, 100);
document.getElementById('login-form').addEventListener('submit', async (e) => {
e.preventDefault();
const username = document.getElementById('login-username').value.trim();
const password = document.getElementById('login-password').value;
const errorEl = document.getElementById('login-error');
const btn = document.getElementById('login-submit');
errorEl.querySelector('.login-error-text').textContent = '';
errorEl.classList.remove('visible');
btn.classList.add('loading');
btn.disabled = true;
const result = await this.login(username, password);
if (!result.success) {
errorEl.querySelector('.login-error-text').textContent = result.error;
errorEl.classList.add('visible');
btn.classList.remove('loading');
btn.disabled = false;
document.getElementById('login-password').value = '';
document.getElementById('login-password').focus();
}
});
});
}
_hideLoginOverlay() {
if (!this._overlayEl) return;
this._overlayEl.classList.add('hiding');
setTimeout(() => {
this._overlayEl?.remove();
this._overlayEl = null;
}, 250);
}
interceptFetch() {
const original = window.fetch.bind(window);
const self = this;
window.fetch = async function(...args) {
// Skip auth endpoints to avoid infinite loops
const url = typeof args[0] === 'string' ? args[0] : args[0]?.url || '';
if (url.includes('/api/auth/')) return original(...args);
const response = await original(...args);
if (response.status === 401 && self.isAuthenticated) {
self.isAuthenticated = false;
await self._showLoginOverlay();
// Retry the original request after re-login
return original(...args);
}
return response;
};
}
}
window.authManager = new AuthManager();