Spaces:
Sleeping
Sleeping
| (() => { | |
| const API_BASE_URL = `https://${window.location.hostname}`; | |
| // API helper function | |
| async function api(endpoint, options = {}) { | |
| try { | |
| const url = endpoint.startsWith('http') ? endpoint : API_BASE_URL + endpoint; | |
| const response = await fetch(url, { | |
| ...options, | |
| credentials: 'include', // Include cookies | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| ...options.headers | |
| } | |
| }); | |
| const data = await response.json(); | |
| if (!response.ok) { | |
| throw new Error(data.message || 'API request failed'); | |
| } | |
| return data; | |
| } catch (err) { | |
| console.error('API Error:', err); | |
| throw err; | |
| } | |
| } | |
| // Elements | |
| const loginForm = document.getElementById('loginForm'); | |
| const signInBtn = document.getElementById('signInBtn'); | |
| const anonBtn = document.getElementById('anonBtn'); | |
| const emailInput = document.getElementById('loginEmail'); | |
| const emailHint = document.getElementById('emailHint'); | |
| const passwordInput = document.getElementById('loginPassword'); | |
| const togglePasswordBtn = document.getElementById('togglePassword'); | |
| const meter = document.getElementById('passwordMeter'); | |
| const meterBar = document.getElementById('passwordMeterBar'); | |
| const capsLockIndicator = document.getElementById('capsLockIndicator'); | |
| const rememberMe = document.getElementById('rememberMe'); | |
| const forgotLink = document.getElementById('forgotLink'); | |
| // Forgot password elements | |
| const fpModal = document.getElementById('fpModal'); | |
| const fpBackdrop = document.getElementById('fpBackdrop'); | |
| const fpClose = document.getElementById('fpClose'); | |
| const fpEmail = document.getElementById('fpEmail'); | |
| const fpRequestBtn = document.getElementById('fpRequestBtn'); | |
| const fpStep1 = document.querySelector('.fp-step-1'); | |
| const fpStep2 = document.querySelector('.fp-step-2'); | |
| const fpCode = document.getElementById('fpCode'); | |
| const fpNewPassword = document.getElementById('fpNewPassword'); | |
| const fpApplyBtn = document.getElementById('fpApplyBtn'); | |
| const fpResendBtn = document.getElementById('fpResendBtn'); | |
| const fpMessage = document.getElementById('fpMessage'); | |
| // MFA elements | |
| const mfaModal = document.getElementById('mfaModal'); | |
| const mfaBackdrop = document.getElementById('mfaBackdrop'); | |
| const mfaClose = document.getElementById('mfaClose'); | |
| const mfaCode = document.getElementById('mfaCode'); | |
| const mfaVerifyBtn = document.getElementById('mfaVerifyBtn'); | |
| const mfaResendBtn = document.getElementById('mfaResendBtn'); | |
| const mfaMessage = document.getElementById('mfaMessage'); | |
| // Show message | |
| function showMessage(text, type = 'error') { | |
| const existing = document.querySelector('.error-message, .success-message'); | |
| if (existing) existing.remove(); | |
| const message = document.createElement('div'); | |
| message.className = type === 'error' ? 'error-message' : 'success-message'; | |
| message.textContent = text; | |
| message.style.cssText = ` | |
| padding: 12px 16px; | |
| margin: 16px 0; | |
| border-radius: 6px; | |
| font-size: 14px; | |
| font-weight: 500; | |
| ${type === 'error' ? | |
| 'background: rgba(239, 68, 68, 0.1); color: #ef4444; border: 1px solid rgba(239, 68, 68, 0.2);' : | |
| 'background: rgba(16, 185, 129, 0.1); color: #10b981; border: 1px solid rgba(16, 185, 129, 0.2);' | |
| } | |
| `; | |
| loginForm.insertBefore(message, loginForm.firstChild); | |
| setTimeout(() => message.remove(), 5000); | |
| } | |
| // Redirect to main app | |
| function redirectToApp(account = null) { | |
| if (account) { | |
| localStorage.setItem('aimhsa_account', account); | |
| } | |
| window.location.href = '/index.html'; | |
| } | |
| // Utility: simple password strength score (0..4) | |
| function getPasswordStrengthScore(pw) { | |
| let score = 0; | |
| if (!pw) return 0; | |
| if (pw.length >= 8) score++; | |
| if (/[A-Z]/.test(pw)) score++; | |
| if (/[a-z]/.test(pw)) score++; | |
| if (/[0-9]/.test(pw)) score++; | |
| if (/[^A-Za-z0-9]/.test(pw)) score++; | |
| return Math.min(score, 4); | |
| } | |
| function updatePasswordMeter() { | |
| const pw = passwordInput.value; | |
| const score = getPasswordStrengthScore(pw); | |
| const pct = (score / 4) * 100; | |
| meterBar.style.width = pct + '%'; | |
| let color = '#ef4444'; | |
| if (score >= 3) color = '#f59e0b'; | |
| if (score >= 4) color = '#10b981'; | |
| meterBar.style.background = color; | |
| meter.setAttribute('aria-hidden', pw ? 'false' : 'true'); | |
| } | |
| // Toggle password visibility | |
| togglePasswordBtn?.addEventListener('click', () => { | |
| const isPassword = passwordInput.type === 'password'; | |
| passwordInput.type = isPassword ? 'text' : 'password'; | |
| togglePasswordBtn.setAttribute('aria-pressed', String(isPassword)); | |
| }); | |
| // Caps lock indicator | |
| function handleKeyEventForCaps(e) { | |
| if (typeof e.getModifierState === 'function') { | |
| const on = e.getModifierState('CapsLock'); | |
| if (on) { | |
| capsLockIndicator?.removeAttribute('hidden'); | |
| } else { | |
| capsLockIndicator?.setAttribute('hidden', ''); | |
| } | |
| } | |
| } | |
| passwordInput.addEventListener('keydown', handleKeyEventForCaps); | |
| passwordInput.addEventListener('keyup', handleKeyEventForCaps); | |
| passwordInput.addEventListener('input', () => { | |
| updatePasswordMeter(); | |
| }); | |
| // Remember me: prefill email | |
| const savedEmail = localStorage.getItem('aimhsa_saved_email'); | |
| if (savedEmail) { | |
| emailInput.value = savedEmail; | |
| rememberMe.checked = true; | |
| } | |
| // Email basic validation hint | |
| emailInput.addEventListener('input', () => { | |
| const v = emailInput.value.trim(); | |
| if (!v) { | |
| emailHint.textContent = ''; | |
| return; | |
| } | |
| const emailPattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; | |
| emailHint.textContent = !emailPattern.test(v) ? 'Please enter a valid email address' : ''; | |
| }); | |
| // Forgot password (client-side placeholder) | |
| forgotLink.addEventListener('click', (e) => { | |
| e.preventDefault(); | |
| openFpModal(); | |
| }); | |
| // Simple cooldown after repeated failures | |
| const COOLDOWN_KEY = 'aimhsa_login_cooldown_until'; | |
| function isInCooldown() { | |
| const until = Number(localStorage.getItem(COOLDOWN_KEY) || '0'); | |
| return Date.now() < until; | |
| } | |
| function applyCooldown(seconds) { | |
| const until = Date.now() + seconds * 1000; | |
| localStorage.setItem(COOLDOWN_KEY, String(until)); | |
| } | |
| function getCooldownRemainingMs() { | |
| const until = Number(localStorage.getItem(COOLDOWN_KEY) || '0'); | |
| return Math.max(0, until - Date.now()); | |
| } | |
| function updateCooldownUI() { | |
| const remaining = getCooldownRemainingMs(); | |
| if (remaining > 0) { | |
| signInBtn.disabled = true; | |
| const secs = Math.ceil(remaining / 1000); | |
| signInBtn.textContent = `Try again in ${secs}s`; | |
| } else { | |
| signInBtn.disabled = false; | |
| signInBtn.textContent = 'Sign In'; | |
| } | |
| } | |
| if (isInCooldown()) { | |
| updateCooldownUI(); | |
| const timer = setInterval(() => { | |
| updateCooldownUI(); | |
| if (!isInCooldown()) clearInterval(timer); | |
| }, 500); | |
| } | |
| // Login form submission | |
| loginForm.addEventListener('submit', async (e) => { | |
| e.preventDefault(); | |
| if (isInCooldown()) { | |
| updateCooldownUI(); | |
| return; | |
| } | |
| const email = emailInput.value.trim(); | |
| const password = passwordInput.value; | |
| if (!email || !password) { | |
| showMessage('Please enter both email and password'); | |
| return; | |
| } | |
| // Email validation | |
| const emailPattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; | |
| if (!emailPattern.test(email)) { | |
| showMessage('Please enter a valid email address'); | |
| return; | |
| } | |
| if (rememberMe.checked) { | |
| localStorage.setItem('aimhsa_saved_email', email); | |
| } else { | |
| localStorage.removeItem('aimhsa_saved_email'); | |
| } | |
| signInBtn.disabled = true; | |
| signInBtn.textContent = 'Signing in...'; | |
| try { | |
| // Try user login first | |
| try { | |
| console.log('Trying user login for:', email); | |
| const res = await api('/api/login', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ email, password }) | |
| }); | |
| if (res && res.mfa_required) { | |
| // Launch MFA modal | |
| openMfaModal({ flow: 'user', email, token: res.mfa_token }); | |
| } else { | |
| showMessage('Successfully signed in as user!', 'success'); | |
| setTimeout(() => redirectToApp(res.account || email), 1000); | |
| } | |
| return; | |
| } catch (userErr) { | |
| console.log('User login failed:', userErr.message); | |
| console.log('Trying professional login...'); | |
| } | |
| // Try professional login | |
| try { | |
| console.log('Trying professional login for:', email); | |
| const res = await api('/professional/login', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ email, password }) | |
| }); | |
| // Store professional data | |
| localStorage.setItem('aimhsa_professional', JSON.stringify(res)); | |
| if (res && res.mfa_required) { | |
| openMfaModal({ flow: 'professional', email, token: res.mfa_token }); | |
| } else { | |
| showMessage('Successfully signed in as professional!', 'success'); | |
| setTimeout(() => { | |
| window.location.href = '/professional_dashboard.html'; | |
| }, 1000); | |
| } | |
| return; | |
| } catch (profErr) { | |
| console.log('Professional login failed:', profErr.message); | |
| console.log('Trying admin login...'); | |
| } | |
| // Try admin login | |
| try { | |
| console.log('Trying admin login for:', email); | |
| const res = await api('/admin/login', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ username: email, password }) | |
| }); | |
| console.log('Admin login successful:', res); | |
| // Store admin data | |
| localStorage.setItem('aimhsa_admin', JSON.stringify(res)); | |
| if (res && res.mfa_required) { | |
| openMfaModal({ flow: 'admin', email, token: res.mfa_token }); | |
| } else { | |
| showMessage('Successfully signed in as admin!', 'success'); | |
| setTimeout(() => { | |
| window.location.href = res.redirect || '/admin_dashboard.html'; | |
| }, 1000); | |
| } | |
| return; | |
| } catch (adminErr) { | |
| console.log('Admin login failed:', adminErr.message); | |
| } | |
| // If all login attempts failed | |
| showMessage('Invalid username or password. Please check your credentials.'); | |
| // backoff: 10s cooldown after aggregated failure | |
| applyCooldown(10); | |
| updateCooldownUI(); | |
| } catch (err) { | |
| console.error('Login error:', err); | |
| showMessage('Login failed. Please try again.'); | |
| } finally { | |
| signInBtn.disabled = false; | |
| signInBtn.textContent = 'Sign In'; | |
| } | |
| }); | |
| // Anonymous access | |
| anonBtn.addEventListener('click', () => { | |
| localStorage.setItem('aimhsa_account', 'null'); | |
| window.location.href = '/index.html'; | |
| }); | |
| // Check if already logged in | |
| const account = localStorage.getItem('aimhsa_account'); | |
| if (account && account !== 'null') { | |
| redirectToApp(account); | |
| } | |
| // --- MFA helpers --- | |
| function openMfaModal(context) { | |
| mfaModal.classList.add('open'); | |
| mfaModal.setAttribute('aria-hidden', 'false'); | |
| mfaCode.value = ''; | |
| mfaMessage.textContent = ''; | |
| mfaCode.focus(); | |
| function close() { | |
| mfaModal.classList.remove('open'); | |
| mfaModal.setAttribute('aria-hidden', 'true'); | |
| } | |
| function onClose() { | |
| close(); | |
| cleanup(); | |
| } | |
| async function verify() { | |
| const code = mfaCode.value.trim(); | |
| if (!/^[0-9]{6}$/.test(code)) { | |
| mfaMessage.textContent = 'Please enter a valid 6-digit code.'; | |
| return; | |
| } | |
| mfaVerifyBtn.disabled = true; | |
| mfaVerifyBtn.textContent = 'Verifying...'; | |
| try { | |
| const endpoint = | |
| context.flow === 'admin' ? '/admin/mfa/verify' : | |
| context.flow === 'professional' ? '/professional/mfa/verify' : | |
| '/mfa/verify'; | |
| const res = await api(endpoint, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ username: context.username, code, token: context.token }) | |
| }); | |
| mfaMessage.textContent = 'MFA verified. Redirecting...'; | |
| setTimeout(() => { | |
| if (context.flow === 'admin') { | |
| window.location.href = '/admin_dashboard.html'; | |
| } else if (context.flow === 'professional') { | |
| window.location.href = '/professional_dashboard.html'; | |
| } else { | |
| redirectToApp(context.username); | |
| } | |
| }, 600); | |
| } catch (err) { | |
| mfaMessage.textContent = 'Invalid or expired code. Please try again.'; | |
| } finally { | |
| mfaVerifyBtn.disabled = false; | |
| mfaVerifyBtn.textContent = 'Verify'; | |
| } | |
| } | |
| async function resend() { | |
| try { | |
| const endpoint = | |
| context.flow === 'admin' ? '/admin/mfa/resend' : | |
| context.flow === 'professional' ? '/professional/mfa/resend' : | |
| '/mfa/resend'; | |
| await api(endpoint, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ username: context.username, token: context.token }) | |
| }); | |
| mfaMessage.textContent = 'Code resent.'; | |
| } catch (err) { | |
| mfaMessage.textContent = 'Could not resend code. Try again later.'; | |
| } | |
| } | |
| function cleanup() { | |
| mfaBackdrop.removeEventListener('click', onClose); | |
| mfaClose.removeEventListener('click', onClose); | |
| mfaVerifyBtn.removeEventListener('click', verify); | |
| mfaResendBtn.removeEventListener('click', resend); | |
| } | |
| mfaBackdrop.addEventListener('click', onClose); | |
| mfaClose.addEventListener('click', onClose); | |
| mfaVerifyBtn.addEventListener('click', verify); | |
| mfaResendBtn.addEventListener('click', resend); | |
| } | |
| // --- Forgot Password helpers --- | |
| function openFpModal() { | |
| fpModal.classList.add('open'); | |
| fpModal.setAttribute('aria-hidden', 'false'); | |
| fpMessage.textContent = ''; | |
| fpStep1.classList.remove('hidden'); | |
| fpStep2.classList.add('hidden'); | |
| fpEmail.value = emailInput.value.trim(); | |
| setTimeout(() => fpEmail.focus(), 0); | |
| function close() { | |
| fpModal.classList.remove('open'); | |
| fpModal.setAttribute('aria-hidden', 'true'); | |
| } | |
| function onClose() { | |
| close(); | |
| cleanup(); | |
| } | |
| async function requestCode() { | |
| console.log('Request code function called'); | |
| console.log('fpEmail element:', fpEmail); | |
| console.log('fpEmail value:', fpEmail ? fpEmail.value : 'fpEmail is null'); | |
| const email = fpEmail.value.trim(); | |
| console.log('Email:', email); | |
| if (!email) { | |
| fpMessage.textContent = 'Please enter your email address.'; | |
| fpMessage.style.display = 'block'; | |
| return; | |
| } | |
| // Email validation | |
| const emailPattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; | |
| if (!emailPattern.test(email)) { | |
| fpMessage.textContent = 'Please enter a valid email address.'; | |
| fpMessage.style.display = 'block'; | |
| return; | |
| } | |
| fpRequestBtn.disabled = true; | |
| fpRequestBtn.textContent = 'Sending...'; | |
| fpMessage.style.display = 'none'; | |
| try { | |
| console.log('Making API call to /forgot_password'); | |
| const res = await api('/forgot_password', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ email: email }) | |
| }); | |
| console.log('API response:', res); | |
| if (res && res.ok) { | |
| // Show success message and token | |
| let message = res.message || 'Reset code sent successfully!'; | |
| if (res.token) { | |
| message += ` Your reset code is: ${res.token}`; | |
| } | |
| fpMessage.textContent = message; | |
| fpMessage.style.display = 'block'; | |
| fpMessage.className = 'modal-message success'; | |
| // Move to step 2 | |
| fpStep1.classList.add('hidden'); | |
| fpStep2.classList.remove('hidden'); | |
| fpCode.value = ''; | |
| fpNewPassword.value = ''; | |
| setTimeout(() => fpCode.focus(), 0); | |
| } else { | |
| fpMessage.textContent = res.error || 'Failed to send reset code.'; | |
| fpMessage.style.display = 'block'; | |
| fpMessage.className = 'modal-message error'; | |
| } | |
| } catch (err) { | |
| console.error('Forgot password error:', err); | |
| fpMessage.textContent = 'Could not initiate reset. Please check your connection and try again.'; | |
| fpMessage.style.display = 'block'; | |
| fpMessage.className = 'modal-message error'; | |
| } finally { | |
| fpRequestBtn.disabled = false; | |
| fpRequestBtn.textContent = 'Send code'; | |
| } | |
| } | |
| async function applyReset() { | |
| console.log('Apply reset function called'); | |
| const email = fpEmail.value.trim(); | |
| const code = fpCode.value.trim(); | |
| const newPw = fpNewPassword.value; | |
| console.log('Reset data:', { email, code, newPw: '***' }); | |
| if (!/^[0-9A-Z]{6}$/.test(code)) { | |
| fpMessage.textContent = 'Please enter the 6-character code.'; | |
| fpMessage.style.display = 'block'; | |
| fpMessage.className = 'modal-message error'; | |
| return; | |
| } | |
| if (newPw.length < 6) { | |
| fpMessage.textContent = 'New password must be at least 6 characters.'; | |
| fpMessage.style.display = 'block'; | |
| fpMessage.className = 'modal-message error'; | |
| return; | |
| } | |
| fpApplyBtn.disabled = true; | |
| fpApplyBtn.textContent = 'Resetting...'; | |
| fpMessage.style.display = 'none'; | |
| try { | |
| console.log('Making API call to /reset_password'); | |
| const res = await api('/reset_password', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ email: email, token: code, new_password: newPw }) | |
| }); | |
| console.log('Reset password response:', res); | |
| if (res && res.ok) { | |
| fpMessage.textContent = res.message || 'Password updated successfully! You can now sign in.'; | |
| fpMessage.style.display = 'block'; | |
| fpMessage.className = 'modal-message success'; | |
| setTimeout(() => { | |
| onClose(); | |
| emailInput.value = email; | |
| passwordInput.focus(); | |
| }, 2000); | |
| } else { | |
| fpMessage.textContent = res.error || 'Invalid code or error updating password.'; | |
| fpMessage.style.display = 'block'; | |
| fpMessage.className = 'modal-message error'; | |
| } | |
| } catch (err) { | |
| console.error('Reset password error:', err); | |
| fpMessage.textContent = 'Invalid code or error updating password. Please try again.'; | |
| fpMessage.style.display = 'block'; | |
| fpMessage.className = 'modal-message error'; | |
| } finally { | |
| fpApplyBtn.disabled = false; | |
| fpApplyBtn.textContent = 'Reset password'; | |
| } | |
| } | |
| async function resendCode() { | |
| // Reuse forgot_password to resend | |
| requestCode(); | |
| } | |
| function cleanup() { | |
| fpBackdrop.removeEventListener('click', onClose); | |
| fpClose.removeEventListener('click', onClose); | |
| fpRequestBtn.removeEventListener('click', requestCode); | |
| fpApplyBtn.removeEventListener('click', applyReset); | |
| fpResendBtn.removeEventListener('click', resendCode); | |
| } | |
| fpBackdrop.addEventListener('click', onClose); | |
| fpClose.addEventListener('click', onClose); | |
| console.log('Attaching event listener to fpRequestBtn:', fpRequestBtn); | |
| fpRequestBtn.addEventListener('click', requestCode); | |
| fpApplyBtn.addEventListener('click', applyReset); | |
| fpResendBtn.addEventListener('click', resendCode); | |
| } | |
| })(); | |