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
294 lines
9.9 KiB
JavaScript
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');
|
|
})();
|