'use client'; import { useState, useEffect, Suspense, useCallback } from 'react'; import { useRouter, useSearchParams } from 'next/navigation'; import { useTranslations, type AppConfig } from 'use-intl'; import { getLandingPage, resolveSafeRedirect } from '@/components/layout/AuthGuard'; import { useAuthStore, StorageUnavailableError } from '@/store/auth'; import { parseLoginFailure } from '@/lib/login-errors'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Card, CardContent } from '@/components/ui/card'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import toast from 'react-hot-toast'; import { Delete, Eye, EyeOff, KeyRound, LifeBuoy, Mail, MessageCircle, Ticket, UserCheck, } from 'lucide-react'; import { ROLE_LABEL_KEYS } from '@/lib/i18n-enums'; import { SupportTicketForm, PRE_LOGIN_SUPPORT_ENDPOINTS, } from '@/components/support/SupportTicketForm'; interface PinPickerUser { id: string; name: string; role: string; } const WHATSAPP_COMMUNITY_URL = 'https://chat.whatsapp.com/LxHobzv6d2X81NzrVfMjzo?mode=gi_t'; const SETUP_STATUS_MAX_RETRIES = 2; const SETUP_STATUS_RETRY_DELAY_MS = 400; // Backend enum → leaf key maps for the tenant picker. type BusinessTypeKey = keyof AppConfig['Messages']['businessType']; const BUSINESS_TYPE_LEAF_KEYS: Record = { restaurant: 'restaurant', }; function LoginContent() { const router = useRouter(); const searchParams = useSearchParams(); const { login, pinLogin, selectTenant, user, tenants, currentTenant, loadFromStorage } = useAuthStore(); const t = useTranslations('auth'); const tStaff = useTranslations('staff'); const tCommon = useTranslations('common'); const tBusinessType = useTranslations('businessType'); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [rememberMe, setRememberMe] = useState(false); const [loading, setLoading] = useState(false); const [dbError, setDbError] = useState(null); const [loginError, setLoginError] = useState(null); const [showPassword, setShowPassword] = useState(false); const [ticketDialogOpen, setTicketDialogOpen] = useState(false); const [pinUsers, setPinUsers] = useState([]); const [selectedPinUser, setSelectedPinUser] = useState(null); const [pin, setPin] = useState(''); const [pinError, setPinError] = useState(null); const [signInMode, setSignInMode] = useState<'pin' | 'password' | null>(null); const tSupport = useTranslations('support'); useEffect(() => { const controller = new AbortController(); let active = true; let retryTimer: ReturnType | null = null; const checkSetupStatus = (attempt: number) => { fetch('/api/auth/setup/status', { signal: controller.signal }) .then((r) => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); }) .then((data) => { if (active && data?.needsSetup) router.replace('/setup'); }) .catch((err) => { if ( !active || (err instanceof Error && (err.name === 'CanceledError' || err.name === 'AbortError')) ) return; if (attempt < SETUP_STATUS_MAX_RETRIES) { retryTimer = setTimeout(() => { if (active) checkSetupStatus(attempt + 1); }, SETUP_STATUS_RETRY_DELAY_MS); } }); }; checkSetupStatus(0); fetch('/api/health') .then((r) => (r.ok ? r.json() : null)) .then((data) => { if (active && data && data.status !== 'ok') { setDbError(data.db || t('dbErrorPrefix')); } }) .catch(() => {}); fetch('/api/auth/pin-users') .then((r) => (r.ok ? r.json() : null)) .then((data) => { if (active && Array.isArray(data?.users)) { setPinUsers(data.users); } }) .catch(() => {}); return () => { active = false; if (retryTimer !== null) clearTimeout(retryTimer); controller.abort(); }; }, [router, t]); useEffect(() => { loadFromStorage(); }, [loadFromStorage]); const handleTenantSelect = useCallback( async (tenantId: number) => { setLoading(true); try { await selectTenant(tenantId); // useEffect on currentTenant will handle the redirect } catch { toast.error(t('selectBusinessFailed')); } finally { setLoading(false); } }, [selectTenant, t], ); useEffect(() => { // Navigate to landing page once user and tenant are selected (auto-selection handled in auth store). if (user && currentTenant) { router.push( resolveSafeRedirect(searchParams.get('redirect')) ?? getLandingPage(currentTenant), ); } }, [user, currentTenant, router, searchParams]); const handleLogin = async (e: React.FormEvent) => { e.preventDefault(); setLoading(true); setLoginError(null); try { await login(email, password, rememberMe); toast.success(t('signInSuccess')); } catch (err: unknown) { if (err instanceof StorageUnavailableError) { // Server login succeeded but the session could not be persisted. setLoginError(t('storageUnavailable')); } else { const failure = parseLoginFailure(err); if (failure.status === 401) { const remaining = failure.attemptsRemaining; if (remaining === 0) { // Just got locked out const mins = failure.lockoutMinutes ?? 15; setLoginError(t('lockedOut', { minutes: mins })); } else if (typeof remaining === 'number' && remaining < 4) { // Warn only when getting close (≤ 4 remaining to avoid noise on first attempt) setLoginError( t('invalidCredentials') + ' ' + t('attemptsRemaining', { count: remaining }), ); } else { setLoginError(t('invalidCredentials')); } } else if (failure.status === 429) { // Middleware-level lockout (authRateLimit window exhausted) setLoginError(t('lockedOut', { minutes: 15 })); } else if (failure.status === undefined) { // No HTTP response at all: the server was unreachable (network). setLoginError(t('connectionFailed')); } else { // Other server-side failures belong under the database/setup banner. setDbError(t('loginFailed')); } } } finally { setLoading(false); } }; const handlePinLogin = async (e: React.FormEvent) => { e.preventDefault(); if (!selectedPinUser || pin.length < 4) return; setLoading(true); setPinError(null); try { await pinLogin(selectedPinUser.id, pin, rememberMe); toast.success(t('signInSuccess')); } catch (err: unknown) { if (err instanceof StorageUnavailableError) { setPinError(t('storageUnavailable')); } else { const failure = parseLoginFailure(err); if (failure.status === 401) { const remaining = failure.attemptsRemaining; if (remaining === 0) { const mins = failure.lockoutMinutes ?? 15; setPinError(t('lockedOut', { minutes: mins })); } else if (typeof remaining === 'number' && remaining < 4) { setPinError( t('invalidCredentials') + ' ' + t('attemptsRemaining', { count: remaining }), ); } else { setPinError(t('invalidCredentials')); } } else if (failure.status === 429) { const mins = failure.lockoutMinutes ?? 15; setPinError(t('lockedOut', { minutes: mins })); } else if (failure.status === undefined) { setPinError(t('connectionFailed')); } else { setPinError(t('loginFailed')); } } setPin(''); } finally { setLoading(false); } }; const shouldShowTenantSelect = !!( user && (tenants.length > 1 || searchParams.get('select_tenant') === 'true') ); if (shouldShowTenantSelect) { return (

