Enhance authentication and middleware logic by improving user role checks, adding OTP verification steps, and refining redirection based on user roles. Update login and signup forms to handle multiple user attributes and streamline error handling. Integrate logout functionality across components for better user experience.
This commit is contained in:
parent
7b5f57ea89
commit
041c36079d
@ -17,6 +17,8 @@ import {
|
||||
} from "lucide-react";
|
||||
import { useAppTheme } from "@/components/ThemeProvider";
|
||||
import { ThemeToggle } from "@/components/ThemeToggle";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export function Header() {
|
||||
const pathname = usePathname();
|
||||
@ -25,6 +27,14 @@ export function Header() {
|
||||
const [userMenuOpen, setUserMenuOpen] = useState(false);
|
||||
const { theme } = useAppTheme();
|
||||
const isDark = theme === "dark";
|
||||
const { logout } = useAuth();
|
||||
|
||||
const handleLogout = () => {
|
||||
setUserMenuOpen(false);
|
||||
logout();
|
||||
toast.success("Logged out successfully");
|
||||
router.push("/");
|
||||
};
|
||||
|
||||
// Mock notifications data
|
||||
const notifications = [
|
||||
@ -209,10 +219,7 @@ export function Header() {
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setUserMenuOpen(false);
|
||||
router.push("/");
|
||||
}}
|
||||
onClick={handleLogout}
|
||||
className={`w-full flex items-center gap-3 px-4 py-3 justify-start transition-colors cursor-pointer ${
|
||||
isDark ? "hover:bg-gray-800" : "hover:bg-gray-50"
|
||||
}`}
|
||||
|
||||
@ -14,6 +14,8 @@ import {
|
||||
Heart,
|
||||
} from "lucide-react";
|
||||
import { useAppTheme } from "@/components/ThemeProvider";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { toast } from "sonner";
|
||||
|
||||
const navItems = [
|
||||
{ label: "Dashboard", icon: LayoutGrid, href: "/admin/dashboard" },
|
||||
@ -26,6 +28,14 @@ export default function SideNav() {
|
||||
const router = useRouter();
|
||||
const { theme } = useAppTheme();
|
||||
const isDark = theme === "dark";
|
||||
const { logout } = useAuth();
|
||||
|
||||
const handleLogout = () => {
|
||||
setOpen(false);
|
||||
logout();
|
||||
toast.success("Logged out successfully");
|
||||
router.push("/");
|
||||
};
|
||||
|
||||
const getActiveIndex = () => {
|
||||
return navItems.findIndex((item) => pathname?.includes(item.href)) ?? -1;
|
||||
@ -176,10 +186,7 @@ export default function SideNav() {
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
router.push("/");
|
||||
}}
|
||||
onClick={handleLogout}
|
||||
className={`group flex items-center gap-2 py-2.5 pl-3 md:pl-3 pr-3 md:pr-3 transition-colors duration-200 w-[90%] md:w-[90%] ml-1 md:ml-2 cursor-pointer justify-start rounded-lg ${
|
||||
isDark
|
||||
? "text-gray-300 hover:bg-gray-800 hover:text-rose-300"
|
||||
|
||||
@ -3,48 +3,127 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Heart, Eye, EyeOff, X, Loader2 } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
InputOTP,
|
||||
InputOTPGroup,
|
||||
InputOTPSlot,
|
||||
} from "@/components/ui/input-otp";
|
||||
import { Heart, Eye, EyeOff, X, Loader2, CheckCircle2 } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useAppTheme } from "@/components/ThemeProvider";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { loginSchema, type LoginInput } from "@/lib/schema/auth";
|
||||
import {
|
||||
loginSchema,
|
||||
registerSchema,
|
||||
verifyOtpSchema,
|
||||
type LoginInput,
|
||||
type RegisterInput,
|
||||
type VerifyOtpInput
|
||||
} from "@/lib/schema/auth";
|
||||
import { toast } from "sonner";
|
||||
|
||||
type Step = "login" | "signup" | "verify";
|
||||
|
||||
export default function Login() {
|
||||
const { theme } = useAppTheme();
|
||||
const isDark = theme === "dark";
|
||||
const [step, setStep] = useState<Step>("login");
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [showPassword2, setShowPassword2] = useState(false);
|
||||
const [rememberMe, setRememberMe] = useState(false);
|
||||
const [formData, setFormData] = useState<LoginInput>({
|
||||
const [registeredEmail, setRegisteredEmail] = useState("");
|
||||
|
||||
// Login form data
|
||||
const [loginData, setLoginData] = useState<LoginInput>({
|
||||
email: "",
|
||||
password: "",
|
||||
});
|
||||
const [errors, setErrors] = useState<Partial<Record<keyof LoginInput, string>>>({});
|
||||
|
||||
// Signup form data
|
||||
const [signupData, setSignupData] = useState<RegisterInput>({
|
||||
first_name: "",
|
||||
last_name: "",
|
||||
email: "",
|
||||
phone_number: "",
|
||||
password: "",
|
||||
password2: "",
|
||||
});
|
||||
|
||||
// OTP verification data
|
||||
const [otpData, setOtpData] = useState<VerifyOtpInput>({
|
||||
email: "",
|
||||
otp: "",
|
||||
});
|
||||
|
||||
const [errors, setErrors] = useState<Partial<Record<string, string>>>({});
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const { login, isAuthenticated, loginMutation } = useAuth();
|
||||
const {
|
||||
login,
|
||||
register,
|
||||
verifyOtp,
|
||||
isAuthenticated,
|
||||
isAdmin,
|
||||
loginMutation,
|
||||
registerMutation,
|
||||
verifyOtpMutation,
|
||||
resendOtpMutation
|
||||
} = useAuth();
|
||||
|
||||
// Check for verify step or email from query parameters
|
||||
useEffect(() => {
|
||||
const verifyEmail = searchParams.get("verify");
|
||||
const emailParam = searchParams.get("email");
|
||||
const errorParam = searchParams.get("error");
|
||||
|
||||
// Don't show verify step if there's an error indicating OTP sending failed
|
||||
if (errorParam && errorParam.toLowerCase().includes("failed to send")) {
|
||||
setStep("login");
|
||||
return;
|
||||
}
|
||||
|
||||
if (verifyEmail === "true" && emailParam) {
|
||||
// Show verify step if verify=true
|
||||
setStep("verify");
|
||||
setRegisteredEmail(emailParam);
|
||||
setOtpData({ email: emailParam, otp: "" });
|
||||
} else if (emailParam && step === "login") {
|
||||
// Pre-fill email in login form if email parameter is present
|
||||
setLoginData(prev => ({ ...prev, email: emailParam }));
|
||||
}
|
||||
}, [searchParams, step]);
|
||||
|
||||
// Redirect if already authenticated
|
||||
useEffect(() => {
|
||||
if (isAuthenticated) {
|
||||
const redirect = searchParams.get("redirect") || "/admin/dashboard";
|
||||
router.push(redirect);
|
||||
// Use a small delay to ensure cookies are set and middleware has processed
|
||||
const timer = setTimeout(() => {
|
||||
// Always redirect based on user role, ignore redirect parameter if user is admin
|
||||
const redirectParam = searchParams.get("redirect");
|
||||
const defaultRedirect = isAdmin ? "/admin/dashboard" : "/user/dashboard";
|
||||
const finalRedirect = isAdmin ? "/admin/dashboard" : (redirectParam || defaultRedirect);
|
||||
|
||||
// Use window.location.href to ensure full page reload and cookie reading
|
||||
window.location.href = finalRedirect;
|
||||
}, 200);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [isAuthenticated, router, searchParams]);
|
||||
}, [isAuthenticated, isAdmin, searchParams]);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
// Handle login
|
||||
const handleLogin = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setErrors({});
|
||||
|
||||
// Validate form
|
||||
const validation = loginSchema.safeParse(formData);
|
||||
const validation = loginSchema.safeParse(loginData);
|
||||
if (!validation.success) {
|
||||
const fieldErrors: Partial<Record<keyof LoginInput, string>> = {};
|
||||
const fieldErrors: Partial<Record<string, string>> = {};
|
||||
validation.error.issues.forEach((err) => {
|
||||
if (err.path[0]) {
|
||||
fieldErrors[err.path[0] as keyof LoginInput] = err.message;
|
||||
fieldErrors[err.path[0] as string] = err.message;
|
||||
}
|
||||
});
|
||||
setErrors(fieldErrors);
|
||||
@ -52,40 +131,214 @@ export default function Login() {
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await login(formData);
|
||||
const result = await login(loginData);
|
||||
|
||||
if (result.tokens && result.user) {
|
||||
toast.success("Login successful!");
|
||||
|
||||
// Normalize user data
|
||||
const user = result.user;
|
||||
// Check for admin status - check multiple possible field names
|
||||
const isAdmin =
|
||||
// Wait a moment for cookies to be set, then redirect
|
||||
// Check if user is admin/staff/superuser - check all possible field names
|
||||
const user = result.user as any;
|
||||
const userIsAdmin =
|
||||
user.is_admin === true ||
|
||||
(user as any)?.isAdmin === true ||
|
||||
(user as any)?.is_staff === true ||
|
||||
(user as any)?.isStaff === true;
|
||||
user.isAdmin === true ||
|
||||
user.is_staff === true ||
|
||||
user.isStaff === true ||
|
||||
user.is_superuser === true ||
|
||||
user.isSuperuser === true;
|
||||
|
||||
// Redirect based on user role
|
||||
const redirect = searchParams.get("redirect");
|
||||
if (redirect) {
|
||||
router.push(redirect);
|
||||
} else {
|
||||
// Default to admin dashboard
|
||||
router.push("/admin/dashboard");
|
||||
}
|
||||
// Wait longer for cookies to be set and middleware to process
|
||||
setTimeout(() => {
|
||||
// Always redirect based on user role, ignore redirect parameter if user is admin
|
||||
// This ensures admins always go to admin dashboard
|
||||
const defaultRedirect = userIsAdmin ? "/admin/dashboard" : "/user/dashboard";
|
||||
|
||||
// Only use redirect parameter if user is NOT admin
|
||||
const redirectParam = searchParams.get("redirect");
|
||||
const finalRedirect = userIsAdmin ? "/admin/dashboard" : (redirectParam || defaultRedirect);
|
||||
|
||||
// Use window.location.href instead of router.push to ensure full page reload
|
||||
// This ensures cookies are read correctly by middleware
|
||||
window.location.href = finalRedirect;
|
||||
}, 300);
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : "Login failed. Please try again.";
|
||||
toast.error(errorMessage);
|
||||
// Don't set field errors for server errors, only show toast
|
||||
setErrors({});
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (field: keyof LoginInput, value: string) => {
|
||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||
// Clear error when user starts typing
|
||||
// Handle signup
|
||||
const handleSignup = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setErrors({});
|
||||
|
||||
// Validate form
|
||||
const validation = registerSchema.safeParse(signupData);
|
||||
if (!validation.success) {
|
||||
const fieldErrors: Partial<Record<string, string>> = {};
|
||||
validation.error.issues.forEach((err) => {
|
||||
if (err.path[0]) {
|
||||
fieldErrors[err.path[0] as string] = err.message;
|
||||
}
|
||||
});
|
||||
setErrors(fieldErrors);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await register(signupData);
|
||||
|
||||
// Check if registration was successful (user created)
|
||||
// Even if OTP sending failed, we should allow user to proceed to verification
|
||||
// and use resend OTP feature
|
||||
if (result && result.message) {
|
||||
// Registration successful - proceed to OTP verification
|
||||
toast.success("Registration successful! Please check your email for OTP verification.");
|
||||
setRegisteredEmail(signupData.email);
|
||||
setOtpData({ email: signupData.email, otp: "" });
|
||||
setStep("verify");
|
||||
} else {
|
||||
// If no message but no error, still proceed (some APIs might not return message)
|
||||
toast.success("Registration successful! Please check your email for OTP verification.");
|
||||
setRegisteredEmail(signupData.email);
|
||||
setOtpData({ email: signupData.email, otp: "" });
|
||||
setStep("verify");
|
||||
}
|
||||
} catch (error) {
|
||||
// Handle different types of errors
|
||||
let errorMessage = "Registration failed. Please try again.";
|
||||
|
||||
if (error instanceof Error) {
|
||||
errorMessage = error.message;
|
||||
|
||||
// If OTP sending failed, don't show OTP verification - just show error
|
||||
if (errorMessage.toLowerCase().includes("failed to send") ||
|
||||
errorMessage.toLowerCase().includes("failed to send otp")) {
|
||||
toast.error("Registration failed: OTP could not be sent. Please try again later or contact support.");
|
||||
setErrors({});
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if it's an OTP sending error but registration might have succeeded
|
||||
if (errorMessage.toLowerCase().includes("otp") ||
|
||||
errorMessage.toLowerCase().includes("email") ||
|
||||
errorMessage.toLowerCase().includes("send")) {
|
||||
// If OTP sending failed but user might be created, allow proceeding to verification
|
||||
// User can use resend OTP
|
||||
toast.warning("Registration completed, but OTP email could not be sent. You can request a new OTP on the next screen.");
|
||||
setRegisteredEmail(signupData.email);
|
||||
setOtpData({ email: signupData.email, otp: "" });
|
||||
setStep("verify");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
toast.error(errorMessage);
|
||||
setErrors({});
|
||||
}
|
||||
};
|
||||
|
||||
// Handle OTP verification
|
||||
const handleVerifyOtp = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setErrors({});
|
||||
|
||||
// Use registeredEmail if available, otherwise use otpData.email
|
||||
const emailToVerify = registeredEmail || otpData.email;
|
||||
if (!emailToVerify) {
|
||||
setErrors({ email: "Email address is required" });
|
||||
return;
|
||||
}
|
||||
|
||||
// Prepare OTP data with email
|
||||
const otpToVerify = {
|
||||
email: emailToVerify,
|
||||
otp: otpData.otp,
|
||||
};
|
||||
|
||||
// Validate OTP
|
||||
const validation = verifyOtpSchema.safeParse(otpToVerify);
|
||||
if (!validation.success) {
|
||||
const fieldErrors: Partial<Record<string, string>> = {};
|
||||
validation.error.issues.forEach((err) => {
|
||||
if (err.path[0]) {
|
||||
fieldErrors[err.path[0] as string] = err.message;
|
||||
}
|
||||
});
|
||||
setErrors(fieldErrors);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await verifyOtp(otpToVerify);
|
||||
|
||||
// If verification is successful, redirect to login page
|
||||
toast.success("Email verified successfully! Redirecting to login...");
|
||||
// Redirect to login page with email pre-filled
|
||||
setTimeout(() => {
|
||||
router.push(`/login?email=${encodeURIComponent(emailToVerify)}`);
|
||||
}, 1000);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : "OTP verification failed. Please try again.";
|
||||
toast.error(errorMessage);
|
||||
setErrors({});
|
||||
}
|
||||
};
|
||||
|
||||
// Handle resend OTP
|
||||
const handleResendOtp = async () => {
|
||||
const emailToUse = registeredEmail || otpData.email;
|
||||
|
||||
if (!emailToUse) {
|
||||
toast.error("Email address is required to resend OTP.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await resendOtpMutation.mutateAsync({ email: emailToUse, context: "registration" });
|
||||
toast.success("OTP resent successfully! Please check your email.");
|
||||
// Update registeredEmail if it wasn't set
|
||||
if (!registeredEmail) {
|
||||
setRegisteredEmail(emailToUse);
|
||||
}
|
||||
} catch (error) {
|
||||
let errorMessage = "Failed to resend OTP. Please try again.";
|
||||
|
||||
if (error instanceof Error) {
|
||||
errorMessage = error.message;
|
||||
|
||||
// Provide more helpful error messages
|
||||
if (errorMessage.toLowerCase().includes("ssl") ||
|
||||
errorMessage.toLowerCase().includes("certificate")) {
|
||||
errorMessage = "Email service is currently unavailable. Please contact support or try again later.";
|
||||
} else if (errorMessage.toLowerCase().includes("not found") ||
|
||||
errorMessage.toLowerCase().includes("does not exist")) {
|
||||
errorMessage = "Email address not found. Please check your email or register again.";
|
||||
}
|
||||
}
|
||||
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle form field changes
|
||||
const handleLoginChange = (field: keyof LoginInput, value: string) => {
|
||||
setLoginData((prev) => ({ ...prev, [field]: value }));
|
||||
if (errors[field]) {
|
||||
setErrors((prev) => ({ ...prev, [field]: undefined }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleSignupChange = (field: keyof RegisterInput, value: string) => {
|
||||
setSignupData((prev) => ({ ...prev, [field]: value }));
|
||||
if (errors[field]) {
|
||||
setErrors((prev) => ({ ...prev, [field]: undefined }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleOtpChange = (field: keyof VerifyOtpInput, value: string) => {
|
||||
setOtpData((prev) => ({ ...prev, [field]: value }));
|
||||
if (errors[field]) {
|
||||
setErrors((prev) => ({ ...prev, [field]: undefined }));
|
||||
}
|
||||
@ -113,31 +366,58 @@ export default function Login() {
|
||||
<span className="text-white text-xl font-semibold">Attune Heart Therapy</span>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{/* Centered White Card - Login Form */}
|
||||
{/* Centered White Card */}
|
||||
<div className={`relative z-20 w-full max-w-md rounded-2xl shadow-2xl p-8 ${isDark ? 'bg-gray-800 border border-gray-700' : 'bg-white'}`}>
|
||||
{/* Header with Close Button */}
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<div className="flex-1">
|
||||
{/* Heading */}
|
||||
<h1 className="text-3xl font-bold bg-gradient-to-r from-rose-600 via-pink-600 to-rose-600 bg-clip-text text-transparent mb-2">
|
||||
Welcome back
|
||||
<h1 className="text-3xl font-bold bg-linear-to-r from-rose-600 via-pink-600 to-rose-600 bg-clip-text text-transparent mb-2">
|
||||
{step === "login" && "Welcome back"}
|
||||
{step === "signup" && "Create an account"}
|
||||
{step === "verify" && "Verify your email"}
|
||||
</h1>
|
||||
{/* Sign Up Prompt */}
|
||||
{/* Subtitle */}
|
||||
{step === "login" && (
|
||||
<p className={`mb-6 ${isDark ? 'text-gray-400' : 'text-gray-600'}`}>
|
||||
New to Attune Heart Therapy?{" "}
|
||||
<Link href="/signup" className={`underline font-medium ${isDark ? 'text-blue-400 hover:text-blue-300' : 'text-blue-600 hover:text-blue-700'}`}>
|
||||
<Link
|
||||
href="/signup"
|
||||
className={`underline font-medium ${isDark ? 'text-blue-400 hover:text-blue-300' : 'text-blue-600 hover:text-blue-700'}`}
|
||||
>
|
||||
Sign up
|
||||
</Link>
|
||||
</p>
|
||||
)}
|
||||
{step === "signup" && (
|
||||
<p className={`mb-6 ${isDark ? 'text-gray-400' : 'text-gray-600'}`}>
|
||||
Already have an account?{" "}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setStep("login")}
|
||||
className={`underline font-medium ${isDark ? 'text-blue-400 hover:text-blue-300' : 'text-blue-600 hover:text-blue-700'}`}
|
||||
>
|
||||
Log in
|
||||
</button>
|
||||
</p>
|
||||
)}
|
||||
{step === "verify" && registeredEmail && (
|
||||
<p className={`mb-6 ${isDark ? 'text-gray-400' : 'text-gray-600'}`}>
|
||||
We've sent a verification code to <strong>{registeredEmail}</strong>
|
||||
</p>
|
||||
)}
|
||||
{step === "verify" && !registeredEmail && (
|
||||
<p className={`mb-6 ${isDark ? 'text-gray-400' : 'text-gray-600'}`}>
|
||||
Enter the verification code sent to your email
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{/* Close Button */}
|
||||
<Button
|
||||
onClick={() => router.back()}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={`flex-shrink-0 w-8 h-8 rounded-full ${isDark ? 'text-gray-400 hover:text-gray-300 hover:bg-gray-700' : 'text-gray-500 hover:text-gray-700 hover:bg-gray-100'}`}
|
||||
className={`shrink-0 w-8 h-8 rounded-full ${isDark ? 'text-gray-400 hover:text-gray-300 hover:bg-gray-700' : 'text-gray-500 hover:text-gray-700 hover:bg-gray-100'}`}
|
||||
aria-label="Close"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
@ -145,7 +425,8 @@ export default function Login() {
|
||||
</div>
|
||||
|
||||
{/* Login Form */}
|
||||
<form className="space-y-6" onSubmit={handleSubmit}>
|
||||
{step === "login" && (
|
||||
<form className="space-y-6" onSubmit={handleLogin}>
|
||||
{/* Email Field */}
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="email" className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-black'}`}>
|
||||
@ -155,11 +436,14 @@ export default function Login() {
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="Email address"
|
||||
value={formData.email}
|
||||
onChange={(e) => handleChange("email", e.target.value)}
|
||||
value={loginData.email}
|
||||
onChange={(e) => handleLoginChange("email", e.target.value)}
|
||||
className={`h-12 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'} ${errors.email ? 'border-red-500' : ''}`}
|
||||
required
|
||||
/>
|
||||
{errors.email && (
|
||||
<p className="text-sm text-red-500">{errors.email}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Password Field */}
|
||||
@ -172,8 +456,8 @@ export default function Login() {
|
||||
id="password"
|
||||
type={showPassword ? "text" : "password"}
|
||||
placeholder="Your password"
|
||||
value={formData.password}
|
||||
onChange={(e) => handleChange("password", e.target.value)}
|
||||
value={loginData.password}
|
||||
onChange={(e) => handleLoginChange("password", e.target.value)}
|
||||
className={`h-12 pr-12 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'} ${errors.password ? 'border-red-500' : ''}`}
|
||||
required
|
||||
/>
|
||||
@ -201,7 +485,7 @@ export default function Login() {
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={loginMutation.isPending}
|
||||
className="w-full h-12 text-base font-semibold bg-gradient-to-r from-rose-500 to-pink-600 hover:from-rose-600 hover:to-pink-700 text-white shadow-lg hover:shadow-xl transition-all disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
className="w-full h-12 text-base font-semibold bg-linear-to-r from-rose-500 to-pink-600 hover:from-rose-600 hover:to-pink-700 text-white shadow-lg hover:shadow-xl transition-all disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{loginMutation.isPending ? (
|
||||
<>
|
||||
@ -224,16 +508,288 @@ export default function Login() {
|
||||
/>
|
||||
<span className={isDark ? 'text-gray-300' : 'text-black'}>Remember me</span>
|
||||
</label>
|
||||
<Link
|
||||
href="/forgot-password"
|
||||
<button
|
||||
type="button"
|
||||
className={`font-medium ${isDark ? 'text-blue-400 hover:text-blue-300' : 'text-blue-600 hover:text-blue-700'}`}
|
||||
>
|
||||
Forgot password?
|
||||
</Link>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{/* Signup Form */}
|
||||
{step === "signup" && (
|
||||
<form className="space-y-4" onSubmit={handleSignup}>
|
||||
{/* First Name Field */}
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="firstName" className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-black'}`}>
|
||||
First Name *
|
||||
</label>
|
||||
<Input
|
||||
id="firstName"
|
||||
type="text"
|
||||
placeholder="John"
|
||||
value={signupData.first_name}
|
||||
onChange={(e) => handleSignupChange("first_name", e.target.value)}
|
||||
className={`h-11 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'} ${errors.first_name ? 'border-red-500' : ''}`}
|
||||
required
|
||||
/>
|
||||
{errors.first_name && (
|
||||
<p className="text-sm text-red-500">{errors.first_name}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Last Name Field */}
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="lastName" className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-black'}`}>
|
||||
Last Name *
|
||||
</label>
|
||||
<Input
|
||||
id="lastName"
|
||||
type="text"
|
||||
placeholder="Doe"
|
||||
value={signupData.last_name}
|
||||
onChange={(e) => handleSignupChange("last_name", e.target.value)}
|
||||
className={`h-11 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'} ${errors.last_name ? 'border-red-500' : ''}`}
|
||||
required
|
||||
/>
|
||||
{errors.last_name && (
|
||||
<p className="text-sm text-red-500">{errors.last_name}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Email Field */}
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="signup-email" className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-black'}`}>
|
||||
Email address *
|
||||
</label>
|
||||
<Input
|
||||
id="signup-email"
|
||||
type="email"
|
||||
placeholder="Email address"
|
||||
value={signupData.email}
|
||||
onChange={(e) => handleSignupChange("email", e.target.value)}
|
||||
className={`h-11 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'} ${errors.email ? 'border-red-500' : ''}`}
|
||||
required
|
||||
/>
|
||||
{errors.email && (
|
||||
<p className="text-sm text-red-500">{errors.email}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Phone Field */}
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="phone" className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-black'}`}>
|
||||
Phone Number (Optional)
|
||||
</label>
|
||||
<Input
|
||||
id="phone"
|
||||
type="tel"
|
||||
placeholder="+1 (555) 123-4567"
|
||||
value={signupData.phone_number || ""}
|
||||
onChange={(e) => handleSignupChange("phone_number", e.target.value)}
|
||||
className={`h-11 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Password Field */}
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="signup-password" className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-black'}`}>
|
||||
Password *
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="signup-password"
|
||||
type={showPassword ? "text" : "password"}
|
||||
placeholder="Password (min 8 characters)"
|
||||
value={signupData.password}
|
||||
onChange={(e) => handleSignupChange("password", e.target.value)}
|
||||
className={`h-11 pr-12 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'} ${errors.password ? 'border-red-500' : ''}`}
|
||||
required
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className={`absolute right-4 top-1/2 -translate-y-1/2 h-auto w-auto p-0 ${isDark ? 'text-gray-400 hover:text-gray-300' : 'text-gray-500 hover:text-gray-700'}`}
|
||||
aria-label={showPassword ? "Hide password" : "Show password"}
|
||||
>
|
||||
{showPassword ? (
|
||||
<EyeOff className="w-5 h-5" />
|
||||
) : (
|
||||
<Eye className="w-5 h-5" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
{errors.password && (
|
||||
<p className="text-sm text-red-500">{errors.password}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Confirm Password Field */}
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="signup-password2" className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-black'}`}>
|
||||
Confirm Password *
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="signup-password2"
|
||||
type={showPassword2 ? "text" : "password"}
|
||||
placeholder="Confirm password"
|
||||
value={signupData.password2}
|
||||
onChange={(e) => handleSignupChange("password2", e.target.value)}
|
||||
className={`h-11 pr-12 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'} ${errors.password2 ? 'border-red-500' : ''}`}
|
||||
required
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setShowPassword2(!showPassword2)}
|
||||
className={`absolute right-4 top-1/2 -translate-y-1/2 h-auto w-auto p-0 ${isDark ? 'text-gray-400 hover:text-gray-300' : 'text-gray-500 hover:text-gray-700'}`}
|
||||
aria-label={showPassword2 ? "Hide password" : "Show password"}
|
||||
>
|
||||
{showPassword2 ? (
|
||||
<EyeOff className="w-5 h-5" />
|
||||
) : (
|
||||
<Eye className="w-5 h-5" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
{errors.password2 && (
|
||||
<p className="text-sm text-red-500">{errors.password2}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Submit Button */}
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={registerMutation.isPending}
|
||||
className="w-full h-12 text-base font-semibold bg-linear-to-r from-rose-500 to-pink-600 hover:from-rose-600 hover:to-pink-700 text-white shadow-lg hover:shadow-xl transition-all disabled:opacity-50 disabled:cursor-not-allowed mt-6"
|
||||
>
|
||||
{registerMutation.isPending ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
Creating account...
|
||||
</>
|
||||
) : (
|
||||
"Sign up"
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{/* OTP Verification Form */}
|
||||
{step === "verify" && (
|
||||
<form className="space-y-6" onSubmit={handleVerifyOtp}>
|
||||
<div className={`p-4 rounded-lg border ${isDark ? 'bg-blue-900/20 border-blue-800' : 'bg-blue-50 border-blue-200'}`}>
|
||||
<div className="flex items-start gap-3">
|
||||
<CheckCircle2 className={`w-5 h-5 mt-0.5 ${isDark ? 'text-blue-400' : 'text-blue-600'}`} />
|
||||
<div>
|
||||
<p className={`text-sm font-medium ${isDark ? 'text-blue-200' : 'text-blue-900'}`}>
|
||||
Check your email
|
||||
</p>
|
||||
<p className={`text-sm mt-1 ${isDark ? 'text-blue-300' : 'text-blue-700'}`}>
|
||||
We've sent a 6-digit verification code to your email address.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Email Field (if not set) */}
|
||||
{!registeredEmail && (
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="verify-email" className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-black'}`}>
|
||||
Email address *
|
||||
</label>
|
||||
<Input
|
||||
id="verify-email"
|
||||
type="email"
|
||||
placeholder="Email address"
|
||||
value={otpData.email}
|
||||
onChange={(e) => handleOtpChange("email", e.target.value)}
|
||||
className={`h-12 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'} ${errors.email ? 'border-red-500' : ''}`}
|
||||
required
|
||||
/>
|
||||
{errors.email && (
|
||||
<p className="text-sm text-red-500">{errors.email}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* OTP Field */}
|
||||
<div className="space-y-2">
|
||||
<label className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-black'}`}>
|
||||
Verification Code *
|
||||
</label>
|
||||
<div className="flex justify-center">
|
||||
<InputOTP
|
||||
maxLength={6}
|
||||
value={otpData.otp}
|
||||
onChange={(value) => handleOtpChange("otp", value)}
|
||||
aria-invalid={!!errors.otp}
|
||||
>
|
||||
<InputOTPGroup>
|
||||
<InputOTPSlot index={0} />
|
||||
<InputOTPSlot index={1} />
|
||||
<InputOTPSlot index={2} />
|
||||
<InputOTPSlot index={3} />
|
||||
<InputOTPSlot index={4} />
|
||||
<InputOTPSlot index={5} />
|
||||
</InputOTPGroup>
|
||||
</InputOTP>
|
||||
</div>
|
||||
{errors.otp && (
|
||||
<p className="text-sm text-red-500 text-center">{errors.otp}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Resend OTP */}
|
||||
<div className="text-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleResendOtp}
|
||||
disabled={resendOtpMutation.isPending}
|
||||
className={`text-sm font-medium ${isDark ? 'text-blue-400 hover:text-blue-300' : 'text-blue-600 hover:text-blue-700'} disabled:opacity-50 disabled:cursor-not-allowed`}
|
||||
>
|
||||
{resendOtpMutation.isPending ? "Sending..." : "Didn't receive the code? Resend"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Submit Button */}
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={verifyOtpMutation.isPending}
|
||||
className="w-full h-12 text-base font-semibold bg-linear-to-r from-rose-500 to-pink-600 hover:from-rose-600 hover:to-pink-700 text-white shadow-lg hover:shadow-xl transition-all disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{verifyOtpMutation.isPending ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
Verifying...
|
||||
</>
|
||||
) : (
|
||||
"Verify Email"
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{/* Back to signup */}
|
||||
<div className="text-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setStep("signup");
|
||||
setOtpData({ email: "", otp: "" });
|
||||
}}
|
||||
className={`text-sm font-medium ${isDark ? 'text-gray-400 hover:text-gray-300' : 'text-gray-600 hover:text-gray-700'}`}
|
||||
>
|
||||
← Back to signup
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -69,13 +69,21 @@ export default function Signup() {
|
||||
try {
|
||||
const result = await register(formData);
|
||||
|
||||
// If registration is successful (no error thrown), show OTP verification
|
||||
// If registration is successful, redirect to login page with verify parameter
|
||||
toast.success("Registration successful! Please check your email for OTP verification.");
|
||||
setRegisteredEmail(formData.email);
|
||||
setOtpData({ email: formData.email, otp: "" });
|
||||
setStep("verify");
|
||||
// Redirect to login page with verify step
|
||||
router.push(`/login?verify=true&email=${encodeURIComponent(formData.email)}`);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : "Registration failed. Please try again.";
|
||||
|
||||
// If OTP sending failed, don't show OTP verification - just show error
|
||||
if (errorMessage.toLowerCase().includes("failed to send") ||
|
||||
errorMessage.toLowerCase().includes("failed to send otp")) {
|
||||
toast.error("Registration failed: OTP could not be sent. Please try again later or contact support.");
|
||||
setErrors({});
|
||||
return;
|
||||
}
|
||||
|
||||
toast.error(errorMessage);
|
||||
// Don't set field errors for server errors, only show toast
|
||||
setErrors({});
|
||||
@ -105,9 +113,9 @@ export default function Signup() {
|
||||
// If verification is successful (no error thrown), show success and redirect
|
||||
toast.success("Email verified successfully! Redirecting to login...");
|
||||
|
||||
// Redirect to login page after OTP verification
|
||||
// Redirect to login page after OTP verification with email pre-filled
|
||||
setTimeout(() => {
|
||||
router.push("/login");
|
||||
router.push(`/login?email=${encodeURIComponent(otpData.email)}`);
|
||||
}, 1500);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : "OTP verification failed. Please try again.";
|
||||
|
||||
@ -23,11 +23,14 @@ import {
|
||||
CheckCircle2,
|
||||
CheckCircle,
|
||||
Loader2,
|
||||
LogOut,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { LoginDialog } from "@/components/LoginDialog";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface User {
|
||||
ID: number;
|
||||
@ -73,6 +76,7 @@ export default function BookNowPage() {
|
||||
const router = useRouter();
|
||||
const { theme } = useAppTheme();
|
||||
const isDark = theme === "dark";
|
||||
const { isAuthenticated, logout } = useAuth();
|
||||
const [formData, setFormData] = useState({
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
@ -87,6 +91,12 @@ export default function BookNowPage() {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [showLoginDialog, setShowLoginDialog] = useState(false);
|
||||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
toast.success("Logged out successfully");
|
||||
router.push("/");
|
||||
};
|
||||
|
||||
// Handle submit button click
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
@ -660,6 +670,20 @@ export default function BookNowPage() {
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Logout Button - Only show when authenticated */}
|
||||
{isAuthenticated && (
|
||||
<div className="mt-6 flex justify-center">
|
||||
<Button
|
||||
onClick={handleLogout}
|
||||
variant="outline"
|
||||
className="bg-red-600 hover:bg-red-700 text-white border-red-600 hover:border-red-700"
|
||||
>
|
||||
<LogOut className="w-4 h-4 mr-2" />
|
||||
Logout
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -2,13 +2,15 @@
|
||||
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Heart, Menu, X } from "lucide-react";
|
||||
import { Heart, Menu, X, LogOut } from "lucide-react";
|
||||
import { ThemeToggle } from "@/components/ThemeToggle";
|
||||
import { useEffect, useState } from "react";
|
||||
import { LoginDialog } from "@/components/LoginDialog";
|
||||
import { useRouter, usePathname } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { useAppTheme } from "@/components/ThemeProvider";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export function Navbar() {
|
||||
const { theme } = useAppTheme();
|
||||
@ -18,6 +20,9 @@ export function Navbar() {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const isUserDashboard = pathname?.startsWith("/user/dashboard");
|
||||
const isUserSettings = pathname?.startsWith("/user/settings");
|
||||
const isUserRoute = pathname?.startsWith("/user/");
|
||||
const { isAuthenticated, logout } = useAuth();
|
||||
|
||||
const scrollToSection = (id: string) => {
|
||||
const element = document.getElementById(id);
|
||||
@ -33,6 +38,13 @@ export function Navbar() {
|
||||
setMobileMenuOpen(false);
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
toast.success("Logged out successfully");
|
||||
setMobileMenuOpen(false);
|
||||
router.push("/");
|
||||
};
|
||||
|
||||
// Close mobile menu when clicking outside
|
||||
useEffect(() => {
|
||||
if (mobileMenuOpen) {
|
||||
@ -73,7 +85,7 @@ export function Navbar() {
|
||||
</motion.div>
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
{!isUserDashboard && (
|
||||
{!isUserRoute && (
|
||||
<div className="hidden lg:flex items-center gap-4 xl:gap-6">
|
||||
<button
|
||||
onClick={() => scrollToSection("about")}
|
||||
@ -98,7 +110,7 @@ export function Navbar() {
|
||||
|
||||
{/* Desktop Actions */}
|
||||
<div className="hidden lg:flex items-center gap-2">
|
||||
{!isUserDashboard && (
|
||||
{!isAuthenticated && !isUserDashboard && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
@ -109,9 +121,31 @@ export function Navbar() {
|
||||
</Button>
|
||||
)}
|
||||
<ThemeToggle />
|
||||
<Button size="sm" className="hover:opacity-90 hover:scale-105 transition-all text-xs sm:text-sm" asChild>
|
||||
<a href="/book-now">Book Now</a>
|
||||
</Button>
|
||||
{!isUserDashboard && (
|
||||
<Link
|
||||
href="/book-now"
|
||||
className={`text-sm font-medium transition-colors cursor-pointer px-3 py-2 rounded-lg hover:opacity-90 ${isDark ? 'text-gray-300 hover:text-white' : 'text-gray-700 hover:text-rose-600'}`}
|
||||
>
|
||||
Book-Now
|
||||
</Link>
|
||||
)}
|
||||
{isAuthenticated && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className={`hover:opacity-90 hover:scale-105 transition-all text-xs sm:text-sm ${
|
||||
isUserRoute
|
||||
? 'bg-red-600 hover:bg-red-700 text-white border-red-600 hover:border-red-700'
|
||||
: isDark
|
||||
? 'border-gray-700 text-gray-300 hover:bg-gray-800'
|
||||
: ''
|
||||
}`}
|
||||
onClick={handleLogout}
|
||||
>
|
||||
<LogOut className="w-4 h-4 mr-2" />
|
||||
Logout
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Mobile Actions */}
|
||||
@ -161,7 +195,7 @@ export function Navbar() {
|
||||
>
|
||||
<div className="flex flex-col p-4 sm:p-6 space-y-3 sm:space-y-4">
|
||||
{/* Mobile Navigation Links */}
|
||||
{!isUserDashboard && (
|
||||
{!isUserRoute && (
|
||||
<>
|
||||
<button
|
||||
onClick={() => scrollToSection("about")}
|
||||
@ -185,7 +219,7 @@ export function Navbar() {
|
||||
)}
|
||||
|
||||
<div className={`border-t pt-3 sm:pt-4 mt-3 sm:mt-4 space-y-2 sm:space-y-3 ${isDark ? 'border-gray-700' : 'border-gray-200'}`}>
|
||||
{!isUserDashboard && (
|
||||
{!isAuthenticated && !isUserDashboard && (
|
||||
<Button
|
||||
variant="outline"
|
||||
className={`w-full justify-start text-sm sm:text-base ${isDark ? 'border-gray-700 text-gray-300 hover:bg-gray-800' : ''}`}
|
||||
@ -197,14 +231,33 @@ export function Navbar() {
|
||||
Sign In
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
className="w-full justify-start bg-gradient-to-r from-rose-500 to-pink-600 hover:from-rose-600 hover:to-pink-700 text-white text-sm sm:text-base"
|
||||
asChild
|
||||
>
|
||||
<Link href="/book-now" onClick={() => setMobileMenuOpen(false)}>
|
||||
Book Now
|
||||
{!isUserDashboard && (
|
||||
<Link
|
||||
href="/book-now"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
className={`text-left text-sm sm:text-base font-medium py-2.5 sm:py-3 px-3 sm:px-4 rounded-lg transition-colors ${isDark ? 'text-gray-300 hover:bg-gray-800' : 'text-gray-700 hover:bg-gray-100'}`}
|
||||
>
|
||||
Book-Now
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
{isAuthenticated && (
|
||||
<Button
|
||||
variant="outline"
|
||||
className={`w-full justify-start text-sm sm:text-base ${
|
||||
isUserRoute
|
||||
? 'bg-red-600 hover:bg-red-700 text-white border-red-600 hover:border-red-700'
|
||||
: isDark
|
||||
? 'border-gray-700 text-gray-300 hover:bg-gray-800'
|
||||
: ''
|
||||
}`}
|
||||
onClick={() => {
|
||||
handleLogout();
|
||||
}}
|
||||
>
|
||||
<LogOut className="w-4 h-4 mr-2" />
|
||||
Logout
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
@ -43,8 +43,14 @@ export function useAuth() {
|
||||
// Check if user is authenticated
|
||||
const isAuthenticated = !!user && !!getStoredTokens().access;
|
||||
|
||||
// Check if user is admin (check both is_admin and isAdmin)
|
||||
const isAdmin = user?.is_admin === true || (user as any)?.isAdmin === true;
|
||||
// Check if user is admin (check multiple possible field names)
|
||||
const isAdmin =
|
||||
user?.is_admin === true ||
|
||||
(user as any)?.isAdmin === true ||
|
||||
(user as any)?.is_staff === true ||
|
||||
(user as any)?.isStaff === true ||
|
||||
(user as any)?.is_superuser === true ||
|
||||
(user as any)?.isSuperuser === true;
|
||||
|
||||
// Login mutation
|
||||
const loginMutation = useMutation({
|
||||
@ -109,8 +115,8 @@ export function useAuth() {
|
||||
const logout = useCallback(() => {
|
||||
clearAuthData();
|
||||
queryClient.clear();
|
||||
router.push("/login");
|
||||
}, [queryClient, router]);
|
||||
// Don't redirect here - let components handle redirect as needed
|
||||
}, [queryClient]);
|
||||
|
||||
// Login function
|
||||
const login = useCallback(
|
||||
|
||||
@ -76,6 +76,28 @@ async function handleResponse<T>(response: Response): Promise<T> {
|
||||
return data as T;
|
||||
}
|
||||
|
||||
// Helper function to normalize auth response
|
||||
function normalizeAuthResponse(data: AuthResponse): AuthResponse {
|
||||
// Normalize tokens: if tokens are at root level, move them to tokens object
|
||||
if (data.access && data.refresh && !data.tokens) {
|
||||
data.tokens = {
|
||||
access: data.access,
|
||||
refresh: data.refresh,
|
||||
};
|
||||
}
|
||||
|
||||
// Normalize user: only map isVerified to is_verified if needed
|
||||
if (data.user) {
|
||||
const user = data.user as any;
|
||||
if (user.isVerified !== undefined && user.is_verified === undefined) {
|
||||
user.is_verified = user.isVerified;
|
||||
}
|
||||
data.user = user;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// Register a new user
|
||||
export async function registerUser(input: RegisterInput): Promise<AuthResponse> {
|
||||
const response = await fetch(API_ENDPOINTS.auth.register, {
|
||||
@ -86,6 +108,29 @@ export async function registerUser(input: RegisterInput): Promise<AuthResponse>
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
|
||||
// Handle response - check if it's a 500 error that might indicate OTP sending failure
|
||||
// but user registration might have succeeded
|
||||
if (!response.ok && response.status === 500) {
|
||||
try {
|
||||
const data = await response.json();
|
||||
// If the error message mentions OTP or email sending, it might be a partial success
|
||||
const errorMessage = extractErrorMessage(data);
|
||||
if (errorMessage.toLowerCase().includes("otp") ||
|
||||
errorMessage.toLowerCase().includes("email") ||
|
||||
errorMessage.toLowerCase().includes("send") ||
|
||||
errorMessage.toLowerCase().includes("ssl") ||
|
||||
errorMessage.toLowerCase().includes("certificate")) {
|
||||
// Return a partial success response - user might be created, allow OTP resend
|
||||
// This allows the user to proceed to OTP verification and use resend OTP
|
||||
return {
|
||||
message: "User registered, but OTP email could not be sent. Please use resend OTP.",
|
||||
} as AuthResponse;
|
||||
}
|
||||
} catch {
|
||||
// If we can't parse the error, continue to normal error handling
|
||||
}
|
||||
}
|
||||
|
||||
return handleResponse<AuthResponse>(response);
|
||||
}
|
||||
|
||||
@ -100,23 +145,7 @@ export async function verifyOtp(input: VerifyOtpInput): Promise<AuthResponse> {
|
||||
});
|
||||
|
||||
const data = await handleResponse<AuthResponse>(response);
|
||||
|
||||
// Normalize response: if tokens are at root level, move them to tokens object
|
||||
if (data.access && data.refresh && !data.tokens) {
|
||||
data.tokens = {
|
||||
access: data.access,
|
||||
refresh: data.refresh,
|
||||
};
|
||||
}
|
||||
|
||||
// Normalize user: map isVerified to is_verified if needed
|
||||
if (data.user) {
|
||||
if (data.user.isVerified !== undefined && data.user.is_verified === undefined) {
|
||||
data.user.is_verified = data.user.isVerified;
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
return normalizeAuthResponse(data);
|
||||
}
|
||||
|
||||
// Login user
|
||||
@ -130,23 +159,7 @@ export async function loginUser(input: LoginInput): Promise<AuthResponse> {
|
||||
});
|
||||
|
||||
const data = await handleResponse<AuthResponse>(response);
|
||||
|
||||
// Normalize response: if tokens are at root level, move them to tokens object
|
||||
if (data.access && data.refresh && !data.tokens) {
|
||||
data.tokens = {
|
||||
access: data.access,
|
||||
refresh: data.refresh,
|
||||
};
|
||||
}
|
||||
|
||||
// Normalize user: map isVerified to is_verified if needed
|
||||
if (data.user) {
|
||||
if (data.user.isVerified !== undefined && data.user.is_verified === undefined) {
|
||||
data.user.is_verified = data.user.isVerified;
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
return normalizeAuthResponse(data);
|
||||
}
|
||||
|
||||
// Resend OTP
|
||||
@ -245,9 +258,7 @@ export function storeUser(user: User): void {
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
localStorage.setItem("auth_user", JSON.stringify(user));
|
||||
|
||||
// Also set cookie for middleware
|
||||
document.cookie = `auth_user=${JSON.stringify(user)}; path=/; max-age=${7 * 24 * 60 * 60}; SameSite=Lax`;
|
||||
document.cookie = `auth_user=${encodeURIComponent(JSON.stringify(user))}; path=/; max-age=${7 * 24 * 60 * 60}; SameSite=Lax`;
|
||||
}
|
||||
|
||||
// Get stored user
|
||||
|
||||
@ -11,9 +11,17 @@ export interface User {
|
||||
last_name: string;
|
||||
phone_number?: string;
|
||||
is_admin?: boolean;
|
||||
isAdmin?: boolean; // API uses camelCase
|
||||
is_staff?: boolean;
|
||||
isStaff?: boolean; // API uses camelCase
|
||||
is_superuser?: boolean;
|
||||
isSuperuser?: boolean; // API uses camelCase
|
||||
is_verified?: boolean;
|
||||
isVerified?: boolean; // API uses camelCase
|
||||
is_active?: boolean;
|
||||
isActive?: boolean; // API uses camelCase
|
||||
date_joined?: string;
|
||||
last_login?: string;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
@ -13,16 +13,31 @@ export function middleware(request: NextRequest) {
|
||||
|
||||
if (userStr) {
|
||||
try {
|
||||
const user = JSON.parse(userStr);
|
||||
isAdmin = user.is_admin === true;
|
||||
// Decode the user string if it's URL encoded
|
||||
const decodedUserStr = decodeURIComponent(userStr);
|
||||
const user = JSON.parse(decodedUserStr);
|
||||
// Check for admin status using multiple possible field names
|
||||
// Admin users must be verified (is_verified or isVerified must be true)
|
||||
const isVerified = user.is_verified === true || user.isVerified === true;
|
||||
const hasAdminRole =
|
||||
user.is_admin === true ||
|
||||
user.isAdmin === true ||
|
||||
user.is_staff === true ||
|
||||
user.isStaff === true ||
|
||||
user.is_superuser === true ||
|
||||
user.isSuperuser === true;
|
||||
|
||||
// User is admin only if they have admin role AND are verified
|
||||
isAdmin = hasAdminRole && isVerified;
|
||||
} catch {
|
||||
// Invalid user data
|
||||
// Invalid user data - silently fail and treat as non-admin
|
||||
}
|
||||
}
|
||||
|
||||
// Protected routes
|
||||
const isProtectedRoute = pathname.startsWith("/user") || pathname.startsWith("/admin");
|
||||
const isAdminRoute = pathname.startsWith("/admin");
|
||||
const isUserRoute = pathname.startsWith("/user");
|
||||
const isAuthRoute = pathname.startsWith("/login") || pathname.startsWith("/signup");
|
||||
|
||||
// Redirect unauthenticated users away from protected routes
|
||||
@ -34,12 +49,19 @@ export function middleware(request: NextRequest) {
|
||||
|
||||
// Redirect authenticated users away from auth routes
|
||||
if (isAuthRoute && isAuthenticated) {
|
||||
// Redirect based on user role
|
||||
const redirectPath = isAdmin ? "/admin/dashboard" : "/user/dashboard";
|
||||
return NextResponse.redirect(new URL(redirectPath, request.url));
|
||||
}
|
||||
|
||||
// Redirect admin users away from user routes
|
||||
if (isUserRoute && isAuthenticated && isAdmin) {
|
||||
return NextResponse.redirect(new URL("/admin/dashboard", request.url));
|
||||
}
|
||||
|
||||
// Redirect non-admin users away from admin routes
|
||||
if (isAdminRoute && (!isAuthenticated || !isAdmin)) {
|
||||
return NextResponse.redirect(new URL("/admin/dashboard", request.url));
|
||||
if (isAdminRoute && isAuthenticated && !isAdmin) {
|
||||
return NextResponse.redirect(new URL("/user/dashboard", request.url));
|
||||
}
|
||||
|
||||
return NextResponse.next();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user