pediatric-ai-scribe-v2/public/js/auth.js
ifedan-ed 1c2098d1dd v2.0.0: Pediatric AI Scribe
Features:
- Live encounter recording → HPI (outpatient/inpatient)
- Voice dictation → HPI / SOAP note
- Hospital course generator (prose/day-by-day/organ system/psych)
- Chart review / precharting (outpatient/subspecialty/ED)
- SOAP note generator (full/subjective only)
- Developmental milestones (AAP/Nelson) with narrative + 3-sentence summary
- AI refine & shorten for all outputs
- Ask AI what's missing (clarification)
- Authentication (email/password, email verification, 2FA)
- Nextcloud integration with auto date folders
- PWA support (installable on phone)
- 18+ AI models via OpenRouter
- HIPAA compliance guidance
- Docker support
2026-03-21 16:55:50 -04:00

294 lines
9.9 KiB
JavaScript

// ============================================================
// AUTH.JS — Login, Register, 2FA, Session Management
// ============================================================
(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';
// Check existing session
var savedToken = localStorage.getItem(TOKEN_KEY);
var savedUser = localStorage.getItem(USER_KEY);
if (savedToken && savedUser) {
try {
var user = JSON.parse(savedUser);
showApp(user, savedToken);
} catch(e) {
localStorage.removeItem(TOKEN_KEY);
localStorage.removeItem(USER_KEY);
}
}
// Form toggles
document.getElementById('show-register').addEventListener('click', function(e) {
e.preventDefault();
loginForm.classList.add('hidden');
registerForm.classList.remove('hidden');
forgotForm.classList.add('hidden');
});
document.getElementById('show-login').addEventListener('click', function(e) {
e.preventDefault();
loginForm.classList.remove('hidden');
registerForm.classList.add('hidden');
forgotForm.classList.add('hidden');
});
document.getElementById('show-login-2').addEventListener('click', function(e) {
e.preventDefault();
loginForm.classList.remove('hidden');
registerForm.classList.add('hidden');
forgotForm.classList.add('hidden');
});
document.getElementById('show-forgot').addEventListener('click', function(e) {
e.preventDefault();
loginForm.classList.add('hidden');
registerForm.classList.add('hidden');
forgotForm.classList.remove('hidden');
});
// Login
loginForm.addEventListener('submit', function(e) {
e.preventDefault();
var email = document.getElementById('login-email').value;
var password = document.getElementById('login-password').value;
var totpCode = document.getElementById('login-totp').value;
showLoading('Signing in...');
fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: email, password: password, totpCode: totpCode || undefined })
})
.then(function(r) { return r.json(); })
.then(function(data) {
hideLoading();
if (data.requires2FA) {
document.getElementById('totp-group').classList.remove('hidden');
showToast('Enter your 2FA code', 'info');
return;
}
if (data.success) {
localStorage.setItem(TOKEN_KEY, data.token);
localStorage.setItem(USER_KEY, JSON.stringify(data.user));
showApp(data.user, data.token);
showToast('Welcome back, ' + data.user.name + '!', 'success');
} else {
showToast(data.error || 'Login failed', 'error');
}
})
// Handle email verification needed
if (data.needsVerification) {
showToast(data.message || 'Please verify your email first', 'error');
// Show resend option
var loginLinks = loginForm.querySelector('.auth-links');
if (!document.getElementById('resend-verify-link')) {
var resendLink = document.createElement('a');
resendLink.href = '#';
resendLink.id = 'resend-verify-link';
resendLink.textContent = 'Resend verification email';
resendLink.addEventListener('click', function(e) {
e.preventDefault();
var email = document.getElementById('login-email').value;
if (!email) { showToast('Enter your email first', 'error'); return; }
fetch('/api/auth/resend-verification', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: email })
})
.then(function(r) { return r.json(); })
.then(function(d) { showToast(d.message || 'Verification email sent!', 'success'); });
});
loginLinks.appendChild(resendLink);
}
return;
}
.catch(function(err) {
hideLoading();
showToast('Error: ' + err.message, 'error');
});
});
// Register
registerForm.addEventListener('submit', function(e) {
e.preventDefault();
var name = document.getElementById('reg-name').value;
var email = document.getElementById('reg-email').value;
var password = document.getElementById('reg-password').value;
if (password.length < 8) {
showToast('Password must be at least 8 characters', 'error');
return;
}
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();
if (data.success) {
localStorage.setItem(TOKEN_KEY, data.token);
localStorage.setItem(USER_KEY, JSON.stringify(data.user));
showApp(data.user, data.token);
showToast('Account created! Welcome, ' + data.user.name, 'success');
} else {
showToast(data.error || 'Registration failed', 'error');
}
})
.catch(function(err) {
hideLoading();
showToast('Error: ' + err.message, 'error');
});
});
// Forgot password
forgotForm.addEventListener('submit', function(e) {
e.preventDefault();
var email = document.getElementById('forgot-email').value;
showLoading('Sending reset link...');
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 || 'If account exists, reset email sent', 'success');
loginForm.classList.remove('hidden');
forgotForm.classList.add('hidden');
})
.catch(function(err) {
hideLoading();
showToast('Error: ' + err.message, 'error');
});
});
// Logout
document.getElementById('btn-logout').addEventListener('click', function() {
localStorage.removeItem(TOKEN_KEY);
localStorage.removeItem(USER_KEY);
authScreen.classList.remove('hidden');
mainApp.classList.add('hidden');
showToast('Logged out', 'info');
});
function showApp(user, token) {
authScreen.classList.add('hidden');
mainApp.classList.remove('hidden');
if (userName) userName.textContent = user.name || user.email;
window.AUTH_TOKEN = token;
}
// Helper: add auth header to all API calls
window.getAuthHeaders = function() {
return {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + (window.AUTH_TOKEN || localStorage.getItem(TOKEN_KEY) || '')
};
};
// Settings modal
document.getElementById('btn-settings').addEventListener('click', function() {
document.getElementById('settings-modal').classList.remove('hidden');
load2FAStatus();
loadNextcloudStatus();
});
document.getElementById('close-settings').addEventListener('click', function() {
document.getElementById('settings-modal').classList.add('hidden');
});
// 2FA Setup
function load2FAStatus() {
fetch('/api/auth/me', { headers: getAuthHeaders() })
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.user) {
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) {
status.textContent = 'Status: ✅ Enabled';
status.style.color = '#10b981';
setupBtn.classList.add('hidden');
disableBtn.classList.remove('hidden');
} else {
status.textContent = 'Status: ❌ Not enabled';
status.style.color = '#ef4444';
setupBtn.classList.remove('hidden');
disableBtn.classList.add('hidden');
}
}
});
}
document.getElementById('btn-setup-2fa').addEventListener('click', function() {
fetch('/api/auth/setup-2fa', { method: 'POST', headers: getAuthHeaders() })
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.success) {
document.getElementById('2fa-qr').src = data.qrCode;
document.getElementById('2fa-secret').textContent = data.secret;
document.getElementById('2fa-setup').classList.remove('hidden');
}
});
});
document.getElementById('btn-verify-2fa').addEventListener('click', function() {
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');
document.getElementById('2fa-setup').classList.add('hidden');
load2FAStatus();
} else {
showToast(data.error || 'Invalid code', 'error');
}
});
});
document.getElementById('btn-disable-2fa').addEventListener('click', function() {
var pw = prompt('Enter your 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');
}
});
});
console.log('✅ Auth module loaded');
})();