From 2251e97688b2af4d11265ae7dbbdefe6080fedec Mon Sep 17 00:00:00 2001 From: Iliyan Angelov Date: Fri, 21 Nov 2025 09:57:52 +0200 Subject: [PATCH] updates --- Frontend/src/App.tsx | 38 +- .../components/ResetPasswordRouteHandler.tsx | 22 + Frontend/src/components/auth/AdminRoute.tsx | 21 +- .../src/components/auth/CustomerRoute.tsx | 21 +- .../src/components/auth/ProtectedRoute.tsx | 21 +- Frontend/src/components/auth/StaffRoute.tsx | 21 +- Frontend/src/components/layout/Header.tsx | 42 +- .../src/components/layout/SidebarAdmin.tsx | 2 +- .../src/components/layout/SidebarStaff.tsx | 2 +- .../components/modals/AuthModalManager.tsx | 30 + .../components/modals/ForgotPasswordModal.tsx | 277 +++++++++ Frontend/src/components/modals/LoginModal.tsx | 390 ++++++++++++ .../src/components/modals/RegisterModal.tsx | 419 +++++++++++++ .../components/modals/ResetPasswordModal.tsx | 390 ++++++++++++ Frontend/src/contexts/AuthModalContext.tsx | 57 ++ Frontend/src/pages/NotFoundPage.tsx | 101 +++ Frontend/src/pages/admin/DashboardPage.tsx | 2 +- .../src/pages/auth/ForgotPasswordPage.tsx | 385 ------------ Frontend/src/pages/auth/LoginPage.tsx | 540 ---------------- Frontend/src/pages/auth/RegisterPage.tsx | 571 ----------------- Frontend/src/pages/auth/ResetPasswordPage.tsx | 579 ------------------ Frontend/src/pages/customer/FavoritesPage.tsx | 8 +- 22 files changed, 1769 insertions(+), 2170 deletions(-) create mode 100644 Frontend/src/components/ResetPasswordRouteHandler.tsx create mode 100644 Frontend/src/components/modals/AuthModalManager.tsx create mode 100644 Frontend/src/components/modals/ForgotPasswordModal.tsx create mode 100644 Frontend/src/components/modals/LoginModal.tsx create mode 100644 Frontend/src/components/modals/RegisterModal.tsx create mode 100644 Frontend/src/components/modals/ResetPasswordModal.tsx create mode 100644 Frontend/src/contexts/AuthModalContext.tsx create mode 100644 Frontend/src/pages/NotFoundPage.tsx delete mode 100644 Frontend/src/pages/auth/ForgotPasswordPage.tsx delete mode 100644 Frontend/src/pages/auth/LoginPage.tsx delete mode 100644 Frontend/src/pages/auth/RegisterPage.tsx delete mode 100644 Frontend/src/pages/auth/ResetPasswordPage.tsx diff --git a/Frontend/src/App.tsx b/Frontend/src/App.tsx index a7973178..1c5cb227 100644 --- a/Frontend/src/App.tsx +++ b/Frontend/src/App.tsx @@ -11,11 +11,14 @@ import { GlobalLoadingProvider } from './contexts/GlobalLoadingContext'; import { CookieConsentProvider } from './contexts/CookieConsentContext'; import { CurrencyProvider } from './contexts/CurrencyContext'; import { CompanySettingsProvider } from './contexts/CompanySettingsContext'; +import { AuthModalProvider } from './contexts/AuthModalContext'; import OfflineIndicator from './components/common/OfflineIndicator'; import CookieConsentBanner from './components/common/CookieConsentBanner'; import AnalyticsLoader from './components/common/AnalyticsLoader'; import Loading from './components/common/Loading'; import ScrollToTop from './components/common/ScrollToTop'; +import AuthModalManager from './components/modals/AuthModalManager'; +import ResetPasswordRouteHandler from './components/ResetPasswordRouteHandler'; import useAuthStore from './store/useAuthStore'; import useFavoritesStore from './store/useFavoritesStore'; @@ -50,10 +53,6 @@ const InvoicePage = lazy(() => import('./pages/customer/InvoicePage')); const ProfilePage = lazy(() => import('./pages/customer/ProfilePage')); const AboutPage = lazy(() => import('./pages/AboutPage')); const ContactPage = lazy(() => import('./pages/ContactPage')); -const LoginPage = lazy(() => import('./pages/auth/LoginPage')); -const RegisterPage = lazy(() => import('./pages/auth/RegisterPage')); -const ForgotPasswordPage = lazy(() => import('./pages/auth/ForgotPasswordPage')); -const ResetPasswordPage = lazy(() => import('./pages/auth/ResetPasswordPage')); const AdminDashboardPage = lazy(() => import('./pages/admin/DashboardPage')); const InvoiceManagementPage = lazy(() => import('./pages/admin/InvoiceManagementPage')); @@ -69,17 +68,7 @@ const ReceptionDashboardPage = lazy(() => import('./pages/admin/ReceptionDashboa const StaffDashboardPage = lazy(() => import('./pages/staff/DashboardPage')); const ChatManagementPage = lazy(() => import('./pages/staff/ChatManagementPage')); const StaffLayout = lazy(() => import('./pages/StaffLayout')); - -const DemoPage: React.FC<{ title: string }> = ({ title }) => ( -
-

- {title} -

-

- This page is under development... -

-
-); +const NotFoundPage = lazy(() => import('./pages/NotFoundPage')); function App() { @@ -129,6 +118,7 @@ function App() { + {} - } - /> - } - /> - } - /> } + element={} /> {} @@ -383,7 +361,7 @@ function App() { {} } + element={} /> @@ -404,8 +382,10 @@ function App() { + + diff --git a/Frontend/src/components/ResetPasswordRouteHandler.tsx b/Frontend/src/components/ResetPasswordRouteHandler.tsx new file mode 100644 index 00000000..71dae119 --- /dev/null +++ b/Frontend/src/components/ResetPasswordRouteHandler.tsx @@ -0,0 +1,22 @@ +import { useEffect } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; +import { useAuthModal } from '../contexts/AuthModalContext'; + +const ResetPasswordRouteHandler: React.FC = () => { + const { token } = useParams<{ token: string }>(); + const navigate = useNavigate(); + const { openModal } = useAuthModal(); + + useEffect(() => { + if (token) { + openModal('reset-password', { token }); + // Navigate to home to keep user on current page + navigate('/', { replace: true }); + } + }, [token, openModal, navigate]); + + return null; +}; + +export default ResetPasswordRouteHandler; + diff --git a/Frontend/src/components/auth/AdminRoute.tsx b/Frontend/src/components/auth/AdminRoute.tsx index 8cc3b20a..66a05696 100644 --- a/Frontend/src/components/auth/AdminRoute.tsx +++ b/Frontend/src/components/auth/AdminRoute.tsx @@ -1,6 +1,7 @@ -import React from 'react'; -import { Navigate, useLocation } from 'react-router-dom'; +import React, { useEffect } from 'react'; +import { Navigate } from 'react-router-dom'; import useAuthStore from '../../store/useAuthStore'; +import { useAuthModal } from '../../contexts/AuthModalContext'; interface AdminRouteProps { children: React.ReactNode; @@ -9,10 +10,16 @@ interface AdminRouteProps { const AdminRoute: React.FC = ({ children }) => { - const location = useLocation(); const { isAuthenticated, userInfo, isLoading } = useAuthStore(); + const { openModal } = useAuthModal(); + useEffect(() => { + if (!isLoading && !isAuthenticated) { + openModal('login'); + } + }, [isLoading, isAuthenticated, openModal]); + if (isLoading) { return (
= ({ if (!isAuthenticated) { - return ( - - ); + return null; // Modal will be shown by AuthModalManager } diff --git a/Frontend/src/components/auth/CustomerRoute.tsx b/Frontend/src/components/auth/CustomerRoute.tsx index c84ba443..f461d259 100644 --- a/Frontend/src/components/auth/CustomerRoute.tsx +++ b/Frontend/src/components/auth/CustomerRoute.tsx @@ -1,6 +1,7 @@ -import React from 'react'; -import { Navigate, useLocation } from 'react-router-dom'; +import React, { useEffect } from 'react'; +import { Navigate } from 'react-router-dom'; import useAuthStore from '../../store/useAuthStore'; +import { useAuthModal } from '../../contexts/AuthModalContext'; interface CustomerRouteProps { children: React.ReactNode; @@ -9,10 +10,16 @@ interface CustomerRouteProps { const CustomerRoute: React.FC = ({ children }) => { - const location = useLocation(); const { isAuthenticated, userInfo, isLoading } = useAuthStore(); + const { openModal } = useAuthModal(); + useEffect(() => { + if (!isLoading && !isAuthenticated) { + openModal('login'); + } + }, [isLoading, isAuthenticated, openModal]); + if (isLoading) { return (
= ({ if (!isAuthenticated) { - return ( - - ); + return null; // Modal will be shown by AuthModalManager } diff --git a/Frontend/src/components/auth/ProtectedRoute.tsx b/Frontend/src/components/auth/ProtectedRoute.tsx index 2538cadd..cf2ee9ad 100644 --- a/Frontend/src/components/auth/ProtectedRoute.tsx +++ b/Frontend/src/components/auth/ProtectedRoute.tsx @@ -1,6 +1,6 @@ -import React from 'react'; -import { Navigate, useLocation } from 'react-router-dom'; +import React, { useEffect } from 'react'; import useAuthStore from '../../store/useAuthStore'; +import { useAuthModal } from '../../contexts/AuthModalContext'; interface ProtectedRouteProps { children: React.ReactNode; @@ -9,8 +9,15 @@ interface ProtectedRouteProps { const ProtectedRoute: React.FC = ({ children }) => { - const location = useLocation(); const { isAuthenticated, isLoading } = useAuthStore(); + const { openModal } = useAuthModal(); + + + useEffect(() => { + if (!isLoading && !isAuthenticated) { + openModal('login'); + } + }, [isLoading, isAuthenticated, openModal]); if (isLoading) { @@ -34,13 +41,7 @@ const ProtectedRoute: React.FC = ({ if (!isAuthenticated) { - return ( - - ); + return null; // Modal will be shown by AuthModalManager } return <>{children}; diff --git a/Frontend/src/components/auth/StaffRoute.tsx b/Frontend/src/components/auth/StaffRoute.tsx index 710f7cf2..7f8eef5b 100644 --- a/Frontend/src/components/auth/StaffRoute.tsx +++ b/Frontend/src/components/auth/StaffRoute.tsx @@ -1,6 +1,7 @@ -import React from 'react'; -import { Navigate, useLocation } from 'react-router-dom'; +import React, { useEffect } from 'react'; +import { Navigate } from 'react-router-dom'; import useAuthStore from '../../store/useAuthStore'; +import { useAuthModal } from '../../contexts/AuthModalContext'; interface StaffRouteProps { children: React.ReactNode; @@ -9,10 +10,16 @@ interface StaffRouteProps { const StaffRoute: React.FC = ({ children }) => { - const location = useLocation(); const { isAuthenticated, userInfo, isLoading } = useAuthStore(); + const { openModal } = useAuthModal(); + useEffect(() => { + if (!isLoading && !isAuthenticated) { + openModal('login'); + } + }, [isLoading, isAuthenticated, openModal]); + if (isLoading) { return (
= ({ if (!isAuthenticated) { - return ( - - ); + return null; // Modal will be shown by AuthModalManager } diff --git a/Frontend/src/components/layout/Header.tsx b/Frontend/src/components/layout/Header.tsx index 8d14c3fb..04b0ea45 100644 --- a/Frontend/src/components/layout/Header.tsx +++ b/Frontend/src/components/layout/Header.tsx @@ -15,6 +15,7 @@ import { } from 'lucide-react'; import { useClickOutside } from '../../hooks/useClickOutside'; import { useCompanySettings } from '../../contexts/CompanySettingsContext'; +import { useAuthModal } from '../../contexts/AuthModalContext'; import { normalizeImageUrl } from '../../utils/imageUtils'; interface HeaderProps { @@ -34,6 +35,7 @@ const Header: React.FC = ({ onLogout }) => { const { settings } = useCompanySettings(); + const { openModal } = useAuthModal(); const displayPhone = settings.company_phone || '+1 (234) 567-890'; @@ -174,8 +176,8 @@ const Header: React.FC = ({ > {!isAuthenticated ? ( <> - openModal('login')} className="flex items-center space-x-2 px-5 py-2 text-white/90 hover:text-[#d4af37] transition-all duration-300 @@ -184,9 +186,9 @@ const Header: React.FC = ({ Login - - + ) : (
@@ -402,37 +404,37 @@ const Header: React.FC = ({ > {!isAuthenticated ? ( <> - - setIsMobileMenuOpen(false) - } + + ) : ( <> diff --git a/Frontend/src/components/layout/SidebarAdmin.tsx b/Frontend/src/components/layout/SidebarAdmin.tsx index d4b2a42c..7f6d5219 100644 --- a/Frontend/src/components/layout/SidebarAdmin.tsx +++ b/Frontend/src/components/layout/SidebarAdmin.tsx @@ -35,7 +35,7 @@ const SidebarAdmin: React.FC = ({ const handleLogout = async () => { try { await logout(); - navigate('/login'); + navigate('/'); if (isMobile) { setIsMobileOpen(false); } diff --git a/Frontend/src/components/layout/SidebarStaff.tsx b/Frontend/src/components/layout/SidebarStaff.tsx index 373c6f7d..b20caca2 100644 --- a/Frontend/src/components/layout/SidebarStaff.tsx +++ b/Frontend/src/components/layout/SidebarStaff.tsx @@ -36,7 +36,7 @@ const SidebarStaff: React.FC = ({ const handleLogout = async () => { try { await logout(); - navigate('/login'); + navigate('/'); if (isMobile) { setIsMobileOpen(false); } diff --git a/Frontend/src/components/modals/AuthModalManager.tsx b/Frontend/src/components/modals/AuthModalManager.tsx new file mode 100644 index 00000000..3d79942b --- /dev/null +++ b/Frontend/src/components/modals/AuthModalManager.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { useAuthModal } from '../../contexts/AuthModalContext'; +import LoginModal from './LoginModal'; +import RegisterModal from './RegisterModal'; +import ForgotPasswordModal from './ForgotPasswordModal'; +import ResetPasswordModal from './ResetPasswordModal'; + +const AuthModalManager: React.FC = () => { + const { isOpen, modalType, resetPasswordParams } = useAuthModal(); + + if (!isOpen) { + return null; + } + + switch (modalType) { + case 'login': + return ; + case 'register': + return ; + case 'forgot-password': + return ; + case 'reset-password': + return resetPasswordParams?.token ? : null; + default: + return null; + } +}; + +export default AuthModalManager; + diff --git a/Frontend/src/components/modals/ForgotPasswordModal.tsx b/Frontend/src/components/modals/ForgotPasswordModal.tsx new file mode 100644 index 00000000..548d78db --- /dev/null +++ b/Frontend/src/components/modals/ForgotPasswordModal.tsx @@ -0,0 +1,277 @@ +import React, { useState, useEffect } from 'react'; +import { useForm } from 'react-hook-form'; +import { yupResolver } from '@hookform/resolvers/yup'; +import { X, Mail, ArrowLeft, Send, Loader2, CheckCircle } from 'lucide-react'; +import useAuthStore from '../../store/useAuthStore'; +import { forgotPasswordSchema, ForgotPasswordFormData } from '../../utils/validationSchemas'; +import { useCompanySettings } from '../../contexts/CompanySettingsContext'; +import { useAuthModal } from '../../contexts/AuthModalContext'; + +const ForgotPasswordModal: React.FC = () => { + const { closeModal, openModal } = useAuthModal(); + const { forgotPassword, isLoading, error, clearError } = useAuthStore(); + const { settings } = useCompanySettings(); + + const [isSuccess, setIsSuccess] = useState(false); + const [submittedEmail, setSubmittedEmail] = useState(''); + + const supportEmail = settings.company_email || 'support@hotel.com'; + const supportPhone = settings.company_phone || '1900-xxxx'; + + const { + register, + handleSubmit, + formState: { errors }, + } = useForm({ + resolver: yupResolver(forgotPasswordSchema), + defaultValues: { + email: '', + }, + }); + + const onSubmit = async (data: ForgotPasswordFormData) => { + try { + clearError(); + setSubmittedEmail(data.email); + await forgotPassword({ email: data.email }); + setIsSuccess(true); + } catch (error) { + console.error('Forgot password error:', error); + } + }; + + // Handle escape key + useEffect(() => { + const handleEscape = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + closeModal(); + } + }; + document.addEventListener('keydown', handleEscape); + return () => document.removeEventListener('keydown', handleEscape); + }, [closeModal]); + + return ( +
{ + if (e.target === e.currentTarget) { + closeModal(); + } + }} + > + {/* Backdrop */} +
+ + {/* Modal */} +
+ {/* Close button */} + + +
+ {/* Header */} +
+
+ {settings.company_logo_url ? ( + {settings.company_name + ) : ( +
+ +
+ )} +
+ {settings.company_tagline && ( +

+ {settings.company_tagline} +

+ )} +

+ Forgot Password? +

+

+ Enter your email to receive a password reset link for {settings.company_name || 'Luxury Hotel'} +

+
+ + {isSuccess ? ( +
+
+
+ +
+
+ +
+

+ Email Sent! +

+

+ We have sent a password reset link to +

+

+ {submittedEmail} +

+
+ +
+

+ Note: +

+
    +
  • Link is valid for 1 hour
  • +
  • Check your Spam/Junk folder
  • +
  • If you don't receive the email, please try again
  • +
+
+ +
+ + + +
+
+ ) : ( +
+ {error && ( +
+ {error} +
+ )} + +
+ +
+
+ +
+ +
+ {errors.email && ( +

+ {errors.email.message} +

+ )} +
+ + + +
+ +
+
+ )} + + {!isSuccess && ( +
+

+ Don't have an account?{' '} + +

+
+ )} + + {!isSuccess && ( +
+

+ Need Help? +

+

+ If you're having trouble resetting your password, please contact our support team via email{' '} + + {supportEmail} + + {supportPhone && ( + <> + {' '}or hotline{' '} + + {supportPhone} + + + )} +

+
+ )} +
+
+
+ ); +}; + +export default ForgotPasswordModal; + diff --git a/Frontend/src/components/modals/LoginModal.tsx b/Frontend/src/components/modals/LoginModal.tsx new file mode 100644 index 00000000..e550ea53 --- /dev/null +++ b/Frontend/src/components/modals/LoginModal.tsx @@ -0,0 +1,390 @@ +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 } from 'lucide-react'; +import useAuthStore from '../../store/useAuthStore'; +import { loginSchema, LoginFormData } from '../../utils/validationSchemas'; +import { useCompanySettings } from '../../contexts/CompanySettingsContext'; +import { useAuthModal } from '../../contexts/AuthModalContext'; +import * as yup from 'yup'; +import { toast } from 'react-toastify'; +import Recaptcha from '../common/Recaptcha'; +import { recaptchaService } from '../../services/api/systemSettingsService'; + +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 LoginModal: React.FC = () => { + const { closeModal, openModal } = useAuthModal(); + const { login, verifyMFA, isLoading, error, clearError, requiresMFA, clearMFA, isAuthenticated } = useAuthStore(); + const { settings } = useCompanySettings(); + + const [showPassword, setShowPassword] = useState(false); + const [recaptchaToken, setRecaptchaToken] = useState(null); + + const { + register: registerMFA, + handleSubmit: handleSubmitMFA, + formState: { errors: mfaErrors }, + } = useForm({ + resolver: yupResolver(mfaTokenSchema), + defaultValues: { + mfaToken: '', + }, + }); + + // Close modal on successful authentication + useEffect(() => { + if (!isLoading && isAuthenticated && !requiresMFA) { + closeModal(); + } + }, [isLoading, isAuthenticated, requiresMFA, closeModal]); + + const { + register, + handleSubmit, + formState: { errors }, + } = useForm({ + resolver: yupResolver(loginSchema), + defaultValues: { + email: '', + password: '', + rememberMe: false, + }, + }); + + const onSubmit = async (data: LoginFormData) => { + try { + clearError(); + + 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; + } + } + + await login({ + email: data.email, + password: data.password, + rememberMe: data.rememberMe, + }); + + setRecaptchaToken(null); + } catch (error) { + console.error('Login error:', error); + setRecaptchaToken(null); + } + }; + + const onSubmitMFA = async (data: MFATokenFormData) => { + try { + clearError(); + await verifyMFA(data.mfaToken); + } catch (error) { + console.error('MFA verification error:', error); + } + }; + + const handleBackToLogin = () => { + clearMFA(); + clearError(); + }; + + // Handle escape key + useEffect(() => { + const handleEscape = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + closeModal(); + } + }; + document.addEventListener('keydown', handleEscape); + return () => document.removeEventListener('keydown', handleEscape); + }, [closeModal]); + + return ( +
{ + if (e.target === e.currentTarget) { + closeModal(); + } + }} + > + {/* Backdrop */} +
+ + {/* Modal */} +
+ {/* Close button */} + + +
+ {/* Header */} +
+
+ {settings.company_logo_url ? ( + {settings.company_name + ) : ( +
+ +
+ )} +
+ {settings.company_tagline && ( +

+ {settings.company_tagline} +

+ )} +

+ {requiresMFA ? 'Verify Your Identity' : 'Welcome Back'} +

+

+ {requiresMFA + ? 'Enter the 6-digit code from your authenticator app' + : `Sign in to ${settings.company_name || 'Luxury Hotel'}`} +

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

+ {mfaErrors.mfaToken.message} +

+ )} +

+ Enter the 6-digit code from your authenticator app or an 8-character backup code +

+
+ + + +
+ +
+
+ ) : ( +
+ {error && ( +
+ {error} +
+ )} + +
+ +
+
+ +
+ +
+ {errors.email && ( +

+ {errors.email.message} +

+ )} +
+ +
+ +
+
+ +
+ + +
+ {errors.password && ( +

+ {errors.password.message} +

+ )} +
+ +
+
+ + +
+ + +
+ +
+ setRecaptchaToken(token)} + onError={(error) => { + console.error('reCAPTCHA error:', error); + setRecaptchaToken(null); + }} + theme="light" + size="normal" + /> +
+ + +
+ )} + + {!requiresMFA && ( +
+

+ Don't have an account?{' '} + +

+
+ )} +
+
+
+ ); +}; + +export default LoginModal; + diff --git a/Frontend/src/components/modals/RegisterModal.tsx b/Frontend/src/components/modals/RegisterModal.tsx new file mode 100644 index 00000000..90307865 --- /dev/null +++ b/Frontend/src/components/modals/RegisterModal.tsx @@ -0,0 +1,419 @@ +import React, { useState, useEffect } from 'react'; +import { useForm } from 'react-hook-form'; +import { yupResolver } from '@hookform/resolvers/yup'; +import { X, Eye, EyeOff, UserPlus, Loader2, Mail, Lock, User, Phone, CheckCircle2, XCircle } from 'lucide-react'; +import useAuthStore from '../../store/useAuthStore'; +import { registerSchema, RegisterFormData } from '../../utils/validationSchemas'; +import { useCompanySettings } from '../../contexts/CompanySettingsContext'; +import { useAuthModal } from '../../contexts/AuthModalContext'; +import { toast } from 'react-toastify'; +import Recaptcha from '../common/Recaptcha'; +import { recaptchaService } from '../../services/api/systemSettingsService'; + +const PasswordRequirement: React.FC<{ met: boolean; text: string }> = ({ met, text }) => ( +
+ {met ? ( + + ) : ( + + )} + + {text} + +
+); + +const RegisterModal: React.FC = () => { + const { closeModal, openModal } = useAuthModal(); + const { register: registerUser, isLoading, error, clearError, isAuthenticated } = useAuthStore(); + const { settings } = useCompanySettings(); + + const [showPassword, setShowPassword] = useState(false); + const [showConfirmPassword, setShowConfirmPassword] = useState(false); + const [recaptchaToken, setRecaptchaToken] = useState(null); + + useEffect(() => { + if (!isLoading && isAuthenticated) { + closeModal(); + } + }, [isLoading, isAuthenticated, closeModal]); + + const { + register, + handleSubmit, + watch, + formState: { errors }, + } = useForm({ + resolver: yupResolver(registerSchema), + defaultValues: { + name: '', + email: '', + password: '', + confirmPassword: '', + phone: '', + }, + }); + + const password = watch('password'); + + const getPasswordStrength = (pwd: string) => { + if (!pwd) return { strength: 0, label: '', color: '' }; + + let strength = 0; + if (pwd.length >= 8) strength++; + if (/[a-z]/.test(pwd)) strength++; + if (/[A-Z]/.test(pwd)) strength++; + if (/\d/.test(pwd)) strength++; + if (/[@$!%*?&]/.test(pwd)) strength++; + + const labels = [ + { label: 'Very Weak', color: 'bg-red-500' }, + { label: 'Weak', color: 'bg-orange-500' }, + { label: 'Medium', color: 'bg-yellow-500' }, + { label: 'Strong', color: 'bg-blue-500' }, + { label: 'Very Strong', color: 'bg-green-500' }, + ]; + + return { strength, ...labels[strength] }; + }; + + const passwordStrength = getPasswordStrength(password || ''); + + const onSubmit = async (data: RegisterFormData) => { + try { + clearError(); + + 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; + } + } + + await registerUser({ + name: data.name, + email: data.email, + password: data.password, + phone: data.phone, + }); + + setRecaptchaToken(null); + } catch (error) { + console.error('Register error:', error); + setRecaptchaToken(null); + } + }; + + // Handle escape key + useEffect(() => { + const handleEscape = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + closeModal(); + } + }; + document.addEventListener('keydown', handleEscape); + return () => document.removeEventListener('keydown', handleEscape); + }, [closeModal]); + + return ( +
{ + if (e.target === e.currentTarget) { + closeModal(); + } + }} + > + {/* Backdrop */} +
+ + {/* Modal */} +
+ {/* Close button */} + + +
+ {/* Header */} +
+
+ {settings.company_logo_url ? ( + {settings.company_name + ) : ( +
+ +
+ )} +
+ {settings.company_tagline && ( +

+ {settings.company_tagline} +

+ )} +

+ Create Account +

+

+ Join {settings.company_name || 'Luxury Hotel'} for exclusive benefits +

+
+ +
+ {error && ( +
+ {error} +
+ )} + +
+ +
+
+ +
+ +
+ {errors.name && ( +

+ {errors.name.message} +

+ )} +
+ +
+ +
+
+ +
+ +
+ {errors.email && ( +

+ {errors.email.message} +

+ )} +
+ +
+ +
+
+ +
+ +
+ {errors.phone && ( +

+ {errors.phone.message} +

+ )} +
+ +
+ +
+
+ +
+ + +
+ {errors.password && ( +

+ {errors.password.message} +

+ )} + + {password && password.length > 0 && ( +
+
+
+
= 4 + ? 'bg-[#d4af37]' + : passwordStrength.strength >= 3 + ? 'bg-yellow-500' + : 'bg-red-500' + }`} + style={{ width: `${(passwordStrength.strength / 5) * 100}%` }} + /> +
+ + {passwordStrength.label} + +
+ +
+ = 8} text="At least 8 characters" /> + + + + +
+
+ )} +
+ +
+ +
+
+ +
+ + +
+ {errors.confirmPassword && ( +

+ {errors.confirmPassword.message} +

+ )} +
+ +
+ setRecaptchaToken(token)} + onError={(error) => { + console.error('reCAPTCHA error:', error); + setRecaptchaToken(null); + }} + theme="light" + size="normal" + /> +
+ + + + +
+

+ Already have an account?{' '} + +

+
+
+
+
+ ); +}; + +export default RegisterModal; + diff --git a/Frontend/src/components/modals/ResetPasswordModal.tsx b/Frontend/src/components/modals/ResetPasswordModal.tsx new file mode 100644 index 00000000..7ab24a7c --- /dev/null +++ b/Frontend/src/components/modals/ResetPasswordModal.tsx @@ -0,0 +1,390 @@ +import React, { useState, useEffect } from 'react'; +import { useForm } from 'react-hook-form'; +import { yupResolver } from '@hookform/resolvers/yup'; +import { X, Eye, EyeOff, Lock, Loader2, CheckCircle2, XCircle, AlertCircle, KeyRound } from 'lucide-react'; +import useAuthStore from '../../store/useAuthStore'; +import { resetPasswordSchema, ResetPasswordFormData } from '../../utils/validationSchemas'; +import { useCompanySettings } from '../../contexts/CompanySettingsContext'; +import { useAuthModal } from '../../contexts/AuthModalContext'; + +const PasswordRequirement: React.FC<{ met: boolean; text: string }> = ({ met, text }) => ( +
+ {met ? ( + + ) : ( + + )} + + {text} + +
+); + +interface ResetPasswordModalProps { + token: string; +} + +const ResetPasswordModal: React.FC = ({ token }) => { + const { closeModal, openModal } = useAuthModal(); + const { resetPassword, isLoading, error, clearError, isAuthenticated } = useAuthStore(); + const { settings } = useCompanySettings(); + + const [showPassword, setShowPassword] = useState(false); + const [showConfirmPassword, setShowConfirmPassword] = useState(false); + const [isSuccess, setIsSuccess] = useState(false); + + // Close modal on successful reset + useEffect(() => { + if (!isLoading && isAuthenticated) { + setTimeout(() => { + closeModal(); + openModal('login'); + }, 2000); + } + }, [isLoading, isAuthenticated, closeModal, openModal]); + + const { + register, + handleSubmit, + watch, + formState: { errors }, + } = useForm({ + resolver: yupResolver(resetPasswordSchema), + defaultValues: { + password: '', + confirmPassword: '', + }, + }); + + const password = watch('password'); + + const getPasswordStrength = (pwd: string) => { + if (!pwd) return { strength: 0, label: '', color: '' }; + + let strength = 0; + if (pwd.length >= 8) strength++; + if (/[a-z]/.test(pwd)) strength++; + if (/[A-Z]/.test(pwd)) strength++; + if (/\d/.test(pwd)) strength++; + if (/[@$!%*?&]/.test(pwd)) strength++; + + const labels = [ + { label: 'Very Weak', color: 'bg-red-500' }, + { label: 'Weak', color: 'bg-orange-500' }, + { label: 'Medium', color: 'bg-yellow-500' }, + { label: 'Strong', color: 'bg-blue-500' }, + { label: 'Very Strong', color: 'bg-green-500' }, + ]; + + return { strength, ...labels[strength] }; + }; + + const passwordStrength = getPasswordStrength(password || ''); + + const onSubmit = async (data: ResetPasswordFormData) => { + if (!token) { + return; + } + + try { + clearError(); + await resetPassword({ + token, + password: data.password, + confirmPassword: data.confirmPassword, + }); + setIsSuccess(true); + } catch (error) { + console.error('Reset password error:', error); + } + }; + + const isTokenError = error?.includes('token') || error?.includes('expired'); + const isReuseError = error?.toLowerCase().includes('must be different') || error?.toLowerCase().includes('different from old'); + + // Handle escape key + useEffect(() => { + const handleEscape = (e: KeyboardEvent) => { + if (e.key === 'Escape' && !isSuccess) { + closeModal(); + } + }; + document.addEventListener('keydown', handleEscape); + return () => document.removeEventListener('keydown', handleEscape); + }, [closeModal, isSuccess]); + + if (!token) { + return null; + } + + return ( +
{ + if (e.target === e.currentTarget && !isSuccess) { + closeModal(); + } + }} + > + {/* Backdrop */} +
+ + {/* Modal */} +
+ {/* Close button */} + {!isSuccess && ( + + )} + +
+ {/* Header */} +
+
+ {settings.company_logo_url ? ( + {settings.company_name + ) : ( +
+ +
+ )} +
+ {settings.company_tagline && ( +

+ {settings.company_tagline} +

+ )} +

+ {isSuccess ? 'Complete!' : 'Reset Password'} +

+

+ {isSuccess + ? 'Password has been reset successfully' + : `Enter a new password for your ${settings.company_name || 'Luxury Hotel'} account`} +

+
+ + {isSuccess ? ( +
+
+
+ +
+
+ +
+

+ Password reset successful! +

+

+ Your password has been updated. +

+

+ You can now login with your new password. +

+
+ +
+

+ Redirecting to login... +

+
+ +
+
+ + +
+ ) : ( +
+ {error && ( +
+ +
+

+ {isReuseError + ? 'New password must be different from old password' + : error} +

+ {isTokenError && ( + + )} +
+
+ )} + +
+ +
+
+ +
+ + +
+ {errors.password && ( +

+ {errors.password.message} +

+ )} + + {password && password.length > 0 && ( +
+
+
+
+
+ + {passwordStrength.label} + +
+ +
+ = 8} text="At least 8 characters" /> + + + + +
+
+ )} +
+ +
+ +
+
+ +
+ + +
+ {errors.confirmPassword && ( +

+ {errors.confirmPassword.message} +

+ )} +
+ + + +
+ +
+ + )} +
+
+
+ ); +}; + +export default ResetPasswordModal; + diff --git a/Frontend/src/contexts/AuthModalContext.tsx b/Frontend/src/contexts/AuthModalContext.tsx new file mode 100644 index 00000000..0aa2cc4e --- /dev/null +++ b/Frontend/src/contexts/AuthModalContext.tsx @@ -0,0 +1,57 @@ +import React, { createContext, useContext, useState, useCallback } from 'react'; + +type AuthModalType = 'login' | 'register' | 'forgot-password' | 'reset-password' | null; +type ResetPasswordParams = { token: string } | null; + +interface AuthModalContextType { + isOpen: boolean; + modalType: AuthModalType; + resetPasswordParams: ResetPasswordParams; + openModal: (type: AuthModalType, params?: ResetPasswordParams) => void; + closeModal: () => void; +} + +const AuthModalContext = createContext(undefined); + +export const AuthModalProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [isOpen, setIsOpen] = useState(false); + const [modalType, setModalType] = useState(null); + const [resetPasswordParams, setResetPasswordParams] = useState(null); + + const openModal = useCallback((type: AuthModalType, params?: ResetPasswordParams) => { + setModalType(type); + setResetPasswordParams(params || null); + setIsOpen(true); + document.body.style.overflow = 'hidden'; + }, []); + + const closeModal = useCallback(() => { + setIsOpen(false); + setModalType(null); + setResetPasswordParams(null); + document.body.style.overflow = 'unset'; + }, []); + + return ( + + {children} + + ); +}; + +export const useAuthModal = () => { + const context = useContext(AuthModalContext); + if (context === undefined) { + throw new Error('useAuthModal must be used within an AuthModalProvider'); + } + return context; +}; + diff --git a/Frontend/src/pages/NotFoundPage.tsx b/Frontend/src/pages/NotFoundPage.tsx new file mode 100644 index 00000000..1726a5c3 --- /dev/null +++ b/Frontend/src/pages/NotFoundPage.tsx @@ -0,0 +1,101 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import { Home, Hotel, AlertCircle } from 'lucide-react'; +import { useCompanySettings } from '../contexts/CompanySettingsContext'; + +const NotFoundPage: React.FC = () => { + const { settings } = useCompanySettings(); + + return ( +
+ {/* Background Pattern */} +
+ +
+ {/* Logo */} +
+ {settings.company_logo_url ? ( + {settings.company_name + ) : ( +
+ +
+
+ )} +
+ + {/* Tagline */} + {settings.company_tagline && ( +

+ {settings.company_tagline} +

+ )} + + {/* 404 Number */} +
+

+ 404 +

+
+ + {/* Error Icon */} +
+
+ +
+
+ + {/* Main Message */} +
+

+ Page Not Found +

+

+ We're sorry, but the page you're looking for doesn't exist or has been moved. +

+
+ + {/* Additional Info */} +
+

+ The page you requested could not be found on our server. +

+
+ + {/* Home Button */} +
+ + + + Back to Homepage + +
+ + {/* Decorative Elements */} +
+
+
+
+
+
+
+ ); +}; + +export default NotFoundPage; + diff --git a/Frontend/src/pages/admin/DashboardPage.tsx b/Frontend/src/pages/admin/DashboardPage.tsx index 6d19c8e5..c9cab694 100644 --- a/Frontend/src/pages/admin/DashboardPage.tsx +++ b/Frontend/src/pages/admin/DashboardPage.tsx @@ -34,7 +34,7 @@ const DashboardPage: React.FC = () => { const handleLogout = async () => { try { await logout(); - navigate('/login'); + navigate('/'); } catch (error) { console.error('Logout error:', error); } diff --git a/Frontend/src/pages/auth/ForgotPasswordPage.tsx b/Frontend/src/pages/auth/ForgotPasswordPage.tsx deleted file mode 100644 index 62a75f51..00000000 --- a/Frontend/src/pages/auth/ForgotPasswordPage.tsx +++ /dev/null @@ -1,385 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { useForm } from 'react-hook-form'; -import { yupResolver } from '@hookform/resolvers/yup'; -import { Link } from 'react-router-dom'; -import { - Mail, - ArrowLeft, - Send, - Loader2, - CheckCircle, - Hotel, - Home, -} from 'lucide-react'; -import useAuthStore from '../../store/useAuthStore'; -import { - forgotPasswordSchema, - ForgotPasswordFormData, -} from '../../utils/validationSchemas'; -import { useCompanySettings } from '../../contexts/CompanySettingsContext'; - -const ForgotPasswordPage: React.FC = () => { - const { forgotPassword, isLoading, error, clearError } = - useAuthStore(); - const { settings } = useCompanySettings(); - - const [isSuccess, setIsSuccess] = useState(false); - const [submittedEmail, setSubmittedEmail] = useState(''); - - - const supportEmail = settings.company_email || 'support@hotel.com'; - const supportPhone = settings.company_phone || '1900-xxxx'; - - - useEffect(() => { - const companyName = settings.company_name || 'Luxury Hotel'; - document.title = `Forgot Password - ${companyName}`; - }, [settings.company_name]); - - - const { - register, - handleSubmit, - formState: { errors }, - } = useForm({ - resolver: yupResolver(forgotPasswordSchema), - defaultValues: { - email: '', - }, - }); - - - const onSubmit = async (data: ForgotPasswordFormData) => { - try { - clearError(); - setSubmittedEmail(data.email); - await forgotPassword({ email: data.email }); - - - setIsSuccess(true); - } catch (error) { - - console.error('Forgot password error:', error); - } - }; - - return ( -
-
- {} -
-
- {settings.company_logo_url ? ( - {settings.company_name - ) : ( -
- -
- )} -
- {settings.company_tagline && ( -

- {settings.company_tagline} -

- )} -

- Forgot Password? -

-

- Enter your email to receive a password reset link for {settings.company_name || 'Luxury Hotel'} -

-
- - {} -
- {isSuccess ? ( - -
-
-
- -
-
- -
-

- Email Sent! -

-

- We have sent a password reset link to -

-

- {submittedEmail} -

-
- -
-

- Note: -

-
    -
  • Link is valid for 1 hour
  • -
  • Check your Spam/Junk folder
  • -
  • - If you don't receive the email, please try again -
  • -
-
- -
- - - - - Back to Login - -
- - {} -
- - - - Back to Homepage - -
-
- ) : ( - -
- {} - {error && ( -
- {error} -
- )} - - {} -
- -
-
- -
- -
- {errors.email && ( -

- {errors.email.message} -

- )} -
- - {} - - - {} -
- - - Back to Login - -
- - {} -
- - - - Back to Homepage - -
-
- )} -
- - {} - {!isSuccess && ( -
-

- Don't have an account?{' '} - - Register now - -

-
- )} - - {} -
-

- Need Help? -

-

- If you're having trouble resetting your password, - please contact our support team via email{' '} - - {supportEmail} - - {supportPhone && ( - <> - {' '}or hotline{' '} - - {supportPhone} - - - )} -

-
-
-
- ); -}; - -export default ForgotPasswordPage; diff --git a/Frontend/src/pages/auth/LoginPage.tsx b/Frontend/src/pages/auth/LoginPage.tsx deleted file mode 100644 index 2a7f44f2..00000000 --- a/Frontend/src/pages/auth/LoginPage.tsx +++ /dev/null @@ -1,540 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { useForm } from 'react-hook-form'; -import { yupResolver } from '@hookform/resolvers/yup'; -import { Link, useNavigate, useLocation } from 'react-router-dom'; -import { - Eye, - EyeOff, - LogIn, - Loader2, - Mail, - Lock, - Hotel, - Home, - Shield, - ArrowLeft -} from 'lucide-react'; -import useAuthStore from '../../store/useAuthStore'; -import { - loginSchema, - LoginFormData -} from '../../utils/validationSchemas'; -import { useCompanySettings } from '../../contexts/CompanySettingsContext'; -import * as yup from 'yup'; -import { toast } from 'react-toastify'; -import Recaptcha from '../../components/common/Recaptcha'; -import { recaptchaService } from '../../services/api/systemSettingsService'; - -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 LoginPage: React.FC = () => { - const navigate = useNavigate(); - const location = useLocation(); - const { login, verifyMFA, isLoading, error, clearError, requiresMFA, clearMFA, userInfo, isAuthenticated } = - useAuthStore(); - const { settings } = useCompanySettings(); - - const [showPassword, setShowPassword] = useState(false); - const [recaptchaToken, setRecaptchaToken] = useState(null); - - - const { - register: registerMFA, - handleSubmit: handleSubmitMFA, - formState: { errors: mfaErrors }, - } = useForm({ - resolver: yupResolver(mfaTokenSchema), - defaultValues: { - mfaToken: '', - }, - }); - - - useEffect(() => { - if (!isLoading && isAuthenticated && !requiresMFA && userInfo) { - if (userInfo.role === 'admin') { - navigate('/admin/dashboard', { replace: true }); - } else if (userInfo.role === 'staff') { - navigate('/staff/dashboard', { replace: true }); - } else { - navigate('/', { replace: true }); - } - } - }, [isLoading, isAuthenticated, requiresMFA, userInfo, navigate]); - - - useEffect(() => { - const companyName = settings.company_name || 'Luxury Hotel'; - document.title = requiresMFA ? `Verify Identity - ${companyName}` : `Login - ${companyName}`; - }, [settings.company_name, requiresMFA]); - - - const { - register, - handleSubmit, - formState: { errors }, - } = useForm({ - resolver: yupResolver(loginSchema), - defaultValues: { - email: '', - password: '', - rememberMe: false, - }, - }); - - - const onSubmit = async (data: LoginFormData) => { - try { - clearError(); - - - 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; - } - } - - await login({ - email: data.email, - password: data.password, - rememberMe: data.rememberMe, - }); - - - if (!requiresMFA) { - - const getRedirectPath = () => { - const from = location.state?.from?.pathname; - if (from) return from; - - const currentUserInfo = useAuthStore.getState().userInfo; - if (currentUserInfo?.role === 'admin') { - return '/admin/dashboard'; - } else if (currentUserInfo?.role === 'staff') { - return '/staff/dashboard'; - } - return '/dashboard'; - }; - - navigate(getRedirectPath(), { replace: true }); - } - setRecaptchaToken(null); - } catch (error) { - - console.error('Login error:', error); - setRecaptchaToken(null); - } - }; - - - const onSubmitMFA = async (data: MFATokenFormData) => { - try { - clearError(); - await verifyMFA(data.mfaToken); - - - const getRedirectPath = () => { - const from = location.state?.from?.pathname; - if (from) return from; - - const currentUserInfo = useAuthStore.getState().userInfo; - if (currentUserInfo?.role === 'admin') { - return '/admin/dashboard'; - } else if (currentUserInfo?.role === 'staff') { - return '/staff/dashboard'; - } - return '/dashboard'; - }; - - navigate(getRedirectPath(), { replace: true }); - } catch (error) { - - console.error('MFA verification error:', error); - } - }; - - - const handleBackToLogin = () => { - clearMFA(); - clearError(); - }; - - return ( -
- {} -
- -
- {} -
-
- {settings.company_logo_url ? ( - {settings.company_name - ) : ( -
- -
-
- )} -
- {settings.company_tagline && ( -

- {settings.company_tagline} -

- )} -

- {requiresMFA ? 'Verify Your Identity' : 'Welcome Back'} -

-

- {requiresMFA - ? 'Enter the 6-digit code from your authenticator app' - : `Sign in to ${settings.company_name || 'Luxury Hotel'}`} -

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

- {mfaErrors.mfaToken.message} -

- )} -

- Enter the 6-digit code from your authenticator app or an 8-character backup code -

-
- - {} - - - {} -
- -
-
-
- ) : ( - - <> -
-
- {} - {error && ( -
- {error} -
- )} - - {} -
- -
-
- -
- -
- {errors.email && ( -

- {errors.email.message} -

- )} -
- - {} -
- -
-
- -
- - -
- {errors.password && ( -

- {errors.password.message} -

- )} -
- - {} -
-
- - -
- - - Forgot password? - -
- - {} -
- setRecaptchaToken(token)} - onError={(error) => { - console.error('reCAPTCHA error:', error); - setRecaptchaToken(null); - }} - theme="light" - size="normal" - /> -
- - {} - - - {} -
- - - - Back to Homepage - -
-
- - {} -
-

- Don't have an account?{' '} - - Register now - -

-
-
- - {} -
-

- By logging in, you agree to our{' '} - - Terms of Service - {' '} - and{' '} - - Privacy Policy - -

-
- - )} -
-
- ); -}; - -export default LoginPage; diff --git a/Frontend/src/pages/auth/RegisterPage.tsx b/Frontend/src/pages/auth/RegisterPage.tsx deleted file mode 100644 index 67727382..00000000 --- a/Frontend/src/pages/auth/RegisterPage.tsx +++ /dev/null @@ -1,571 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { useForm } from 'react-hook-form'; -import { yupResolver } from '@hookform/resolvers/yup'; -import { Link, useNavigate } from 'react-router-dom'; -import { - Eye, - EyeOff, - UserPlus, - Loader2, - Mail, - Lock, - User, - Phone, - Hotel, - CheckCircle2, - XCircle, - Home, -} from 'lucide-react'; -import useAuthStore from '../../store/useAuthStore'; -import { - registerSchema, - RegisterFormData, -} from '../../utils/validationSchemas'; -import { useCompanySettings } from '../../contexts/CompanySettingsContext'; -import { toast } from 'react-toastify'; -import Recaptcha from '../../components/common/Recaptcha'; -import { recaptchaService } from '../../services/api/systemSettingsService'; - -const RegisterPage: React.FC = () => { - const navigate = useNavigate(); - const { register: registerUser, isLoading, error, clearError } = - useAuthStore(); - const { settings } = useCompanySettings(); - - const [showPassword, setShowPassword] = useState(false); - const [showConfirmPassword, setShowConfirmPassword] = - useState(false); - const [recaptchaToken, setRecaptchaToken] = useState(null); - - - useEffect(() => { - const companyName = settings.company_name || 'Luxury Hotel'; - document.title = `Register - ${companyName}`; - }, [settings.company_name]); - - - const { - register, - handleSubmit, - watch, - formState: { errors }, - } = useForm({ - resolver: yupResolver(registerSchema), - defaultValues: { - name: '', - email: '', - password: '', - confirmPassword: '', - phone: '', - }, - }); - - - const password = watch('password'); - - - const getPasswordStrength = (pwd: string) => { - if (!pwd) return { strength: 0, label: '', color: '' }; - - let strength = 0; - if (pwd.length >= 8) strength++; - if (/[a-z]/.test(pwd)) strength++; - if (/[A-Z]/.test(pwd)) strength++; - if (/\d/.test(pwd)) strength++; - if (/[@$!%*?&]/.test(pwd)) strength++; - - const labels = [ - { label: 'Very Weak', color: 'bg-red-500' }, - { label: 'Weak', color: 'bg-orange-500' }, - { label: 'Medium', color: 'bg-yellow-500' }, - { label: 'Strong', color: 'bg-blue-500' }, - { label: 'Very Strong', color: 'bg-green-500' }, - ]; - - return { strength, ...labels[strength] }; - }; - - const passwordStrength = getPasswordStrength(password || ''); - - - const onSubmit = async (data: RegisterFormData) => { - try { - clearError(); - - - 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; - } - } - - await registerUser({ - name: data.name, - email: data.email, - password: data.password, - phone: data.phone, - }); - - - navigate('/login', { replace: true }); - setRecaptchaToken(null); - } catch (error) { - - console.error('Register error:', error); - setRecaptchaToken(null); - } - }; - - return ( -
- {} -
- -
- {} -
-
- {settings.company_logo_url ? ( - {settings.company_name - ) : ( -
- -
-
- )} -
- {settings.company_tagline && ( -

- {settings.company_tagline} -

- )} -

- Create Account -

-

- Join {settings.company_name || 'Luxury Hotel'} for exclusive benefits -

-
- - {} -
-
- {} - {error && ( -
- {error} -
- )} - - {} -
- -
-
- -
- -
- {errors.name && ( -

- {errors.name.message} -

- )} -
- - {} -
- -
-
- -
- -
- {errors.email && ( -

- {errors.email.message} -

- )} -
- - {} -
- -
-
- -
- -
- {errors.phone && ( -

- {errors.phone.message} -

- )} -
- - {} -
- -
-
- -
- - -
- {errors.password && ( -

- {errors.password.message} -

- )} - - {} - {password && password.length > 0 && ( -
-
-
-
= 4 - ? 'bg-[#d4af37]' - : passwordStrength.strength >= 3 - ? 'bg-yellow-500' - : 'bg-red-500' - }`} - style={{ - width: `${ - (passwordStrength.strength / 5) * 100 - }%`, - }} - /> -
- - {passwordStrength.label} - -
- - {} -
- = 8} - text="At least 8 characters" - /> - - - - -
-
- )} -
- - {} -
- -
-
- -
- - -
- {errors.confirmPassword && ( -

- {errors.confirmPassword.message} -

- )} -
- - {} -
- setRecaptchaToken(token)} - onError={(error) => { - console.error('reCAPTCHA error:', error); - setRecaptchaToken(null); - }} - theme="light" - size="normal" - /> -
- - {} - - - - {} -
-

- Already have an account?{' '} - - Login now - -

-
- - {} -
- - - - Back to Homepage - -
-
- - {} -
-

- By registering, you agree to our{' '} - - Terms of Service - {' '} - and{' '} - - Privacy Policy - -

-
-
-
- ); -}; - -const PasswordRequirement: React.FC<{ - met: boolean; - text: string; -}> = ({ met, text }) => ( -
- {met ? ( - - ) : ( - - )} - - {text} - -
-); - -export default RegisterPage; diff --git a/Frontend/src/pages/auth/ResetPasswordPage.tsx b/Frontend/src/pages/auth/ResetPasswordPage.tsx deleted file mode 100644 index c7936803..00000000 --- a/Frontend/src/pages/auth/ResetPasswordPage.tsx +++ /dev/null @@ -1,579 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { useForm } from 'react-hook-form'; -import { yupResolver } from '@hookform/resolvers/yup'; -import { useNavigate, useParams, Link } from 'react-router-dom'; -import { - Eye, - EyeOff, - Lock, - Loader2, - CheckCircle2, - XCircle, - AlertCircle, - KeyRound, - Hotel, - Home, -} from 'lucide-react'; -import useAuthStore from '../../store/useAuthStore'; -import { - resetPasswordSchema, - ResetPasswordFormData, -} from '../../utils/validationSchemas'; -import { useCompanySettings } from '../../contexts/CompanySettingsContext'; - -const ResetPasswordPage: React.FC = () => { - const navigate = useNavigate(); - const { token } = useParams<{ token: string }>(); - const { resetPassword, isLoading, error, clearError } = - useAuthStore(); - const { settings } = useCompanySettings(); - - const [showPassword, setShowPassword] = useState(false); - const [showConfirmPassword, setShowConfirmPassword] = - useState(false); - const [isSuccess, setIsSuccess] = useState(false); - - - useEffect(() => { - const companyName = settings.company_name || 'Luxury Hotel'; - document.title = `Reset Password - ${companyName}`; - }, [settings.company_name]); - - - const { - register, - handleSubmit, - watch, - formState: { errors }, - } = useForm({ - resolver: yupResolver(resetPasswordSchema), - defaultValues: { - password: '', - confirmPassword: '', - }, - }); - - - const password = watch('password'); - - - useEffect(() => { - if (!token) { - navigate('/forgot-password', { replace: true }); - } - }, [token, navigate]); - - - const getPasswordStrength = (pwd: string) => { - if (!pwd) return { strength: 0, label: '', color: '' }; - - let strength = 0; - if (pwd.length >= 8) strength++; - if (/[a-z]/.test(pwd)) strength++; - if (/[A-Z]/.test(pwd)) strength++; - if (/\d/.test(pwd)) strength++; - if (/[@$!%*?&]/.test(pwd)) strength++; - - const labels = [ - { label: 'Very Weak', color: 'bg-red-500' }, - { label: 'Weak', color: 'bg-orange-500' }, - { label: 'Medium', color: 'bg-yellow-500' }, - { label: 'Strong', color: 'bg-blue-500' }, - { label: 'Very Strong', color: 'bg-green-500' }, - ]; - - return { strength, ...labels[strength] }; - }; - - const passwordStrength = getPasswordStrength(password || ''); - - - const onSubmit = async (data: ResetPasswordFormData) => { - if (!token) { - return; - } - - try { - clearError(); - await resetPassword({ - token, - password: data.password, - confirmPassword: data.confirmPassword, - }); - - - setIsSuccess(true); - - - setTimeout(() => { - navigate('/login', { replace: true }); - }, 3000); - } catch (error) { - - console.error('Reset password error:', error); - } - }; - - - const isTokenError = - error?.includes('token') || error?.includes('expired'); - - - const isReuseError = - error?.toLowerCase().includes('must be different') || - error?.toLowerCase().includes('different from old'); - - return ( -
-
- {} -
-
- {settings.company_logo_url ? ( - {settings.company_name - ) : ( -
- -
- )} -
- {settings.company_tagline && ( -

- {settings.company_tagline} -

- )} -

- {isSuccess ? 'Complete!' : 'Reset Password'} -

-

- {isSuccess - ? 'Password has been reset successfully' - : `Enter a new password for your ${settings.company_name || 'Luxury Hotel'} account`} -

-
- - {} -
- {isSuccess ? ( - -
-
-
- -
-
- -
-

- Password reset successful! -

-

- Your password has been updated. -

-

- You can now login with your new password. -

-
- -
-

- Redirecting to login page... -

-
- -
-
- - - - Login Now - - - {} -
- - - - Back to Homepage - -
-
- ) : ( - -
- {} - {error && ( -
- -
-

- {isReuseError - ? 'New password must be different from old password' - : error} -

- {isTokenError && ( - - Request new link - - )} -
-
- )} - - {} -
- -
-
- -
- - -
- {errors.password && ( -

- {errors.password.message} -

- )} - - {} - {password && password.length > 0 && ( -
-
-
-
-
- - {passwordStrength.label} - -
- - {} -
- = 8} - text="At least 8 characters" - /> - - - - -
-
- )} -
- - {} -
- -
-
- -
- - -
- {errors.confirmPassword && ( -

- {errors.confirmPassword.message} -

- )} -
- - {} - - - {} -
- - Back to Login - -
- - {} -
- - - - Back to Homepage - -
- - )} -
- - {} - {!isSuccess && ( -
-

- - Security -

-
    -
  • Reset link is valid for 1 hour only
  • -
  • Password is securely encrypted
  • -
  • - If the link expires, please request a new link -
  • -
-
- )} -
-
- ); -}; - -const PasswordRequirement: React.FC<{ - met: boolean; - text: string; -}> = ({ met, text }) => ( -
- {met ? ( - - ) : ( - - )} - - {text} - -
-); - -export default ResetPasswordPage; diff --git a/Frontend/src/pages/customer/FavoritesPage.tsx b/Frontend/src/pages/customer/FavoritesPage.tsx index 771e7a73..8b314be2 100644 --- a/Frontend/src/pages/customer/FavoritesPage.tsx +++ b/Frontend/src/pages/customer/FavoritesPage.tsx @@ -6,9 +6,11 @@ import { RoomCard, RoomCardSkeleton } from import useFavoritesStore from '../../store/useFavoritesStore'; import useAuthStore from '../../store/useAuthStore'; +import { useAuthModal } from '../../contexts/AuthModalContext'; const FavoritesPage: React.FC = () => { const { isAuthenticated } = useAuthStore(); + const { openModal } = useAuthModal(); const { favorites, isLoading, @@ -44,15 +46,15 @@ const FavoritesPage: React.FC = () => {

You need to login to view your favorites list

- openModal('login')} className="inline-block px-6 py-3 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors font-semibold" > Login - +