{t('selectBusiness')}

{t('selectBusinessHint')}

{tenants.map((tenant) => { const businessTypeKey = tenant.business_type ? BUSINESS_TYPE_LEAF_KEYS[tenant.business_type] : undefined; const roleKey = tenant.role ? ROLE_LABEL_KEYS[tenant.role] : undefined; return ( ); })}
); } const activeSignInMode = pinUsers.length > 0 && (signInMode ?? 'pin') === 'pin' && !email && !password ? 'pin' : 'password'; return (
Flo

{t('signInTitle')}

{dbError && (
{t('dbErrorPrefix')} {dbError}
)} {pinUsers.length > 0 && (
)} {activeSignInMode === 'pin' ? (

{t('quickSwitchTitle')}

{t('quickSwitchSubtitle')}

{selectedPinUser ? (
{selectedPinUser.name}
{ROLE_LABEL_KEYS[selectedPinUser.role] ? tStaff(ROLE_LABEL_KEYS[selectedPinUser.role]) : selectedPinUser.role}
{ setPin(e.target.value.replace(/\D/g, '').slice(0, 6)); if (pinError) setPinError(null); }} placeholder="••••" className="text-center tracking-widest text-lg font-mono" />
{(['1', '2', '3', '4', '5', '6', '7', '8', '9'] as const).map((digit) => ( ))}
{pinError && (

{pinError}

)}
) : (
{pinUsers.map((u) => { const roleKey = ROLE_LABEL_KEYS[u.role]; return ( ); })}
)}
) : (
setSignInMode('password')} onChange={(e) => setEmail(e.target.value)} placeholder={t('emailPlaceholder')} dir="ltr" required />
setSignInMode('password')} onChange={(e) => setPassword(e.target.value)} placeholder={t('passwordPlaceholder')} className="pe-10" required />
{loginError &&

{loginError}

}
)}
window.setTimeout(() => setTicketDialogOpen(true), 0)} > {tSupport('menuSubmitTicket')} {tSupport('menuWhatsapp')}
{tSupport('dialogTitle')}
); } export default function LoginPage() { return ( ); }