From eb8a800eb76e64dedbc3236a9cb905d6bed258c9 Mon Sep 17 00:00:00 2001
From: iamkiddy
Date: Sun, 23 Nov 2025 13:29:31 +0000
Subject: [PATCH 01/10] Enhance authentication flow by integrating
@tanstack/react-query for improved data fetching, adding form validation in
Login and LoginDialog components, and updating user redirection logic
post-login. Also, include new dependencies: input-otp and zod for OTP input
handling and schema validation.
---
app/(auth)/login/page.tsx | 109 ++++++++-
app/(auth)/signup/page.tsx | 450 ++++++++++++++++++++++++++++++++++++
app/providers.tsx | 22 +-
components/LoginDialog.tsx | 201 ++++++++++++----
components/Navbar.tsx | 4 +-
components/ui/input-otp.tsx | 77 ++++++
components/ui/toaster.tsx | 25 +-
hooks/useAuth.ts | 178 ++++++++++++++
lib/actions/auth.ts | 289 +++++++++++++++++++++++
lib/api_urls.ts | 28 +++
lib/models/auth.ts | 48 ++++
lib/schema/auth.ts | 80 +++++++
middleware.ts | 53 +++++
package.json | 5 +-
pnpm-lock.yaml | 35 +++
15 files changed, 1543 insertions(+), 61 deletions(-)
create mode 100644 app/(auth)/signup/page.tsx
create mode 100644 components/ui/input-otp.tsx
create mode 100644 hooks/useAuth.ts
create mode 100644 lib/actions/auth.ts
create mode 100644 lib/api_urls.ts
create mode 100644 lib/models/auth.ts
create mode 100644 lib/schema/auth.ts
create mode 100644 middleware.ts
diff --git a/app/(auth)/login/page.tsx b/app/(auth)/login/page.tsx
index e67b95f..5aefcdd 100644
--- a/app/(auth)/login/page.tsx
+++ b/app/(auth)/login/page.tsx
@@ -1,20 +1,95 @@
"use client";
-import { useState } from "react";
+import { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
-import { Heart, Eye, EyeOff, X } from "lucide-react";
+import { Heart, Eye, EyeOff, X, Loader2 } from "lucide-react";
import Link from "next/link";
import Image from "next/image";
-import { useRouter } from "next/navigation";
+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 { toast } from "sonner";
export default function Login() {
const { theme } = useAppTheme();
const isDark = theme === "dark";
const [showPassword, setShowPassword] = useState(false);
const [rememberMe, setRememberMe] = useState(false);
+ const [formData, setFormData] = useState({
+ email: "",
+ password: "",
+ });
+ const [errors, setErrors] = useState>>({});
const router = useRouter();
+ const searchParams = useSearchParams();
+ const { login, isAuthenticated, loginMutation } = useAuth();
+
+ // Redirect if already authenticated
+ useEffect(() => {
+ if (isAuthenticated) {
+ const redirect = searchParams.get("redirect") || "/admin/dashboard";
+ router.push(redirect);
+ }
+ }, [isAuthenticated, router, searchParams]);
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setErrors({});
+
+ // Validate form
+ const validation = loginSchema.safeParse(formData);
+ if (!validation.success) {
+ const fieldErrors: Partial> = {};
+ validation.error.issues.forEach((err) => {
+ if (err.path[0]) {
+ fieldErrors[err.path[0] as keyof LoginInput] = err.message;
+ }
+ });
+ setErrors(fieldErrors);
+ return;
+ }
+
+ try {
+ const result = await login(formData);
+
+ 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 =
+ user.is_admin === true ||
+ (user as any)?.isAdmin === true ||
+ (user as any)?.is_staff === true ||
+ (user as any)?.isStaff === 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");
+ }
+ }
+ } 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
+ if (errors[field]) {
+ setErrors((prev) => ({ ...prev, [field]: undefined }));
+ }
+ };
return (
@@ -70,10 +145,7 @@ export default function Login() {
{/* Login Form */}
-
+ )}
+
+ {/* Signup Form */}
+ {step === "signup" && (
+
+ )}
+
+ {/* OTP Verification Form */}
+ {step === "verify" && (
+
+ )}
);
-}
\ No newline at end of file
+}
diff --git a/app/(auth)/signup/page.tsx b/app/(auth)/signup/page.tsx
index 898cd16..ca29c90 100644
--- a/app/(auth)/signup/page.tsx
+++ b/app/(auth)/signup/page.tsx
@@ -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.";
diff --git a/app/(pages)/book-now/page.tsx b/app/(pages)/book-now/page.tsx
index 07e50e0..b8528af 100644
--- a/app/(pages)/book-now/page.tsx
+++ b/app/(pages)/book-now/page.tsx
@@ -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(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() {
+
+ {/* Logout Button - Only show when authenticated */}
+ {isAuthenticated && (
+
+
+
+ )}
>
)}
diff --git a/components/Navbar.tsx b/components/Navbar.tsx
index a3a55a2..dcd863a 100644
--- a/components/Navbar.tsx
+++ b/components/Navbar.tsx
@@ -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() {
{/* Desktop Navigation */}
- {!isUserDashboard && (
+ {!isUserRoute && (