import React, { useState, useEffect } from 'react'; import { useForm } from 'react-hook-form'; import { yupResolver } from '@hookform/resolvers/yup'; import { X, Eye, EyeOff, LogIn, Loader2, Mail, Lock, Shield, ArrowLeft, Sparkles } from 'lucide-react'; import { useNavigate, Link } from 'react-router-dom'; import useAuthStore from '../../../store/useAuthStore'; import { loginSchema, LoginFormData } from '../../../shared/utils/validationSchemas'; import { useCompanySettings } from '../../../shared/contexts/CompanySettingsContext'; import * as yup from 'yup'; import { toast } from 'react-toastify'; import Recaptcha from '../../../shared/components/Recaptcha'; import { recaptchaService } from '../../../features/system/services/systemSettingsService'; import { useAntibotForm } from '../hooks/useAntibotForm'; import HoneypotField from '../../../shared/components/HoneypotField'; import authService from '../services/authService'; const mfaTokenSchema = yup.object().shape({ mfaToken: yup .string() .required('MFA token is required') .min(6, 'MFA token must be 6 digits') .max(8, 'MFA token must be 6-8 characters') .matches(/^\d+$|^[A-Z0-9]{8}$/, 'Invalid token format'), }); type MFATokenFormData = yup.InferType; const HousekeepingLoginPage: React.FC = () => { const { isLoading, error, clearError, requiresMFA, clearMFA, isAuthenticated, userInfo } = useAuthStore(); const { settings } = useCompanySettings(); const navigate = useNavigate(); const [showPassword, setShowPassword] = useState(false); const { honeypotValue, setHoneypotValue, recaptchaToken, setRecaptchaToken, validate: validateAntibot, rateLimitInfo, } = useAntibotForm({ formId: 'housekeeping-login', minTimeOnPage: 3000, minTimeToFill: 2000, requireRecaptcha: false, maxAttempts: 5, onValidationError: (errors) => { errors.forEach((err) => toast.error(err)); }, }); const { register: registerMFA, handleSubmit: handleSubmitMFA, formState: { errors: mfaErrors }, } = useForm({ resolver: yupResolver(mfaTokenSchema), defaultValues: { mfaToken: '', }, }); useEffect(() => { if (!isLoading && isAuthenticated && !requiresMFA && userInfo) { const role = userInfo.role?.toLowerCase() || (userInfo as any).role_name?.toLowerCase(); // Safety check - should not happen if onSubmit logic works correctly if (role !== 'housekeeping') { navigate(`/${role}/login`, { replace: true }); return; } navigate('/housekeeping/dashboard', { replace: true }); } }, [isLoading, isAuthenticated, requiresMFA, userInfo, navigate]); const { register, handleSubmit, formState: { errors }, } = useForm({ resolver: yupResolver(loginSchema), defaultValues: { email: '', password: '', rememberMe: false, }, }); const onSubmit = async (data: LoginFormData) => { try { clearError(); // Validate antibot protection const isValid = await validateAntibot(); if (!isValid) { return; } // Verify reCAPTCHA if token is provided if (recaptchaToken) { try { const verifyResponse = await recaptchaService.verifyRecaptcha(recaptchaToken); if (verifyResponse.status === 'error' || !verifyResponse.data.verified) { toast.error('reCAPTCHA verification failed. Please try again.'); setRecaptchaToken(null); return; } } catch (error) { toast.error('reCAPTCHA verification failed. Please try again.'); setRecaptchaToken(null); return; } } // Call login API directly to check role BEFORE showing success toast useAuthStore.setState({ isLoading: true, error: null }); try { const response = await authService.login({ email: data.email, password: data.password, rememberMe: data.rememberMe, expectedRole: 'housekeeping', // Housekeeping login page only accepts housekeeping }); // Handle MFA requirement if (response.requires_mfa) { useAuthStore.setState({ isLoading: false, requiresMFA: true, mfaUserId: response.user_id || null, pendingCredentials: { email: data.email, password: data.password, rememberMe: data.rememberMe, expectedRole: 'housekeeping', }, error: null, }); setRecaptchaToken(null); return; } // Check if login was successful if (response.success || response.status === 'success') { const user = response.data?.user ?? null; if (!user) { throw new Error(response.message || 'Login failed.'); } // Check role BEFORE setting authenticated state or showing success toast const role = user.role?.toLowerCase() || (user as any).role_name?.toLowerCase(); // Reject non-housekeeping roles - show error and don't authenticate if (role !== 'housekeeping') { // Call logout API to clear any server-side session await authService.logout().catch(() => { // Ignore logout errors }); // Show error toast toast.error(`This login is only for housekeeping staff. Please use the ${role} login page at /${role}/login to access your account.`, { autoClose: 6000, position: 'top-center', toastId: 'role-error', }); // Reset loading state useAuthStore.setState({ isLoading: false, error: 'Invalid role for this login page', }); setRecaptchaToken(null); // Navigate to the appropriate login page setTimeout(() => { navigate(`/${role}/login`, { replace: true }); }, 2000); return; } // Only proceed with authentication if user is housekeeping // Store minimal userInfo in localStorage const minimalUserInfo = { id: user.id, name: user.name, email: user.email, role: user.role, avatar: user.avatar, }; localStorage.setItem('userInfo', JSON.stringify(minimalUserInfo)); // Update auth store state useAuthStore.setState({ token: null, userInfo: user, isAuthenticated: true, isLoading: false, error: null, requiresMFA: false, mfaUserId: null, pendingCredentials: null, }); // Show success toast only for housekeeping toast.success('Login successful!'); setRecaptchaToken(null); } } catch (error: any) { const errorMessage = error.response?.data?.message || 'Login failed. Please try again.'; useAuthStore.setState({ isLoading: false, error: errorMessage, isAuthenticated: false, requiresMFA: false, mfaUserId: null, pendingCredentials: null, }); toast.error(errorMessage); setRecaptchaToken(null); } } catch (error) { if (import.meta.env.DEV) { console.error('Login error:', error); } setRecaptchaToken(null); } }; const onSubmitMFA = async (data: MFATokenFormData) => { try { clearError(); // Get pending credentials from store const state = useAuthStore.getState(); if (!state.pendingCredentials) { toast.error('No pending login credentials'); return; } // Call MFA verification API directly to check role before showing success useAuthStore.setState({ isLoading: true, error: null }); try { const credentials = { ...state.pendingCredentials, mfaToken: data.mfaToken, expectedRole: 'housekeeping', // Housekeeping login page only accepts housekeeping }; const response = await authService.login(credentials); if (response.success || response.status === 'success') { const user = response.data?.user ?? null; if (!user) { throw new Error(response.message || 'MFA verification failed.'); } // Check role BEFORE setting authenticated state or showing success toast const role = user.role?.toLowerCase() || (user as any).role_name?.toLowerCase(); // Reject non-housekeeping roles - show error and don't authenticate if (role !== 'housekeeping') { // Call logout API to clear any server-side session await authService.logout().catch(() => { // Ignore logout errors }); // Show error toast toast.error(`This login is only for housekeeping staff. Please use the ${role} login page at /${role}/login to access your account.`, { autoClose: 6000, position: 'top-center', toastId: 'role-error', }); // Reset auth store state useAuthStore.setState({ isLoading: false, error: 'Invalid role for this login page', requiresMFA: false, mfaUserId: null, pendingCredentials: null, }); // Navigate to the appropriate login page setTimeout(() => { navigate(`/${role}/login`, { replace: true }); }, 2000); return; } // Only proceed with authentication if user is housekeeping // Store minimal userInfo in localStorage const minimalUserInfo = { id: user.id, name: user.name, email: user.email, role: user.role, avatar: user.avatar, }; localStorage.setItem('userInfo', JSON.stringify(minimalUserInfo)); // Update auth store state useAuthStore.setState({ token: null, userInfo: user, isAuthenticated: true, isLoading: false, error: null, requiresMFA: false, mfaUserId: null, pendingCredentials: null, }); // Show success toast only for housekeeping toast.success('Login successful!'); } } catch (error: any) { const errorMessage = error.response?.data?.message || 'MFA verification failed. Please try again.'; useAuthStore.setState({ isLoading: false, error: errorMessage, requiresMFA: true, // Keep MFA state in case of error }); toast.error(errorMessage); } } catch (error) { if (import.meta.env.DEV) { console.error('MFA verification error:', error); } } }; const handleBackToLogin = () => { clearMFA(); clearError(); }; return (
{settings.company_logo_url ? ( {settings.company_name ) : (
)}

Housekeeping Login

Access your housekeeping dashboard

{requiresMFA ? (
{error && (
{error}
)}
{mfaErrors.mfaToken && (

{mfaErrors.mfaToken.message}

)}
) : (
{error && (
{error}
)} {rateLimitInfo && !rateLimitInfo.allowed && (

Too many login attempts.

Please try again after {new Date(rateLimitInfo.resetTime).toLocaleTimeString()}

)}
{errors.email && (

{errors.email.message}

)}
{errors.password && (

{errors.password.message}

)}
setRecaptchaToken(token)} onError={(error) => { if (import.meta.env.DEV) { console.error('reCAPTCHA error:', error); } setRecaptchaToken(null); }} theme="light" size="normal" />
)}
← Back to Home
); }; export default HousekeepingLoginPage;