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";
|
} from "lucide-react";
|
||||||
import { useAppTheme } from "@/components/ThemeProvider";
|
import { useAppTheme } from "@/components/ThemeProvider";
|
||||||
import { ThemeToggle } from "@/components/ThemeToggle";
|
import { ThemeToggle } from "@/components/ThemeToggle";
|
||||||
|
import { useAuth } from "@/hooks/useAuth";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
export function Header() {
|
export function Header() {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
@ -25,6 +27,14 @@ export function Header() {
|
|||||||
const [userMenuOpen, setUserMenuOpen] = useState(false);
|
const [userMenuOpen, setUserMenuOpen] = useState(false);
|
||||||
const { theme } = useAppTheme();
|
const { theme } = useAppTheme();
|
||||||
const isDark = theme === "dark";
|
const isDark = theme === "dark";
|
||||||
|
const { logout } = useAuth();
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
setUserMenuOpen(false);
|
||||||
|
logout();
|
||||||
|
toast.success("Logged out successfully");
|
||||||
|
router.push("/");
|
||||||
|
};
|
||||||
|
|
||||||
// Mock notifications data
|
// Mock notifications data
|
||||||
const notifications = [
|
const notifications = [
|
||||||
@ -209,10 +219,7 @@ export function Header() {
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => {
|
onClick={handleLogout}
|
||||||
setUserMenuOpen(false);
|
|
||||||
router.push("/");
|
|
||||||
}}
|
|
||||||
className={`w-full flex items-center gap-3 px-4 py-3 justify-start transition-colors cursor-pointer ${
|
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"
|
isDark ? "hover:bg-gray-800" : "hover:bg-gray-50"
|
||||||
}`}
|
}`}
|
||||||
|
|||||||
@ -14,6 +14,8 @@ import {
|
|||||||
Heart,
|
Heart,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useAppTheme } from "@/components/ThemeProvider";
|
import { useAppTheme } from "@/components/ThemeProvider";
|
||||||
|
import { useAuth } from "@/hooks/useAuth";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ label: "Dashboard", icon: LayoutGrid, href: "/admin/dashboard" },
|
{ label: "Dashboard", icon: LayoutGrid, href: "/admin/dashboard" },
|
||||||
@ -26,6 +28,14 @@ export default function SideNav() {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { theme } = useAppTheme();
|
const { theme } = useAppTheme();
|
||||||
const isDark = theme === "dark";
|
const isDark = theme === "dark";
|
||||||
|
const { logout } = useAuth();
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
setOpen(false);
|
||||||
|
logout();
|
||||||
|
toast.success("Logged out successfully");
|
||||||
|
router.push("/");
|
||||||
|
};
|
||||||
|
|
||||||
const getActiveIndex = () => {
|
const getActiveIndex = () => {
|
||||||
return navItems.findIndex((item) => pathname?.includes(item.href)) ?? -1;
|
return navItems.findIndex((item) => pathname?.includes(item.href)) ?? -1;
|
||||||
@ -176,10 +186,7 @@ export default function SideNav() {
|
|||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => {
|
onClick={handleLogout}
|
||||||
setOpen(false);
|
|
||||||
router.push("/");
|
|
||||||
}}
|
|
||||||
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 ${
|
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
|
isDark
|
||||||
? "text-gray-300 hover:bg-gray-800 hover:text-rose-300"
|
? "text-gray-300 hover:bg-gray-800 hover:text-rose-300"
|
||||||
|
|||||||
@ -3,48 +3,127 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Heart, Eye, EyeOff, X, Loader2 } from "lucide-react";
|
import {
|
||||||
import Link from "next/link";
|
InputOTP,
|
||||||
|
InputOTPGroup,
|
||||||
|
InputOTPSlot,
|
||||||
|
} from "@/components/ui/input-otp";
|
||||||
|
import { Heart, Eye, EyeOff, X, Loader2, CheckCircle2 } from "lucide-react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
import { useRouter, useSearchParams } from "next/navigation";
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
import { useAppTheme } from "@/components/ThemeProvider";
|
import { useAppTheme } from "@/components/ThemeProvider";
|
||||||
import { useAuth } from "@/hooks/useAuth";
|
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";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
type Step = "login" | "signup" | "verify";
|
||||||
|
|
||||||
export default function Login() {
|
export default function Login() {
|
||||||
const { theme } = useAppTheme();
|
const { theme } = useAppTheme();
|
||||||
const isDark = theme === "dark";
|
const isDark = theme === "dark";
|
||||||
|
const [step, setStep] = useState<Step>("login");
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
const [showPassword2, setShowPassword2] = useState(false);
|
||||||
const [rememberMe, setRememberMe] = 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: "",
|
email: "",
|
||||||
password: "",
|
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 router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
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
|
// Redirect if already authenticated
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isAuthenticated) {
|
if (isAuthenticated) {
|
||||||
const redirect = searchParams.get("redirect") || "/admin/dashboard";
|
// Use a small delay to ensure cookies are set and middleware has processed
|
||||||
router.push(redirect);
|
const timer = setTimeout(() => {
|
||||||
}
|
// Always redirect based on user role, ignore redirect parameter if user is admin
|
||||||
}, [isAuthenticated, router, searchParams]);
|
const redirectParam = searchParams.get("redirect");
|
||||||
|
const defaultRedirect = isAdmin ? "/admin/dashboard" : "/user/dashboard";
|
||||||
|
const finalRedirect = isAdmin ? "/admin/dashboard" : (redirectParam || defaultRedirect);
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
// Use window.location.href to ensure full page reload and cookie reading
|
||||||
|
window.location.href = finalRedirect;
|
||||||
|
}, 200);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [isAuthenticated, isAdmin, searchParams]);
|
||||||
|
|
||||||
|
// Handle login
|
||||||
|
const handleLogin = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setErrors({});
|
setErrors({});
|
||||||
|
|
||||||
// Validate form
|
// Validate form
|
||||||
const validation = loginSchema.safeParse(formData);
|
const validation = loginSchema.safeParse(loginData);
|
||||||
if (!validation.success) {
|
if (!validation.success) {
|
||||||
const fieldErrors: Partial<Record<keyof LoginInput, string>> = {};
|
const fieldErrors: Partial<Record<string, string>> = {};
|
||||||
validation.error.issues.forEach((err) => {
|
validation.error.issues.forEach((err) => {
|
||||||
if (err.path[0]) {
|
if (err.path[0]) {
|
||||||
fieldErrors[err.path[0] as keyof LoginInput] = err.message;
|
fieldErrors[err.path[0] as string] = err.message;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
setErrors(fieldErrors);
|
setErrors(fieldErrors);
|
||||||
@ -52,40 +131,214 @@ export default function Login() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await login(formData);
|
const result = await login(loginData);
|
||||||
|
|
||||||
if (result.tokens && result.user) {
|
if (result.tokens && result.user) {
|
||||||
toast.success("Login successful!");
|
toast.success("Login successful!");
|
||||||
|
// Wait a moment for cookies to be set, then redirect
|
||||||
// Normalize user data
|
// Check if user is admin/staff/superuser - check all possible field names
|
||||||
const user = result.user;
|
const user = result.user as any;
|
||||||
// Check for admin status - check multiple possible field names
|
const userIsAdmin =
|
||||||
const isAdmin =
|
|
||||||
user.is_admin === true ||
|
user.is_admin === true ||
|
||||||
(user as any)?.isAdmin === true ||
|
user.isAdmin === true ||
|
||||||
(user as any)?.is_staff === true ||
|
user.is_staff === true ||
|
||||||
(user as any)?.isStaff === true;
|
user.isStaff === true ||
|
||||||
|
user.is_superuser === true ||
|
||||||
|
user.isSuperuser === true;
|
||||||
|
|
||||||
// Redirect based on user role
|
// Wait longer for cookies to be set and middleware to process
|
||||||
const redirect = searchParams.get("redirect");
|
setTimeout(() => {
|
||||||
if (redirect) {
|
// Always redirect based on user role, ignore redirect parameter if user is admin
|
||||||
router.push(redirect);
|
// This ensures admins always go to admin dashboard
|
||||||
} else {
|
const defaultRedirect = userIsAdmin ? "/admin/dashboard" : "/user/dashboard";
|
||||||
// Default to admin dashboard
|
|
||||||
router.push("/admin/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) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : "Login failed. Please try again.";
|
const errorMessage = error instanceof Error ? error.message : "Login failed. Please try again.";
|
||||||
toast.error(errorMessage);
|
toast.error(errorMessage);
|
||||||
// Don't set field errors for server errors, only show toast
|
|
||||||
setErrors({});
|
setErrors({});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChange = (field: keyof LoginInput, value: string) => {
|
// Handle signup
|
||||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
const handleSignup = async (e: React.FormEvent) => {
|
||||||
// Clear error when user starts typing
|
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]) {
|
if (errors[field]) {
|
||||||
setErrors((prev) => ({ ...prev, [field]: undefined }));
|
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>
|
<span className="text-white text-xl font-semibold">Attune Heart Therapy</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Centered White Card */}
|
||||||
|
|
||||||
{/* Centered White Card - Login Form */}
|
|
||||||
<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'}`}>
|
<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 */}
|
{/* Header with Close Button */}
|
||||||
<div className="flex items-start justify-between mb-2">
|
<div className="flex items-start justify-between mb-2">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
{/* Heading */}
|
{/* 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">
|
<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">
|
||||||
Welcome back
|
{step === "login" && "Welcome back"}
|
||||||
|
{step === "signup" && "Create an account"}
|
||||||
|
{step === "verify" && "Verify your email"}
|
||||||
</h1>
|
</h1>
|
||||||
{/* Sign Up Prompt */}
|
{/* Subtitle */}
|
||||||
|
{step === "login" && (
|
||||||
<p className={`mb-6 ${isDark ? 'text-gray-400' : 'text-gray-600'}`}>
|
<p className={`mb-6 ${isDark ? 'text-gray-400' : 'text-gray-600'}`}>
|
||||||
New to Attune Heart Therapy?{" "}
|
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
|
Sign up
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</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>
|
</div>
|
||||||
{/* Close Button */}
|
{/* Close Button */}
|
||||||
<Button
|
<Button
|
||||||
onClick={() => router.back()}
|
onClick={() => router.back()}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
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"
|
aria-label="Close"
|
||||||
>
|
>
|
||||||
<X className="w-5 h-5" />
|
<X className="w-5 h-5" />
|
||||||
@ -145,7 +425,8 @@ export default function Login() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Login Form */}
|
{/* Login Form */}
|
||||||
<form className="space-y-6" onSubmit={handleSubmit}>
|
{step === "login" && (
|
||||||
|
<form className="space-y-6" onSubmit={handleLogin}>
|
||||||
{/* Email Field */}
|
{/* Email Field */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label htmlFor="email" className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-black'}`}>
|
<label htmlFor="email" className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-black'}`}>
|
||||||
@ -155,11 +436,14 @@ export default function Login() {
|
|||||||
id="email"
|
id="email"
|
||||||
type="email"
|
type="email"
|
||||||
placeholder="Email address"
|
placeholder="Email address"
|
||||||
value={formData.email}
|
value={loginData.email}
|
||||||
onChange={(e) => handleChange("email", e.target.value)}
|
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' : ''}`}
|
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
|
required
|
||||||
/>
|
/>
|
||||||
|
{errors.email && (
|
||||||
|
<p className="text-sm text-red-500">{errors.email}</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Password Field */}
|
{/* Password Field */}
|
||||||
@ -172,8 +456,8 @@ export default function Login() {
|
|||||||
id="password"
|
id="password"
|
||||||
type={showPassword ? "text" : "password"}
|
type={showPassword ? "text" : "password"}
|
||||||
placeholder="Your password"
|
placeholder="Your password"
|
||||||
value={formData.password}
|
value={loginData.password}
|
||||||
onChange={(e) => handleChange("password", e.target.value)}
|
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' : ''}`}
|
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
|
required
|
||||||
/>
|
/>
|
||||||
@ -201,7 +485,7 @@ export default function Login() {
|
|||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loginMutation.isPending}
|
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 ? (
|
{loginMutation.isPending ? (
|
||||||
<>
|
<>
|
||||||
@ -224,15 +508,287 @@ export default function Login() {
|
|||||||
/>
|
/>
|
||||||
<span className={isDark ? 'text-gray-300' : 'text-black'}>Remember me</span>
|
<span className={isDark ? 'text-gray-300' : 'text-black'}>Remember me</span>
|
||||||
</label>
|
</label>
|
||||||
<Link
|
<button
|
||||||
href="/forgot-password"
|
type="button"
|
||||||
className={`font-medium ${isDark ? 'text-blue-400 hover:text-blue-300' : 'text-blue-600 hover:text-blue-700'}`}
|
className={`font-medium ${isDark ? 'text-blue-400 hover:text-blue-300' : 'text-blue-600 hover:text-blue-700'}`}
|
||||||
>
|
>
|
||||||
Forgot password?
|
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>
|
</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>
|
</form>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -69,13 +69,21 @@ export default function Signup() {
|
|||||||
try {
|
try {
|
||||||
const result = await register(formData);
|
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.");
|
toast.success("Registration successful! Please check your email for OTP verification.");
|
||||||
setRegisteredEmail(formData.email);
|
// Redirect to login page with verify step
|
||||||
setOtpData({ email: formData.email, otp: "" });
|
router.push(`/login?verify=true&email=${encodeURIComponent(formData.email)}`);
|
||||||
setStep("verify");
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : "Registration failed. Please try again.";
|
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);
|
toast.error(errorMessage);
|
||||||
// Don't set field errors for server errors, only show toast
|
// Don't set field errors for server errors, only show toast
|
||||||
setErrors({});
|
setErrors({});
|
||||||
@ -105,9 +113,9 @@ export default function Signup() {
|
|||||||
// If verification is successful (no error thrown), show success and redirect
|
// If verification is successful (no error thrown), show success and redirect
|
||||||
toast.success("Email verified successfully! Redirecting to login...");
|
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(() => {
|
setTimeout(() => {
|
||||||
router.push("/login");
|
router.push(`/login?email=${encodeURIComponent(otpData.email)}`);
|
||||||
}, 1500);
|
}, 1500);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : "OTP verification failed. Please try again.";
|
const errorMessage = error instanceof Error ? error.message : "OTP verification failed. Please try again.";
|
||||||
|
|||||||
@ -23,11 +23,14 @@ import {
|
|||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
Loader2,
|
Loader2,
|
||||||
|
LogOut,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { LoginDialog } from "@/components/LoginDialog";
|
import { LoginDialog } from "@/components/LoginDialog";
|
||||||
|
import { useAuth } from "@/hooks/useAuth";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
ID: number;
|
ID: number;
|
||||||
@ -73,6 +76,7 @@ export default function BookNowPage() {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { theme } = useAppTheme();
|
const { theme } = useAppTheme();
|
||||||
const isDark = theme === "dark";
|
const isDark = theme === "dark";
|
||||||
|
const { isAuthenticated, logout } = useAuth();
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
firstName: "",
|
firstName: "",
|
||||||
lastName: "",
|
lastName: "",
|
||||||
@ -87,6 +91,12 @@ export default function BookNowPage() {
|
|||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [showLoginDialog, setShowLoginDialog] = useState(false);
|
const [showLoginDialog, setShowLoginDialog] = useState(false);
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
logout();
|
||||||
|
toast.success("Logged out successfully");
|
||||||
|
router.push("/");
|
||||||
|
};
|
||||||
|
|
||||||
// Handle submit button click
|
// Handle submit button click
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -660,6 +670,20 @@ export default function BookNowPage() {
|
|||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|||||||
@ -2,13 +2,15 @@
|
|||||||
|
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
import { Button } from "@/components/ui/button";
|
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 { ThemeToggle } from "@/components/ThemeToggle";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { LoginDialog } from "@/components/LoginDialog";
|
import { LoginDialog } from "@/components/LoginDialog";
|
||||||
import { useRouter, usePathname } from "next/navigation";
|
import { useRouter, usePathname } from "next/navigation";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useAppTheme } from "@/components/ThemeProvider";
|
import { useAppTheme } from "@/components/ThemeProvider";
|
||||||
|
import { useAuth } from "@/hooks/useAuth";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
export function Navbar() {
|
export function Navbar() {
|
||||||
const { theme } = useAppTheme();
|
const { theme } = useAppTheme();
|
||||||
@ -18,6 +20,9 @@ export function Navbar() {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const isUserDashboard = pathname?.startsWith("/user/dashboard");
|
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 scrollToSection = (id: string) => {
|
||||||
const element = document.getElementById(id);
|
const element = document.getElementById(id);
|
||||||
@ -33,6 +38,13 @@ export function Navbar() {
|
|||||||
setMobileMenuOpen(false);
|
setMobileMenuOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
logout();
|
||||||
|
toast.success("Logged out successfully");
|
||||||
|
setMobileMenuOpen(false);
|
||||||
|
router.push("/");
|
||||||
|
};
|
||||||
|
|
||||||
// Close mobile menu when clicking outside
|
// Close mobile menu when clicking outside
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (mobileMenuOpen) {
|
if (mobileMenuOpen) {
|
||||||
@ -73,7 +85,7 @@ export function Navbar() {
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* Desktop Navigation */}
|
{/* Desktop Navigation */}
|
||||||
{!isUserDashboard && (
|
{!isUserRoute && (
|
||||||
<div className="hidden lg:flex items-center gap-4 xl:gap-6">
|
<div className="hidden lg:flex items-center gap-4 xl:gap-6">
|
||||||
<button
|
<button
|
||||||
onClick={() => scrollToSection("about")}
|
onClick={() => scrollToSection("about")}
|
||||||
@ -98,7 +110,7 @@ export function Navbar() {
|
|||||||
|
|
||||||
{/* Desktop Actions */}
|
{/* Desktop Actions */}
|
||||||
<div className="hidden lg:flex items-center gap-2">
|
<div className="hidden lg:flex items-center gap-2">
|
||||||
{!isUserDashboard && (
|
{!isAuthenticated && !isUserDashboard && (
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@ -109,9 +121,31 @@ export function Navbar() {
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<ThemeToggle />
|
<ThemeToggle />
|
||||||
<Button size="sm" className="hover:opacity-90 hover:scale-105 transition-all text-xs sm:text-sm" asChild>
|
{!isUserDashboard && (
|
||||||
<a href="/book-now">Book Now</a>
|
<Link
|
||||||
</Button>
|
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>
|
</div>
|
||||||
|
|
||||||
{/* Mobile Actions */}
|
{/* 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">
|
<div className="flex flex-col p-4 sm:p-6 space-y-3 sm:space-y-4">
|
||||||
{/* Mobile Navigation Links */}
|
{/* Mobile Navigation Links */}
|
||||||
{!isUserDashboard && (
|
{!isUserRoute && (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
onClick={() => scrollToSection("about")}
|
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'}`}>
|
<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
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className={`w-full justify-start text-sm sm:text-base ${isDark ? 'border-gray-700 text-gray-300 hover:bg-gray-800' : ''}`}
|
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
|
Sign In
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button
|
{!isUserDashboard && (
|
||||||
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"
|
<Link
|
||||||
asChild
|
href="/book-now"
|
||||||
>
|
onClick={() => setMobileMenuOpen(false)}
|
||||||
<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
|
>
|
||||||
|
Book-Now
|
||||||
</Link>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|||||||
@ -43,8 +43,14 @@ export function useAuth() {
|
|||||||
// Check if user is authenticated
|
// Check if user is authenticated
|
||||||
const isAuthenticated = !!user && !!getStoredTokens().access;
|
const isAuthenticated = !!user && !!getStoredTokens().access;
|
||||||
|
|
||||||
// Check if user is admin (check both is_admin and isAdmin)
|
// Check if user is admin (check multiple possible field names)
|
||||||
const isAdmin = user?.is_admin === true || (user as any)?.isAdmin === true;
|
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
|
// Login mutation
|
||||||
const loginMutation = useMutation({
|
const loginMutation = useMutation({
|
||||||
@ -109,8 +115,8 @@ export function useAuth() {
|
|||||||
const logout = useCallback(() => {
|
const logout = useCallback(() => {
|
||||||
clearAuthData();
|
clearAuthData();
|
||||||
queryClient.clear();
|
queryClient.clear();
|
||||||
router.push("/login");
|
// Don't redirect here - let components handle redirect as needed
|
||||||
}, [queryClient, router]);
|
}, [queryClient]);
|
||||||
|
|
||||||
// Login function
|
// Login function
|
||||||
const login = useCallback(
|
const login = useCallback(
|
||||||
|
|||||||
@ -76,6 +76,28 @@ async function handleResponse<T>(response: Response): Promise<T> {
|
|||||||
return data as 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
|
// Register a new user
|
||||||
export async function registerUser(input: RegisterInput): Promise<AuthResponse> {
|
export async function registerUser(input: RegisterInput): Promise<AuthResponse> {
|
||||||
const response = await fetch(API_ENDPOINTS.auth.register, {
|
const response = await fetch(API_ENDPOINTS.auth.register, {
|
||||||
@ -86,6 +108,29 @@ export async function registerUser(input: RegisterInput): Promise<AuthResponse>
|
|||||||
body: JSON.stringify(input),
|
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);
|
return handleResponse<AuthResponse>(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,23 +145,7 @@ export async function verifyOtp(input: VerifyOtpInput): Promise<AuthResponse> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const data = await handleResponse<AuthResponse>(response);
|
const data = await handleResponse<AuthResponse>(response);
|
||||||
|
return normalizeAuthResponse(data);
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Login user
|
// Login user
|
||||||
@ -130,23 +159,7 @@ export async function loginUser(input: LoginInput): Promise<AuthResponse> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const data = await handleResponse<AuthResponse>(response);
|
const data = await handleResponse<AuthResponse>(response);
|
||||||
|
return normalizeAuthResponse(data);
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resend OTP
|
// Resend OTP
|
||||||
@ -245,9 +258,7 @@ export function storeUser(user: User): void {
|
|||||||
if (typeof window === "undefined") return;
|
if (typeof window === "undefined") return;
|
||||||
|
|
||||||
localStorage.setItem("auth_user", JSON.stringify(user));
|
localStorage.setItem("auth_user", JSON.stringify(user));
|
||||||
|
document.cookie = `auth_user=${encodeURIComponent(JSON.stringify(user))}; path=/; max-age=${7 * 24 * 60 * 60}; SameSite=Lax`;
|
||||||
// Also set cookie for middleware
|
|
||||||
document.cookie = `auth_user=${JSON.stringify(user)}; path=/; max-age=${7 * 24 * 60 * 60}; SameSite=Lax`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get stored user
|
// Get stored user
|
||||||
|
|||||||
@ -11,9 +11,17 @@ export interface User {
|
|||||||
last_name: string;
|
last_name: string;
|
||||||
phone_number?: string;
|
phone_number?: string;
|
||||||
is_admin?: boolean;
|
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;
|
is_verified?: boolean;
|
||||||
isVerified?: boolean; // API uses camelCase
|
isVerified?: boolean; // API uses camelCase
|
||||||
|
is_active?: boolean;
|
||||||
|
isActive?: boolean; // API uses camelCase
|
||||||
date_joined?: string;
|
date_joined?: string;
|
||||||
|
last_login?: string;
|
||||||
created_at?: string;
|
created_at?: string;
|
||||||
updated_at?: string;
|
updated_at?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,16 +13,31 @@ export function middleware(request: NextRequest) {
|
|||||||
|
|
||||||
if (userStr) {
|
if (userStr) {
|
||||||
try {
|
try {
|
||||||
const user = JSON.parse(userStr);
|
// Decode the user string if it's URL encoded
|
||||||
isAdmin = user.is_admin === true;
|
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 {
|
} catch {
|
||||||
// Invalid user data
|
// Invalid user data - silently fail and treat as non-admin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Protected routes
|
// Protected routes
|
||||||
const isProtectedRoute = pathname.startsWith("/user") || pathname.startsWith("/admin");
|
const isProtectedRoute = pathname.startsWith("/user") || pathname.startsWith("/admin");
|
||||||
const isAdminRoute = pathname.startsWith("/admin");
|
const isAdminRoute = pathname.startsWith("/admin");
|
||||||
|
const isUserRoute = pathname.startsWith("/user");
|
||||||
const isAuthRoute = pathname.startsWith("/login") || pathname.startsWith("/signup");
|
const isAuthRoute = pathname.startsWith("/login") || pathname.startsWith("/signup");
|
||||||
|
|
||||||
// Redirect unauthenticated users away from protected routes
|
// Redirect unauthenticated users away from protected routes
|
||||||
@ -34,12 +49,19 @@ export function middleware(request: NextRequest) {
|
|||||||
|
|
||||||
// Redirect authenticated users away from auth routes
|
// Redirect authenticated users away from auth routes
|
||||||
if (isAuthRoute && isAuthenticated) {
|
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));
|
return NextResponse.redirect(new URL("/admin/dashboard", request.url));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redirect non-admin users away from admin routes
|
// Redirect non-admin users away from admin routes
|
||||||
if (isAdminRoute && (!isAuthenticated || !isAdmin)) {
|
if (isAdminRoute && isAuthenticated && !isAdmin) {
|
||||||
return NextResponse.redirect(new URL("/admin/dashboard", request.url));
|
return NextResponse.redirect(new URL("/user/dashboard", request.url));
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.next();
|
return NextResponse.next();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user