'use client'; import { Button } from '@/components/actions/Button'; import { TextInput } from '@/components/forms/TextInput'; import { FormField } from '@/components/forms/FormField'; import { AuthShell } from '@/components/auth/AuthShell'; import { API_BASE, EMPLOYEE_PROFILE_KEY } from '@/lib/api'; import { localizedPath, type Locale } from '@/lib/localization/config'; import { useSearchParams } from 'next/navigation'; import { useState } from 'react'; interface Dict { title: string; subtitle: string; email: string; emailPlaceholder: string; password: string; signIn: string; signingIn: string; verify: string; verifying: string; authCode: string; enterCode: string; totpPlaceholder: string; back: string; forgotPassword: string; invalidCredentials: string; passwordNotSet: string; tooManyRequests: string; emailNotVerified: string; unexpectedError: string; } const dicts: Record = { en: { title: 'Sign in', subtitle: 'Enter your credentials to access your account.', email: 'Email', emailPlaceholder: 'owner@company.com', password: 'Password', signIn: 'Sign in', signingIn: 'Signing in…', verify: 'Verify code', verifying: 'Verifying…', authCode: 'Authentication code', enterCode: 'Enter the 6-digit code from your authenticator app.', totpPlaceholder: '000000 or XXXX-XXXX-XXXX', back: 'Back to credentials', forgotPassword: 'Forgot your password?', invalidCredentials: 'Invalid email or password.', passwordNotSet: 'No password set yet. Check your invitation email or use "Forgot your password?"', tooManyRequests: 'Too many attempts. Please try again later.', emailNotVerified: 'Please verify your email first. Check your inbox for the verification link.', unexpectedError: 'Something went wrong. Please try again.', }, fr: { title: 'Connexion', subtitle: 'Saisissez vos identifiants pour accéder à votre compte.', email: 'Email', emailPlaceholder: 'owner@company.com', password: 'Mot de passe', signIn: 'Connexion', signingIn: 'Connexion…', verify: 'Vérifier le code', verifying: 'Vérification…', authCode: "Code d'authentification", enterCode: "Entrez le code à 6 chiffres de votre application d'authentification.", totpPlaceholder: '000000 ou XXXX-XXXX-XXXX', back: 'Retour aux identifiants', forgotPassword: 'Mot de passe oublié ?', invalidCredentials: 'Adresse e-mail ou mot de passe invalide.', passwordNotSet: "Aucun mot de passe défini. Vérifiez votre e-mail d'invitation ou utilisez « Mot de passe oublié ? »", tooManyRequests: 'Trop de tentatives. Veuillez réessayer plus tard.', emailNotVerified: 'Veuillez vérifier votre adresse email. Consultez votre boîte de réception.', unexpectedError: 'Une erreur est survenue. Veuillez réessayer.', }, ar: { title: 'تسجيل الدخول', subtitle: 'أدخل بياناتك للوصول إلى حسابك.', email: 'البريد الإلكتروني', emailPlaceholder: 'owner@company.com', password: 'كلمة المرور', signIn: 'تسجيل الدخول', signingIn: 'جارٍ تسجيل الدخول…', verify: 'تحقق من الرمز', verifying: 'جارٍ التحقق…', authCode: 'رمز المصادقة', enterCode: 'أدخل الرمز المكون من 6 أرقام من تطبيق المصادقة.', totpPlaceholder: '000000 أو XXXX-XXXX-XXXX', back: 'العودة إلى بيانات الدخول', forgotPassword: 'نسيت كلمة المرور؟', invalidCredentials: 'البريد الإلكتروني أو كلمة المرور غير صحيحة.', passwordNotSet: 'لم يتم تعيين كلمة مرور بعد. تحقق من بريد الدعوة أو استخدم "نسيت كلمة المرور؟"', tooManyRequests: 'محاولات كثيرة جدًا. يرجى المحاولة لاحقًا.', emailNotVerified: 'يرجى التحقق من بريدك الإلكتروني أولاً. تحقق من صندوق الوارد.', unexpectedError: 'حدث خطأ ما. يرجى المحاولة مرة أخرى.', }, }; export function SignInForm({ locale }: { locale: Locale }) { const dict = (dicts[locale] ?? dicts.en) as Dict; const searchParams = useSearchParams(); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [showPassword, setShowPassword] = useState(false); const [totpCode, setTotpCode] = useState(''); const [step, setStep] = useState<'credentials' | 'totp'>('credentials'); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const requestedPortal = searchParams.get('portal'); const employeeRedirect = searchParams.get('redirect') || '/dashboard'; const preferAdminAuth = requestedPortal === 'admin'; async function handleCredentials(e: React.FormEvent) { e.preventDefault(); setLoading(true); setError(null); try { const tryAdminLogin = async () => { const adminRes = await fetch(`${API_BASE}/admin/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ email, password }), }); const adminJson = await adminRes.json(); if (adminRes.ok && adminJson?.data?.admin) { window.location.href = `${window.location.origin}/admin/dashboard`; return true; } if (adminRes.status === 401 && adminJson?.error === 'totp_required') { setStep('totp'); return true; } if (adminRes.status === 429) { setError(dict.tooManyRequests); return true; } return false; }; const tryEmployeeLogin = async () => { const empRes = await fetch(`${API_BASE}/auth/employee/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ email, password }), }); const empJson = await empRes.json(); if (empRes.ok && empJson?.data?.employee) { if (empJson?.data?.employee) { localStorage.setItem(EMPLOYEE_PROFILE_KEY, JSON.stringify(empJson.data.employee)); const prefLang = empJson.data.employee?.preferredLanguage; if (prefLang === 'en' || prefLang === 'fr' || prefLang === 'ar') { document.cookie = `rentaldrivego-language=${prefLang}; path=/; max-age=31536000; samesite=lax`; } } window.dispatchEvent(new CustomEvent('rentaldrivego:auth-changed')); window.location.replace(employeeRedirect); return true; } if (empJson?.error === 'password_not_set') { setError(dict.passwordNotSet); return true; } if (empJson?.error === 'email_not_verified') { setError(dict.emailNotVerified); return true; } if (empRes.status === 429) { setError(dict.tooManyRequests); return true; } return false; }; if (preferAdminAuth) { if (await tryAdminLogin()) return; setError(dict.invalidCredentials); return; } if (await tryEmployeeLogin()) return; setError(dict.invalidCredentials); } catch { setError(dict.unexpectedError); } finally { setLoading(false); } } async function handleTotp(e: React.FormEvent) { e.preventDefault(); setLoading(true); setError(null); try { const normalizedCode = totpCode.trim().toUpperCase(); const secondFactor = /^\d{6}$/.test(normalizedCode) ? { totpCode: normalizedCode } : { recoveryCode: normalizedCode }; const adminRes = await fetch(`${API_BASE}/admin/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ email, password, ...secondFactor }), }); const adminJson = await adminRes.json(); if (adminRes.ok && adminJson?.data?.admin) { window.location.href = `${window.location.origin}/admin/dashboard`; return; } setError(dict.invalidCredentials); } catch { setError(dict.unexpectedError); } finally { setLoading(false); } } const hrefBase = localizedPath('forgot-password', locale); return ( {error ? (
{error}
) : null} {step === 'credentials' ? (
setEmail(e.target.value)} placeholder={dict.emailPlaceholder} autoComplete="email" />
setPassword(e.target.value)} placeholder="••••••••" autoComplete="current-password" style={{ width: '100%', minHeight: '2.875rem', padding: 'var(--space-3) 2.5rem var(--space-3) var(--space-4)', border: '1px solid var(--border-strong)', borderRadius: 'var(--radius-sm)', background: 'var(--surface-primary)', color: 'var(--text-primary)', fontSize: '0.875rem', }} />
{dict.forgotPassword}
) : (
{dict.enterCode}
setTotpCode(e.target.value.replace(/[^0-9A-Za-z-]/g, '').toUpperCase()) } placeholder={dict.totpPlaceholder} style={{ textAlign: 'center', fontSize: '1.25rem', letterSpacing: '0.45em' }} />
)}
); }