// Login / create-account page. Two sign-in modes: // * "password" — email + password → session cookie // * "otp" — mobile number → SMS OTP → session cookie // Reuses ddaApi(), showDdaError(), showDdaSuccess() from shared.js. (function () { let mode = 'login'; // 'login' | 'register' let loginMethod = 'password'; // 'password' | 'otp' let pendingOtp = null; const form = document.getElementById('auth-form'); const otpForm = document.getElementById('otp-form'); const title = document.getElementById('auth-title'); const submitBtn = document.getElementById('auth-submit'); const tabsEl = document.getElementById('auth-tabs'); const toggleToRegister = document.getElementById('toggle-to-register'); const toggleToLogin = document.getElementById('toggle-to-login'); const countrySelect = document.getElementById('country_code'); const phoneInput = document.getElementById('phone'); const fields = { full_name: document.getElementById('full_name'), email: document.getElementById('email'), password: document.getElementById('password'), confirm_password: document.getElementById('confirm_password'), phone: phoneInput, }; const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (typeof window.populateCountrySelect === 'function') { window.populateCountrySelect(countrySelect, '+91'); } function setFieldError(name, message) { const el = document.getElementById('err-' + name); const input = fields[name]; if (message) { if (el) { el.textContent = message; el.classList.remove('hidden'); } if (input) input.classList.add('invalid'); } else { if (el) { el.textContent = ''; el.classList.add('hidden'); } if (input) input.classList.remove('invalid'); } } function clearFieldErrors() { Object.keys(fields).forEach((name) => setFieldError(name, null)); } function digitsOnly(value) { return String(value || '').replace(/\D/g, ''); } // Client-side validity check for the local part of the mobile number. // The full E.164 validation still happens on the server. function isPhoneValid(dial, local) { const digits = digitsOnly(local); if (dial === '+91') return digits.length === 10 && /^[6-9]/.test(digits); return digits.length >= 6 && digits.length <= 15; } function showFields() { const showByField = (name, on) => { document.querySelectorAll(`[data-field="${name}"]`).forEach((el) => { el.classList.toggle('hidden', !on); }); }; const isRegister = mode === 'register'; const isLogin = mode === 'login'; const passwordMode = isLogin && loginMethod === 'password'; const otpMode = isLogin && loginMethod === 'otp'; showByField('full_name', isRegister); showByField('email', true); showByField('password', isRegister || passwordMode); showByField('confirm_password', isRegister); showByField('phone', isRegister); showByField('remember', isLogin); showByField('forgot', passwordMode); document.getElementById('hint-password').classList.toggle('hidden', !isRegister); fields.email.required = true; fields.password.required = isRegister || passwordMode; fields.full_name.required = isRegister; fields.confirm_password.required = isRegister; fields.phone.required = false; tabsEl.classList.toggle('hidden', isRegister); if (isRegister) { title.textContent = 'Create account'; submitBtn.textContent = 'Create account'; } else if (otpMode) { title.textContent = 'Sign in with OTP'; submitBtn.textContent = 'Send OTP to email'; } else { title.textContent = 'Sign in'; submitBtn.textContent = 'Sign in'; } } function setMode(next) { mode = next; const isRegister = mode === 'register'; toggleToRegister.classList.toggle('hidden', isRegister); toggleToLogin.classList.toggle('hidden', !isRegister); fields.password.autocomplete = isRegister ? 'new-password' : 'current-password'; hideDdaError(); clearFieldErrors(); showFields(); } function setLoginMethod(next) { loginMethod = next; tabsEl.querySelectorAll('.dda-auth-tab').forEach((btn) => { const active = btn.dataset.tab === next; btn.classList.toggle('is-active', active); btn.setAttribute('aria-selected', active ? 'true' : 'false'); }); hideDdaError(); clearFieldErrors(); showFields(); } tabsEl?.querySelectorAll('.dda-auth-tab').forEach((btn) => { btn.addEventListener('click', () => setLoginMethod(btn.dataset.tab)); }); function showOtpStep(on, masked) { form.classList.toggle('hidden', on); otpForm.classList.toggle('hidden', !on); tabsEl.classList.toggle('hidden', on); document.querySelector('.auth-toggle')?.classList.toggle('hidden', on); if (on) { title.textContent = 'Enter email code'; const help = document.getElementById('otp-help'); if (help) { help.textContent = masked ? `We sent a 6-digit code to ${masked}. It expires in 10 minutes.` : 'Enter the 6-digit code sent to your email.'; } document.getElementById('otp_code').value = ''; document.getElementById('otp_code').focus(); } else { pendingOtp = null; showFields(); } hideDdaError(); } otpForm.addEventListener('submit', async (e) => { e.preventDefault(); hideDdaError(); const code = digitsOnly(document.getElementById('otp_code')?.value); if (!/^\d{6}$/.test(code)) { showDdaError('Enter the 6-digit code from your email.'); return; } const btn = document.getElementById('otp-submit'); if (btn) btn.disabled = true; try { await ddaApi('POST', '/api/auth/login/verify-otp', { body: JSON.stringify({ otpToken: pendingOtp?.token, code }), }); window.location.href = '/'; } catch (err) { showDdaError(err.message || 'Invalid sign-in code.'); } finally { if (btn) btn.disabled = false; } }); document.getElementById('otp-resend')?.addEventListener('click', async () => { hideDdaError(); const btn = document.getElementById('otp-resend'); if (btn) btn.disabled = true; try { const data = await ddaApi('POST', '/api/auth/login/resend-otp', { body: JSON.stringify({ otpToken: pendingOtp?.token }), }); if (data?.otpToken) pendingOtp = { ...pendingOtp, token: data.otpToken }; showDdaSuccess('A new code was sent to your email.'); showOtpStep(true, data?.emailMasked || pendingOtp?.emailMasked); } catch (err) { showDdaError(err.message || 'Could not resend the code.'); } finally { if (btn) btn.disabled = false; } }); document.getElementById('otp-back')?.addEventListener('click', () => showOtpStep(false)); document.getElementById('show-register').addEventListener('click', () => setMode('register')); document.getElementById('show-login').addEventListener('click', () => setMode('login')); function validate() { clearFieldErrors(); let ok = true; const isRegister = mode === 'register'; const passwordMode = !isRegister && loginMethod === 'password'; const otpMode = !isRegister && loginMethod === 'otp'; if (isRegister && !fields.full_name.value.trim()) { setFieldError('full_name', 'Full name is required.'); ok = false; } const email = fields.email.value.trim(); if (!email) { setFieldError('email', 'Email is required.'); ok = false; } else if (!EMAIL_RE.test(email)) { setFieldError('email', 'Enter a valid email address.'); ok = false; } if (isRegister || passwordMode) { const password = fields.password.value; if (!password) { setFieldError('password', 'Password is required.'); ok = false; } else if (isRegister && password.length < 8) { setFieldError('password', 'Password must be at least 8 characters.'); ok = false; } if (isRegister) { const confirm = fields.confirm_password.value; if (!confirm) { setFieldError('confirm_password', 'Please confirm your password.'); ok = false; } else if (confirm !== password) { setFieldError('confirm_password', 'Passwords do not match.'); ok = false; } } } if (isRegister) { const local = digitsOnly(fields.phone.value); if (local) { const dial = countrySelect.value || '+91'; if (!isPhoneValid(dial, local)) { setFieldError('phone', dial === '+91' ? 'Enter a valid 10-digit Indian mobile number.' : 'Enter a valid mobile number.'); ok = false; } } } return ok; } fields.confirm_password.addEventListener('input', () => { if (mode !== 'register') return; const confirm = fields.confirm_password.value; if (!confirm) return; setFieldError('confirm_password', confirm === fields.password.value ? null : 'Passwords do not match.'); }); phoneInput.addEventListener('input', () => { phoneInput.value = digitsOnly(phoneInput.value); }); form.addEventListener('submit', async (e) => { e.preventDefault(); hideDdaError(); document.getElementById('dda-success')?.classList.add('hidden'); if (!validate()) return; submitBtn.disabled = true; try { if (mode === 'register') { await ddaApi('POST', '/api/auth/register', { body: JSON.stringify({ email: fields.email.value.trim(), password: fields.password.value, full_name: fields.full_name.value.trim(), phone: digitsOnly(fields.phone.value), country_code: countrySelect.value || '+91', }), }); window.location.href = '/login?registered=1'; return; } const remember_me = document.getElementById('remember_me').checked; if (loginMethod === 'password') { await ddaApi('POST', '/api/auth/login', { body: JSON.stringify({ email: fields.email.value.trim(), password: fields.password.value, remember_me, }), }); window.location.href = '/'; return; } const data = await ddaApi('POST', '/api/auth/login/request-otp', { body: JSON.stringify({ email: fields.email.value.trim(), remember_me, }), }); pendingOtp = { token: data.otpToken, emailMasked: data.emailMasked, }; showOtpStep(true, data.emailMasked); } catch (err) { const msg = err.message || 'Sign-in failed.'; if (/already registered/i.test(msg) && /mobile/i.test(msg)) { setFieldError('phone', 'This mobile number is already registered.'); } else if (/already registered/i.test(msg)) { setFieldError('email', 'This email is already registered.'); } else if (/no account/i.test(msg) || /unknown email/i.test(msg)) { setFieldError('email', 'No account is registered with this email.'); showDdaError('No account is registered with this email. Try Password sign-in or create an account.'); } else if (/mobile number/i.test(msg)) { setFieldError('phone', msg); } else { showDdaError(msg); } } finally { submitBtn.disabled = false; } }); showFields(); if (new URLSearchParams(window.location.search).get('registered') === '1') { showDdaSuccess('Account created. Please sign in.'); window.history.replaceState({}, '', '/login'); } })();