(() => { 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); } })();