Files
GopherGate/static/js/auth.js
hobokenchicken 0f0486d8d4
Some checks failed
CI / Lint (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Build (push) Has been cancelled
fix: resolve user dashboard field mapping and session consistency
Added JSON tags to the User struct to match frontend expectations and excluded sensitive fields.
Updated session management to include and persist DisplayName.
Unified user field names (using display_name) across backend, sessions, and frontend UI.
2026-03-19 14:01:59 -04:00

272 lines
8.4 KiB
JavaScript

// Authentication Module for GopherGate Dashboard
class AuthManager {
constructor() {
this.isAuthenticated = false;
this.token = null;
this.user = null;
this.init();
}
init() {
// Check for existing session
const savedToken = localStorage.getItem('dashboard_token');
const savedUser = localStorage.getItem('dashboard_user');
if (savedToken && savedUser) {
this.token = savedToken;
this.user = JSON.parse(savedUser);
this.isAuthenticated = true;
this.showDashboard();
} else {
this.showLogin();
}
// Setup login form
this.setupLoginForm();
this.setupLogout();
}
setupLoginForm() {
const loginForm = document.getElementById('login-form');
if (!loginForm) return;
loginForm.addEventListener('submit', async (e) => {
e.preventDefault();
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
await this.login(username, password);
});
}
setupLogout() {
const logoutBtn = document.getElementById('logout-btn');
if (!logoutBtn) return;
logoutBtn.addEventListener('click', () => {
this.logout();
});
}
setToken(newToken) {
if (!newToken) return;
this.token = newToken;
localStorage.setItem('dashboard_token', this.token);
}
async login(username, password) {
const errorElement = document.getElementById('login-error');
const loginBtn = document.getElementById('login-btn');
try {
loginBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Authenticating...';
loginBtn.disabled = true;
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const result = await response.json();
if (result.success) {
this.token = result.data.token;
this.user = result.data.user;
localStorage.setItem('dashboard_token', this.token);
localStorage.setItem('dashboard_user', JSON.stringify(this.user));
this.isAuthenticated = true;
this.showDashboard();
this.showToast('Successfully logged in!', 'success');
} else {
throw new Error(result.error || 'Invalid credentials');
}
} catch (error) {
errorElement.style.display = 'flex';
errorElement.querySelector('span').textContent = error.message;
loginBtn.innerHTML = '<i class="fas fa-sign-in-alt"></i> Sign In';
loginBtn.disabled = false;
}
}
async logout() {
// Revoke server-side session first
try {
if (this.token) {
await fetch('/api/auth/logout', {
method: 'POST',
headers: { 'Authorization': `Bearer ${this.token}` }
});
}
} catch (e) {
// Best-effort — still clear local state even if server call fails
console.warn('Server logout failed:', e);
}
// Clear localStorage
localStorage.removeItem('dashboard_token');
localStorage.removeItem('dashboard_user');
// Reset state
this.isAuthenticated = false;
this.token = null;
this.user = null;
// Show login screen
this.showLogin();
// Show logout message
this.showToast('Successfully logged out', 'info');
}
showLogin() {
const loginScreen = document.getElementById('auth-page');
const dashboard = document.getElementById('dashboard');
if (loginScreen) loginScreen.style.display = 'flex';
if (dashboard) dashboard.style.display = 'none';
// Clear form
const loginForm = document.getElementById('login-form');
if (loginForm) loginForm.reset();
// Hide error
const errorElement = document.getElementById('login-error');
if (errorElement) errorElement.style.display = 'none';
// Reset button
const loginBtn = document.getElementById('login-btn');
if (loginBtn) {
loginBtn.innerHTML = '<i class="fas fa-sign-in-alt"></i> Sign In';
loginBtn.disabled = false;
}
}
showDashboard() {
const loginScreen = document.getElementById('auth-page');
const dashboard = document.getElementById('dashboard');
if (loginScreen) loginScreen.style.display = 'none';
if (dashboard) dashboard.style.display = 'flex';
// Update user info in sidebar
this.updateUserInfo();
// Initialize dashboard components
if (typeof window.initDashboard === 'function') {
window.initDashboard();
}
}
updateUserInfo() {
const userNameElement = document.querySelector('.user-name');
const userRoleElement = document.querySelector('.user-role');
if (userNameElement && this.user) {
userNameElement.textContent = this.user.display_name || this.user.username || 'User';
}
if (userRoleElement && this.user) {
const roleLabels = { admin: 'Administrator', viewer: 'Viewer' };
userRoleElement.textContent = roleLabels[this.user.role] || this.user.role || 'User';
}
}
getAuthHeaders() {
if (!this.token) return {};
return {
'Authorization': `Bearer ${this.token}`,
'Content-Type': 'application/json'
};
}
async fetchWithAuth(url, options = {}) {
const headers = this.getAuthHeaders();
const response = await fetch(url, {
...options,
headers: {
...headers,
...options.headers
}
});
if (response.status === 401) {
// Token expired or invalid
this.logout();
throw new Error('Authentication required');
}
return response;
}
showToast(message, type = 'info') {
// Create toast container if it doesn't exist
let container = document.querySelector('.toast-container');
if (!container) {
container = document.createElement('div');
container.className = 'toast-container';
document.body.appendChild(container);
}
// Create toast
const toast = document.createElement('div');
toast.className = `toast ${type}`;
// Set icon based on type
let icon = 'info-circle';
switch (type) {
case 'success':
icon = 'check-circle';
break;
case 'error':
icon = 'exclamation-circle';
break;
case 'warning':
icon = 'exclamation-triangle';
break;
}
toast.innerHTML = `
<i class="fas fa-${icon} toast-icon"></i>
<div class="toast-content">
<div class="toast-title">${type.charAt(0).toUpperCase() + type.slice(1)}</div>
<div class="toast-message">${message}</div>
</div>
<button class="toast-close">
<i class="fas fa-times"></i>
</button>
`;
// Add close functionality
const closeBtn = toast.querySelector('.toast-close');
closeBtn.addEventListener('click', () => {
toast.remove();
});
// Add to container
container.appendChild(toast);
// Auto-remove after 5 seconds
setTimeout(() => {
if (toast.parentNode) {
toast.remove();
}
}, 5000);
}
}
// Initialize auth manager when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
window.authManager = new AuthManager();
});
// Export for use in other modules
if (typeof module !== 'undefined' && module.exports) {
module.exports = AuthManager;
}