pediatric-ai-scribe-v3/public/js/auth.js
ifedan-ed 04252c3015 v3.0.0: Auth, admin panel, security fixes, per-tab model selector
- Add authMiddleware to all AI/transcribe routes (were unauthenticated)
- Add full admin panel: user management, registration toggle, stats
- Fix XSS in email verification (escape user.name in HTML)
- Fix missing APP_URL fallback in password reset email
- Add per-tab model selector (respects OpenRouter/Bedrock/Azure lists)
- Fix transcribeAudio to send Authorization header
- Fix labs input: textarea instead of single-line input
- Add structured logging: audit_log, api_log, access_log tables
- Add admin CLI (admin-cli.js) for Docker exec management
- Fix duplicate var duration declaration in ai.js catch block
- Fix RETURNING check case-sensitivity in database.js
2026-03-21 19:25:51 -04:00

404 lines
13 KiB
JavaScript

// ============================================================
// AUTH.JS — Login, Register, 2FA, Session Management
// ============================================================
// Wait for DOM to be fully ready
document.addEventListener('DOMContentLoaded', function() {
var authScreen = document.getElementById('auth-screen');
var mainApp = document.getElementById('main-app');
var loginForm = document.getElementById('login-form');
var registerForm = document.getElementById('register-form');
var forgotForm = document.getElementById('forgot-form');
var userName = document.getElementById('user-name');
var TOKEN_KEY = 'ped_scribe_token';
var USER_KEY = 'ped_scribe_user';
console.log('[Auth] Initializing...');
console.log('[Auth] authScreen:', !!authScreen);
console.log('[Auth] mainApp:', !!mainApp);
console.log('[Auth] loginForm:', !!loginForm);
console.log('[Auth] registerForm:', !!registerForm);
// Make sure forms are visible/hidden correctly on load
showLoginForm();
// Check for saved session
var savedToken = localStorage.getItem(TOKEN_KEY);
if (savedToken) {
console.log('[Auth] Found saved token, verifying...');
fetch('/api/auth/me', {
headers: { 'Authorization': 'Bearer ' + savedToken }
})
.then(function(r) {
if (r.ok) return r.json();
throw new Error('expired');
})
.then(function(data) {
if (data && data.user) {
console.log('[Auth] Token valid, logging in as:', data.user.email);
enterApp(data.user, savedToken);
} else {
console.log('[Auth] Token invalid');
clearSession();
}
})
.catch(function() {
console.log('[Auth] Token expired or invalid');
clearSession();
});
}
// ---- HELPER FUNCTIONS ----
function showLoginForm() {
if (loginForm) loginForm.style.display = 'block';
if (registerForm) registerForm.style.display = 'none';
if (forgotForm) forgotForm.style.display = 'none';
}
function showRegisterForm() {
if (loginForm) loginForm.style.display = 'none';
if (registerForm) registerForm.style.display = 'block';
if (forgotForm) forgotForm.style.display = 'none';
}
function showForgotForm() {
if (loginForm) loginForm.style.display = 'none';
if (registerForm) registerForm.style.display = 'none';
if (forgotForm) forgotForm.style.display = 'block';
}
function enterApp(user, token) {
window.AUTH_TOKEN = token;
localStorage.setItem(TOKEN_KEY, token);
localStorage.setItem(USER_KEY, JSON.stringify(user));
if (authScreen) authScreen.style.display = 'none';
if (mainApp) {
mainApp.style.display = 'block';
mainApp.className = mainApp.className.replace('hidden', '').trim();
}
if (userName) userName.textContent = user.name || user.email;
// Show admin tab for admins
var adminTabBtn = document.getElementById('admin-tab-btn');
if (adminTabBtn) {
if (user.role === 'admin') {
adminTabBtn.classList.remove('hidden');
} else {
adminTabBtn.classList.add('hidden');
}
}
console.log('[Auth] ✅ Entered app as:', user.email, 'Role:', user.role);
}
function exitApp() {
clearSession();
if (authScreen) authScreen.style.display = 'flex';
if (mainApp) mainApp.style.display = 'none';
var adminTabBtn = document.getElementById('admin-tab-btn');
if (adminTabBtn) adminTabBtn.classList.add('hidden');
showLoginForm();
}
function clearSession() {
localStorage.removeItem(TOKEN_KEY);
localStorage.removeItem(USER_KEY);
window.AUTH_TOKEN = null;
}
// Global auth header helper
window.getAuthHeaders = function() {
return {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + (window.AUTH_TOKEN || localStorage.getItem(TOKEN_KEY) || '')
};
};
// ---- FORM TOGGLE LINKS ----
document.addEventListener('click', function(e) {
var target = e.target;
// Handle link clicks by ID
if (target.id === 'show-register' || target.closest('#show-register')) {
e.preventDefault();
showRegisterForm();
}
if (target.id === 'show-login' || target.closest('#show-login')) {
e.preventDefault();
showLoginForm();
}
if (target.id === 'show-login-2' || target.closest('#show-login-2')) {
e.preventDefault();
showLoginForm();
}
if (target.id === 'show-forgot' || target.closest('#show-forgot')) {
e.preventDefault();
showForgotForm();
}
// Logout button
if (target.id === 'btn-logout' || target.closest('#btn-logout')) {
e.preventDefault();
exitApp();
showToast('Logged out', 'info');
}
// Settings button
if (target.id === 'btn-settings' || target.closest('#btn-settings')) {
e.preventDefault();
var modal = document.getElementById('settings-modal');
if (modal) {
modal.style.display = 'flex';
modal.className = modal.className.replace('hidden', '').trim();
load2FAStatus();
if (typeof loadNextcloudStatus === 'function') loadNextcloudStatus();
}
}
// Close settings
if (target.id === 'close-settings' || target.closest('#close-settings')) {
e.preventDefault();
var modal = document.getElementById('settings-modal');
if (modal) modal.style.display = 'none';
}
// Setup 2FA
if (target.id === 'btn-setup-2fa' || target.closest('#btn-setup-2fa')) {
e.preventDefault();
fetch('/api/auth/setup-2fa', { method: 'POST', headers: getAuthHeaders() })
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.success) {
var qr = document.getElementById('2fa-qr');
var secret = document.getElementById('2fa-secret');
var setup = document.getElementById('2fa-setup');
if (qr) qr.src = data.qrCode;
if (secret) secret.textContent = data.secret;
if (setup) { setup.style.display = 'block'; setup.className = setup.className.replace('hidden', '').trim(); }
}
});
}
// Verify 2FA
if (target.id === 'btn-verify-2fa' || target.closest('#btn-verify-2fa')) {
e.preventDefault();
var code = document.getElementById('2fa-verify-code').value;
fetch('/api/auth/verify-2fa', {
method: 'POST', headers: getAuthHeaders(),
body: JSON.stringify({ code: code })
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.success) {
showToast('2FA enabled!', 'success');
var setup = document.getElementById('2fa-setup');
if (setup) setup.style.display = 'none';
load2FAStatus();
} else {
showToast(data.error || 'Invalid code', 'error');
}
});
}
// Disable 2FA
if (target.id === 'btn-disable-2fa' || target.closest('#btn-disable-2fa')) {
e.preventDefault();
var pw = prompt('Enter password to disable 2FA:');
if (!pw) return;
fetch('/api/auth/disable-2fa', {
method: 'POST', headers: getAuthHeaders(),
body: JSON.stringify({ password: pw })
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.success) { showToast('2FA disabled', 'info'); load2FAStatus(); }
else showToast(data.error || 'Failed', 'error');
});
}
});
// ---- LOGIN FORM SUBMIT ----
if (loginForm) {
loginForm.addEventListener('submit', function(e) {
e.preventDefault();
e.stopPropagation();
var email = document.getElementById('login-email').value.trim();
var password = document.getElementById('login-password').value;
var totpEl = document.getElementById('login-totp');
var totpCode = totpEl ? totpEl.value.trim() : '';
if (!email || !password) {
showToast('Enter email and password', 'error');
return false;
}
console.log('[Auth] Attempting login for:', email);
showLoading('Signing in...');
var body = { email: email, password: password };
if (totpCode) body.totpCode = totpCode;
fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
})
.then(function(r) { return r.json(); })
.then(function(data) {
hideLoading();
console.log('[Auth] Login response:', JSON.stringify(data).substring(0, 200));
if (data.requires2FA) {
var grp = document.getElementById('totp-group');
if (grp) { grp.style.display = 'block'; grp.className = grp.className.replace('hidden', '').trim(); }
showToast('Enter your 2FA code', 'info');
return;
}
if (data.needsVerification) {
showToast('Verify your email first. Check inbox.', 'error');
return;
}
if (data.success && data.token && data.user) {
enterApp(data.user, data.token);
showToast('Welcome, ' + data.user.name + '!', 'success');
} else {
showToast(data.error || 'Login failed', 'error');
}
})
.catch(function(err) {
hideLoading();
console.error('[Auth] Login error:', err);
showToast('Connection error', 'error');
});
return false;
});
}
// ---- REGISTER FORM SUBMIT ----
if (registerForm) {
registerForm.addEventListener('submit', function(e) {
e.preventDefault();
e.stopPropagation();
var name = document.getElementById('reg-name').value.trim();
var email = document.getElementById('reg-email').value.trim();
var password = document.getElementById('reg-password').value;
if (!name || !email || !password) {
showToast('Fill all fields', 'error');
return false;
}
if (password.length < 8) {
showToast('Password must be 8+ characters', 'error');
return false;
}
console.log('[Auth] Registering:', email);
showLoading('Creating account...');
fetch('/api/auth/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: name, email: email, password: password })
})
.then(function(r) { return r.json(); })
.then(function(data) {
hideLoading();
console.log('[Auth] Register response:', JSON.stringify(data).substring(0, 200));
if (data.success && data.token && data.user) {
enterApp(data.user, data.token);
showToast(data.message || 'Account created!', 'success');
} else if (data.success && data.needsVerification) {
showToast(data.message || 'Check email to verify', 'success');
showLoginForm();
} else {
showToast(data.error || 'Registration failed', 'error');
}
})
.catch(function(err) {
hideLoading();
console.error('[Auth] Register error:', err);
showToast('Connection error', 'error');
});
return false;
});
}
// ---- FORGOT PASSWORD SUBMIT ----
if (forgotForm) {
forgotForm.addEventListener('submit', function(e) {
e.preventDefault();
e.stopPropagation();
var email = document.getElementById('forgot-email').value.trim();
if (!email) { showToast('Enter email', 'error'); return false; }
showLoading('Sending...');
fetch('/api/auth/forgot-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: email })
})
.then(function(r) { return r.json(); })
.then(function(data) {
hideLoading();
showToast(data.message || 'Check your email', 'success');
showLoginForm();
})
.catch(function(err) {
hideLoading();
showToast('Error', 'error');
});
return false;
});
}
// ---- 2FA STATUS ----
function load2FAStatus() {
fetch('/api/auth/me', { headers: getAuthHeaders() })
.then(function(r) { return r.json(); })
.then(function(data) {
if (!data || !data.user) return;
var status = document.getElementById('2fa-status');
var setupBtn = document.getElementById('btn-setup-2fa');
var disableBtn = document.getElementById('btn-disable-2fa');
if (data.user.totp_enabled) {
if (status) { status.textContent = 'Status: ✅ Enabled'; status.style.color = '#10b981'; }
if (setupBtn) setupBtn.style.display = 'none';
if (disableBtn) disableBtn.style.display = 'inline-flex';
} else {
if (status) { status.textContent = 'Status: ❌ Not enabled'; status.style.color = '#ef4444'; }
if (setupBtn) setupBtn.style.display = 'inline-flex';
if (disableBtn) disableBtn.style.display = 'none';
}
})
.catch(function() {});
}
// Check registration status
fetch('/api/auth/registration-status')
.then(function(r) { return r.json(); })
.then(function(data) {
if (!data.registrationEnabled) {
var link = document.getElementById('show-register');
if (link) link.style.display = 'none';
}
})
.catch(function() {});
console.log('[Auth] ✅ Module ready');
});