Repository: GarudaHacks/gh-portal
Pull request #32: feat: email verification
Author: login: hib4
Labels: name: Review effort 4/5
Created: 2025-05-23T08:10:32Z | Merged: 2025-05-23T08:13:27Z

### **PR Type**
Enhancement


___

### **Description**
- Add full email verification flow support

- Enforce verification before granting access

- Introduce polling component for verification

- Block unverified users in auth context


___



### **Changes walkthrough** 📝
<table><thead><tr><th></th><th align="left">Relevant files</th></tr></thead><tbody><tr><td><strong>Enhancement</strong></td><td><table>
<tr>
  <td>
    <details>
      <summary><strong>Auth.tsx</strong><dd><code>Add email verification route handling</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

src/components/Auth.tsx

<li>Import and use <code>useSearchParams</code> hook<br> <li> Skip redirect when mode=verify-email<br> <li> Add <code>VERIFY_EMAIL</code> state mode<br> <li> Render <code>AuthVerificationComponent</code>


</details>


  </td>
  <td><a href="https://github.com/GarudaHacks/gh-portal/pull/32/files#diff-64efe290f74bd3ddbbe451ed5eb405817010c42485e2d8244b1ef4bf5b6fdc3d">+15/-8</a>&nbsp; &nbsp; </td>

</tr>

<tr>
  <td>
    <details>
      <summary><strong>AuthLoginComponent.tsx</strong><dd><code>Enforce email verification on login</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

src/components/AuthLoginComponent.tsx

<li>After login, call <code>/api/auth/session-check</code><br> <li> Redirect unverified users to verify page<br> <li> Navigate to home on verified login


</details>


  </td>
  <td><a href="https://github.com/GarudaHacks/gh-portal/pull/32/files#diff-b522ad906186573f5eed86fe96739beeffada5ca731e6d5fdcd245f323b6c5e8">+23/-8</a>&nbsp; &nbsp; </td>

</tr>

<tr>
  <td>
    <details>
      <summary><strong>AuthRegisterComponent.tsx</strong><dd><code>Redirect to email verification after signup</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

src/components/AuthRegisterComponent.tsx

<li>Update signup success toast to prompt verification<br> <li> Redirect new users to verify-email mode<br> <li> Minor formatting and import cleanup


</details>


  </td>
  <td><a href="https://github.com/GarudaHacks/gh-portal/pull/32/files#diff-0d4ceb1343e69af643ae118c65226cb608bdb3d5c62e020aa47b839f9064e9d6">+196/-175</a></td>

</tr>

<tr>
  <td>
    <details>
      <summary><strong>AuthVerificationComponent.tsx</strong><dd><code>Add email verification component</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

src/components/AuthVerificationComponent.tsx

<li>New component for email verification UI<br> <li> Poll session-check every 5 seconds<br> <li> Allow resending verification email<br> <li> Provide back-to-login option


</details>


  </td>
  <td><a href="https://github.com/GarudaHacks/gh-portal/pull/32/files#diff-a04a5b3e878473c8fbd5ca6120d21e2342492a906a4f736afff5e88326720cb9">+131/-0</a>&nbsp; </td>

</tr>

<tr>
  <td>
    <details>
      <summary><strong>AuthContext.tsx</strong><dd><code>Block unverified users in auth context</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

src/context/AuthContext.tsx

<li>Only set user when <code>emailVerified</code> is true<br> <li> Nullify user for unverified sessions<br> <li> Ensure signup leaves user unverified


</details>


  </td>
  <td><a href="https://github.com/GarudaHacks/gh-portal/pull/32/files#diff-18818acbefb343d45b10cdd411ea121ebbf4b9dee1824a3dc449fa1bdbbb29f9">+12/-3</a>&nbsp; &nbsp; </td>

</tr>
</table></td></tr></tr></tbody></table>

___

> <details> <summary>  Need help?</summary><li>Type <code>/help how to ...</code> in the comments thread for any questions about PR-Agent usage.</li><li>Check out the <a href="https://qodo-merge-docs.qodo.ai/usage-guide/">documentation</a> for more information.</li></details>


Diff:
diff --git a/src/components/Auth.tsx b/src/components/Auth.tsx
index 81399df..618395c 100644
--- a/src/components/Auth.tsx
+++ b/src/components/Auth.tsx
@@ -1,11 +1,12 @@
-import React, { useEffect, useState } from "react";
-import { useNavigate } from "react-router-dom";
+import { useEffect, useState } from "react";
+import { useNavigate, useSearchParams } from "react-router-dom";
 import { useAuth } from "../context/AuthContext";
 
 import ghLogo from "/images/logo/gh_logo.svg";
 import AuthLoginComponent from "@/components/AuthLoginComponent.tsx";
 import AuthRegisterComponent from "./AuthRegisterComponent";
 import AuthForgotPasswordComponent from "./AuthForgotPasswordComponent";
+import AuthVerificationComponent from "./AuthVerificationComponent";
 
 export const getAuthErrorMessage = (error: any) => {
   const errorCode = error.code || "";
@@ -25,17 +26,22 @@ export const getAuthErrorMessage = (error: any) => {
 
 function Auth() {
   const navigate = useNavigate();
+  const [searchParams] = useSearchParams();
   const { user, loading } = useAuth();
 
   useEffect(() => {
     if (!loading && user) {
+      // Check if we're on the verification page
+      if (searchParams.get("mode") === "verify-email") {
+        return;
+      }
       navigate("/home");
     }
-  }, [user, navigate, loading]);
+  }, [user, navigate, loading, searchParams]);
 
-  const [mode, setMode] = useState<"LOGIN" | "SIGNUP" | "FORGOT_PASSWORD">(
-    "LOGIN"
-  );
+  const [mode, setMode] = useState<
+    "LOGIN" | "SIGNUP" | "FORGOT_PASSWORD" | "VERIFY_EMAIL"
+  >(searchParams.get("mode") === "verify-email" ? "VERIFY_EMAIL" : "LOGIN");
 
   return (
     <div className="flex items-center justify-center h-screen min-w-screen auth">
@@ -45,6 +51,7 @@ function Auth() {
         {mode === "LOGIN" && <AuthLoginComponent />}
         {mode === "SIGNUP" && <AuthRegisterComponent />}
         {mode === "FORGOT_PASSWORD" && <AuthForgotPasswordComponent />}
+        {mode === "VERIFY_EMAIL" && <AuthVerificationComponent />}
 
         {/* Toggle Between Login and Signup */}
         <p className="text-center text-sm mt-4">
@@ -75,7 +82,7 @@ function Auth() {
                 Log in
               </span>
             </>
-          ) : (
+          ) : mode === "FORGOT_PASSWORD" ? (
             <>
               Remember your password?
               <span
@@ -85,7 +92,7 @@ function Auth() {
                 Log in
               </span>
             </>
-          )}
+          ) : null}
         </p>
       </div>
     </div>
diff --git a/src/components/AuthLoginComponent.tsx b/src/components/AuthLoginComponent.tsx
index e955d39..089854d 100644
--- a/src/components/AuthLoginComponent.tsx
+++ b/src/components/AuthLoginComponent.tsx
@@ -52,10 +52,28 @@ export default function AuthLoginComponent() {
       if (error) {
         setError(error.message || "Login failed");
         return;
-      } else {
-        toast.success("Successfully logged in!");
-        navigate("/home");
       }
+
+      // Check if email is verified
+      const response = await fetch("/api/auth/session-check", {
+        method: "GET",
+        headers: {
+          "Content-Type": "application/json",
+        },
+        credentials: "include",
+      });
+
+      const sessionData = await response.json();
+
+      if (response.ok && !sessionData.data.user?.emailVerified) {
+        toast.error("Please verify your email first");
+        // Force navigation to verification page
+        window.location.href = "/auth?mode=verify-email";
+        return;
+      }
+
+      toast.success("Successfully logged in!");
+      window.location.href = "/home";
     } catch (err) {
       setError("An unexpected error occurred");
       console.error(err);
@@ -103,7 +121,7 @@ export default function AuthLoginComponent() {
     <div>
       <h2 className="text-2xl font-semibold">Login into your account</h2>
       <p className="text-sm mt-2">
-        Join 400+ hackers at SEA’s largest hackathon.
+        Join 400+ hackers at SEA's largest hackathon.
       </p>
 
       <div className={`pt-4`}>
@@ -150,10 +168,7 @@ export default function AuthLoginComponent() {
               <p className="text-red-100 text-sm text-center mt-4">{error}</p>
             )}
 
-            <Button
-              type="submit"
-              className={`w-full font-semibold text-white`}
-            >
+            <Button type="submit" className={`w-full font-semibold text-white`}>
               {loading && <LoaderCircle className={"animate-spin"} />}
               Log in
             </Button>
diff --git a/src/components/AuthRegisterComponent.tsx b/src/components/AuthRegisterComponent.tsx
index d8b2f2b..4dfdf70 100644
--- a/src/components/AuthRegisterComponent.tsx
+++ b/src/components/AuthRegisterComponent.tsx
@@ -1,10 +1,17 @@
-import React, { useState } from "react"
-import { z } from "zod"
-import { zodResolver } from "@hookform/resolvers/zod"
-import { useForm } from "react-hook-form"
-import { Button } from "@/components/ui/button.tsx"
-import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form.tsx"
-import { Input } from "@/components/ui/input.tsx"
+import React, { useState } from "react";
+import { z } from "zod";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { useForm } from "react-hook-form";
+import { Button } from "@/components/ui/button.tsx";
+import {
+  Form,
+  FormControl,
+  FormField,
+  FormItem,
+  FormLabel,
+  FormMessage,
+} from "@/components/ui/form.tsx";
+import { Input } from "@/components/ui/input.tsx";
 import { useAuth } from "@/context/AuthContext.tsx";
 import { useNavigate } from "react-router-dom";
 import { LoaderCircle } from "lucide-react";
@@ -12,176 +19,190 @@ import { GoogleAuthProvider, signInWithPopup } from "firebase/auth";
 import { auth, db } from "@/utils/firebase.ts";
 import { doc, getDoc, setDoc } from "firebase/firestore";
 import { getAuthErrorMessage } from "@/components/Auth.tsx";
-import googleIcon from "/assets/google-icon.svg"
-import toast from "react-hot-toast"
+import googleIcon from "/assets/google-icon.svg";
+import toast from "react-hot-toast";
 
 const formSchema = z.object({
-    email: z.string().email().nonempty("Email is required"),
-    password: z.string().nonempty("Password is required"),
-    displayName: z.string().nonempty("Name is required"),
-})
+  email: z.string().email().nonempty("Email is required"),
+  password: z.string().nonempty("Password is required"),
+  displayName: z.string().nonempty("Name is required"),
+});
 
 export default function AuthRegisterComponent() {
-    const navigate = useNavigate();
-
-    const { user, loading, loginWithGoogle, signUpWithEmailPassword } = useAuth();
-    const [error, setError] = useState<string>("");
-
-
-    const form = useForm<z.infer<typeof formSchema>>({
-        resolver: zodResolver(formSchema),
-        defaultValues: {
-            email: "",
-            password: "",
-            displayName: "",
-        },
-    })
-
-    async function onSubmit(values: z.infer<typeof formSchema>) {
-        setError("");
-        try {
-            const passwordPolicy = /^(?=.*[A-Z])(?=.*[!@#$%^&*])(?=.{8,})/;
-            if (!passwordPolicy.test(values.password)) {
-                setError(
-                    "Password must be at least 8 characters, contain an uppercase letter and a special character."
-                );
-                return;
-            }
-
-            const { error } = await signUpWithEmailPassword({
-                displayName: values.displayName,
-                email: values.email,
-                password: values.password,
-            });
-
-            if (error) {
-                setError(error.message || "Signup failed");
-                return;
-            } else {
-                toast.success("Successfully signed up!");
-                navigate("/home");
-            }
-        } catch (err) {
-            setError("An unexpected error occurred");
-            console.error(err);
-        }
+  const navigate = useNavigate();
+
+  const { user, loading, loginWithGoogle, signUpWithEmailPassword } = useAuth();
+  const [error, setError] = useState<string>("");
+
+  const form = useForm<z.infer<typeof formSchema>>({
+    resolver: zodResolver(formSchema),
+    defaultValues: {
+      email: "",
+      password: "",
+      displayName: "",
+    },
+  });
+
+  async function onSubmit(values: z.infer<typeof formSchema>) {
+    setError("");
+    try {
+      const passwordPolicy = /^(?=.*[A-Z])(?=.*[!@#$%^&*])(?=.{8,})/;
+      if (!passwordPolicy.test(values.password)) {
+        setError(
+          "Password must be at least 8 characters, contain an uppercase letter and a special character."
+        );
+        return;
+      }
+
+      const { error } = await signUpWithEmailPassword({
+        displayName: values.displayName,
+        email: values.email,
+        password: values.password,
+      });
+
+      if (error) {
+        setError(error.message || "Signup failed");
+        return;
+      } else {
+        toast.success("Successfully signed up! Please verify your email.");
+        navigate("/auth?mode=verify-email");
+      }
+    } catch (err) {
+      setError("An unexpected error occurred");
+      console.error(err);
     }
-
-    const handleGoogleRegister = async () => {
-        try {
-            const provider = new GoogleAuthProvider();
-            const result = await signInWithPopup(auth, provider);
-            const user = result.user;
-
-            // Check if user exists in Firestore
-            const userRef = doc(db, 'users', user.uid);
-            const userDoc = await getDoc(userRef);
-
-            // If user doesn't exist, create a new document
-            if (!userDoc.exists()) {
-                await setDoc(userRef, {
-                    email: user.email,
-                    displayName: user.displayName,
-                    createdAt: new Date().toISOString(),
-                    updatedAt: new Date().toISOString()
-                });
-            }
-
-            const token = await user.getIdToken()
-            const { error } = await loginWithGoogle(token);
-
-            if (error) {
-                setError(error.message || "Sign Up failed");
-            } else {
-                toast.success("Successfully signed up!");
-                await auth.signOut()
-                navigate("/home");
-            }
-        } catch (error: any) {
-            console.error(error);
-            setError(getAuthErrorMessage(error));
-        }
-    };
-
-
-    return <>
-        <div>
-            <h2 className="text-2xl font-semibold">
-                Sign up for an account
-            </h2>
-            <p className="text-sm mt-2">
-                Join 400+ hackers at SEA’s largest hackathon.
-            </p>
-
-            <div className={`pt-4`}>
-                <Form {...form}>
-                    <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
-                        <FormField
-                            control={form.control}
-                            name="displayName"
-                            render={({ field }) => (
-                                <FormItem>
-                                    <FormLabel className={`text-white`}>Name</FormLabel>
-                                    <FormControl>
-                                        <Input placeholder="Enter your name" type={`text`}
-                                            className={`text-white`} {...field} />
-                                    </FormControl>
-                                    <FormMessage className={`text-white`} />
-                                </FormItem>
-                            )}
-                        />
-                        <FormField
-                            control={form.control}
-                            name="email"
-                            render={({ field }) => (
-                                <FormItem>
-                                    <FormLabel className={`text-white`}>Email Address</FormLabel>
-                                    <FormControl>
-                                        <Input placeholder="Enter your email" type={`email`}
-                                            className={`text-white`} {...field} />
-                                    </FormControl>
-                                    <FormMessage className={`text-white`} />
-                                </FormItem>
-                            )}
-                        />
-                        <FormField
-                            control={form.control}
-                            name="password"
-                            render={({ field }) => (
-                                <FormItem>
-                                    <FormLabel>Password</FormLabel>
-                                    <FormControl>
-                                        <Input placeholder="Enter your password" type={`password`}
-                                            className={`text-white`} {...field} />
-                                    </FormControl>
-                                    <FormMessage className={`text-white`} />
-                                </FormItem>
-                            )}
-                        />
-
-                        {error && <p className="text-red-100 text-sm text-center mt-4">{error}</p>}
-
-                        <Button
-                            type="submit"
-                            className={`w-full font-semibold text-white`}
-                        >
-                            {loading && <LoaderCircle className={"animate-spin"} />}
-                            Sign up
-                        </Button>
-
-
-                        {/* Google Sign-up */}
-                        <Button
-                            type={`button`}
-                            variant={"ghost"}
-                            onClick={handleGoogleRegister}
-                            className="w-full flex items-center justify-center text-black bg-white hover:text-black"
-                        >
-                            <img src={googleIcon} width={20} height={20} />
-                            Sign up with Google
-                        </Button>
-                    </form>
-                </Form>
-            </div>
-        </div></>;
-}
\ No newline at end of file
+  }
+
+  const handleGoogleRegister = async () => {
+    try {
+      const provider = new GoogleAuthProvider();
+      const result = await signInWithPopup(auth, provider);
+      const user = result.user;
+
+      // Check if user exists in Firestore
+      const userRef = doc(db, "users", user.uid);
+      const userDoc = await getDoc(userRef);
+
+      // If user doesn't exist, create a new document
+      if (!userDoc.exists()) {
+        await setDoc(userRef, {
+          email: user.email,
+          displayName: user.displayName,
+          createdAt: new Date().toISOString(),
+          updatedAt: new Date().toISOString(),
+        });
+      }
+
+      const token = await user.getIdToken();
+      const { error } = await loginWithGoogle(token);
+
+      if (error) {
+        setError(error.message || "Sign Up failed");
+      } else {
+        toast.success("Successfully signed up!");
+        await auth.signOut();
+        navigate("/home");
+      }
+    } catch (error: any) {
+      console.error(error);
+      setError(getAuthErrorMessage(error));
+    }
+  };
+
+  return (
+    <>
+      <div>
+        <h2 className="text-2xl font-semibold">Sign up for an account</h2>
+        <p className="text-sm mt-2">
+          Join 400+ hackers at SEA's largest hackathon.
+        </p>
+
+        <div className={`pt-4`}>
+          <Form {...form}>
+            <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
+              <FormField
+                control={form.control}
+                name="displayName"
+                render={({ field }) => (
+                  <FormItem>
+                    <FormLabel className={`text-white`}>Name</FormLabel>
+                    <FormControl>
+                      <Input
+                        placeholder="Enter your name"
+                        type={`text`}
+                        className={`text-white`}
+                        {...field}
+                      />
+                    </FormControl>
+                    <FormMessage className={`text-white`} />
+                  </FormItem>
+                )}
+              />
+              <FormField
+                control={form.control}
+                name="email"
+                render={({ field }) => (
+                  <FormItem>
+                    <FormLabel className={`text-white`}>
+                      Email Address
+                    </FormLabel>
+                    <FormControl>
+                      <Input
+                        placeholder="Enter your email"
+                        type={`email`}
+                        className={`text-white`}
+                        {...field}
+                      />
+                    </FormControl>
+                    <FormMessage className={`text-white`} />
+                  </FormItem>
+                )}
+              />
+              <FormField
+                control={form.control}
+                name="password"
+                render={({ field }) => (
+                  <FormItem>
+                    <FormLabel>Password</FormLabel>
+                    <FormControl>
+                      <Input
+                        placeholder="Enter your password"
+                        type={`password`}
+                        className={`text-white`}
+                        {...field}
+                      />
+                    </FormControl>
+                    <FormMessage className={`text-white`} />
+                  </FormItem>
+                )}
+              />
+
+              {error && (
+                <p className="text-red-100 text-sm text-center mt-4">{error}</p>
+              )}
+
+              <Button
+                type="submit"
+                className={`w-full font-semibold text-white`}
+              >
+                {loading && <LoaderCircle className={"animate-spin"} />}
+                Sign up
+              </Button>
+
+              {/* Google Sign-up */}
+              <Button
+                type={`button`}
+                variant={"ghost"}
+                onClick={handleGoogleRegister}
+                className="w-full flex items-center justify-center text-black bg-white hover:text-black"
+              >
+                <img src={googleIcon} width={20} height={20} />
+                Sign up with Google
+              </Button>
+            </form>
+          </Form>
+        </div>
+      </div>
+    </>
+  );
+}
diff --git a/src/components/AuthVerificationComponent.tsx b/src/components/AuthVerificationComponent.tsx
new file mode 100644
index 0000000..d2e6f0e
--- /dev/null
+++ b/src/components/AuthVerificationComponent.tsx
@@ -0,0 +1,131 @@
+import { useEffect, useState } from "react";
+import { useNavigate } from "react-router-dom";
+import { Button } from "@/components/ui/button";
+import { LoaderCircle } from "lucide-react";
+import toast from "react-hot-toast";
+import { useAuth } from "@/context/AuthContext";
+
+export default function AuthVerificationComponent() {
+  const navigate = useNavigate();
+  const [isVerifying, setIsVerifying] = useState(false);
+  const { signOut } = useAuth();
+
+  useEffect(() => {
+    let intervalId: NodeJS.Timeout;
+
+    const checkVerificationStatus = async () => {
+      try {
+        const response = await fetch("/api/auth/session-check", {
+          method: "GET",
+          headers: {
+            "Content-Type": "application/json",
+          },
+          credentials: "include",
+        });
+
+        const data = await response.json();
+
+        if (response.ok && data.data.user?.emailVerified) {
+          toast.success("Email verified successfully!");
+          clearInterval(intervalId);
+          window.location.href = "/home";
+        }
+      } catch (error) {
+        console.error("Error checking verification status:", error);
+      }
+    };
+
+    // Initial check
+    checkVerificationStatus();
+
+    // Set up polling every 5 seconds
+    intervalId = setInterval(checkVerificationStatus, 5000);
+
+    // Cleanup interval on component unmount
+    return () => {
+      if (intervalId) {
+        clearInterval(intervalId);
+      }
+    };
+  }, [navigate]);
+
+  const handleResendVerification = async () => {
+    setIsVerifying(true);
+    try {
+      const response = await fetch("/api/auth/verify-account", {
+        method: "POST",
+        headers: {
+          "Content-Type": "application/json",
+        },
+        credentials: "include",
+      });
+
+      const data = await response.json();
+
+      if (response.ok) {
+        toast.success("Verification email sent successfully!");
+      } else {
+        toast.error(data.message || "Failed to send verification email");
+      }
+    } catch (error) {
+      console.error("Error sending verification email:", error);
+      toast.error("Failed to send verification email");
+    } finally {
+      setIsVerifying(false);
+    }
+  };
+
+  const handleBackToLogin = async () => {
+    try {
+      await signOut();
+      window.location.href = "/auth?mode=login";
+    } catch (error) {
+      console.error("Error during logout:", error);
+      // Still redirect even if logout fails
+      window.location.href = "/auth?mode=login";
+    }
+  };
+
+  return (
+    <div>
+      <h2 className="text-2xl font-semibold">Verify your email</h2>
+      <p className="text-sm mt-2">
+        We've sent a verification link to your email address. Please check your
+        inbox and click the link to verify your account.
+      </p>
+
+      <div className="pt-4">
+        <div className="space-y-4">
+          <p className="text-sm text-center">
+            Didn't receive the email? Check your spam folder or click the button
+            below to resend.
+          </p>
+
+          <Button
+            onClick={handleResendVerification}
+            className="w-full font-semibold text-white"
+            disabled={isVerifying}
+          >
+            {isVerifying && <LoaderCircle className="animate-spin mr-2" />}
+            Resend Verification Email
+          </Button>
+
+          <p className="text-sm text-center">
+            After verifying your email, you'll be automatically redirected to
+            the home page.
+          </p>
+
+          <p className="text-sm text-center mt-4">
+            Want to try a different account?
+            <span
+              className="underline font-semibold cursor-pointer ml-1"
+              onClick={handleBackToLogin}
+            >
+              Back to Login
+            </span>
+          </p>
+        </div>
+      </div>
+    </div>
+  );
+}
diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx
index 62742a2..d252312 100644
--- a/src/context/AuthContext.tsx
+++ b/src/context/AuthContext.tsx
@@ -103,7 +103,11 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
         const data = await response.json();
 
         if (response.ok) {
-          setUser(data.data.user);
+          if (data.data.user?.emailVerified) {
+            setUser(data.data.user);
+          } else {
+            setUser(null);
+          }
         } else {
           setUser(null);
         }
@@ -145,7 +149,12 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
         };
       }
 
-      setUser(data.user || data);
+      if (data.user?.emailVerified || data.emailVerified) {
+        setUser(data.user || data);
+      } else {
+        setUser(null);
+      }
+
       return {
         error: null,
         data: { message: "Login successful", user: data.user || data },
@@ -183,7 +192,7 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
         };
       }
 
-      setUser(data.user || data);
+      setUser(null);
       return {
         error: null,
         data: { message: "Signup successful", user: data.user || data },


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR implements email verification functionality across the authentication flow to ensure that only users with verified email addresses can access the application.  
- Adds email verification checks in session management, login, and signup flows.  
- Introduces a dedicated AuthVerificationComponent to handle verification status and email resend functionality.  
- Adjusts navigation to redirect unverified users to the verification page instead of granting direct access.

### Reviewed Changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

<details>
<summary>Show a summary per file</summary>

| File                                  | Description                                                                                           |
| ------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| src/context/AuthContext.tsx           | Updates session handling to set the user to null when email is not verified; inconsistency in structure observed.  |
| src/components/AuthVerificationComponent.tsx | Adds a new component for handling email verification status with polling and resend functionality. |
| src/components/AuthRegisterComponent.tsx | Modifies the sign-up flow to direct users to email verification; inconsistent redirect behavior for Google sign-up noted. |
| src/components/AuthLoginComponent.tsx  | Incorporates an explicit email verification check during login and redirects unverified users accordingly. |
| src/components/Auth.tsx               | Adjusts routing logic to accommodate a verification mode based on URL query parameters.              |
</details>





state: COMMENTED
author_association: NONE
submitted_at: 2025-05-23T08:11:11Z

Review comments:
diff_hunk: @@ -103,7 +103,11 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
         const data = await response.json();
 
         if (response.ok) {
-          setUser(data.data.user);
+          if (data.data.user?.emailVerified) {
+            setUser(data.data.user);
path: src/context/AuthContext.tsx
user: login: Copilot
body: The email verification check accesses the property via data.data.user, while later checks use data.user. Consider standardizing the API response structure for email verification to improve maintainability.
```suggestion
          if (data.user?.emailVerified) {
            setUser(data.user);
```
created_at: 2025-05-23T08:11:10Z
author_association: NONE
diff_hunk: @@ -1,187 +1,208 @@
-import React, { useState } from "react"
-import { z } from "zod"
-import { zodResolver } from "@hookform/resolvers/zod"
-import { useForm } from "react-hook-form"
-import { Button } from "@/components/ui/button.tsx"
-import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form.tsx"
-import { Input } from "@/components/ui/input.tsx"
+import React, { useState } from "react";
+import { z } from "zod";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { useForm } from "react-hook-form";
+import { Button } from "@/components/ui/button.tsx";
+import {
+  Form,
+  FormControl,
+  FormField,
+  FormItem,
+  FormLabel,
+  FormMessage,
+} from "@/components/ui/form.tsx";
+import { Input } from "@/components/ui/input.tsx";
 import { useAuth } from "@/context/AuthContext.tsx";
 import { useNavigate } from "react-router-dom";
 import { LoaderCircle } from "lucide-react";
 import { GoogleAuthProvider, signInWithPopup } from "firebase/auth";
 import { auth, db } from "@/utils/firebase.ts";
 import { doc, getDoc, setDoc } from "firebase/firestore";
 import { getAuthErrorMessage } from "@/components/Auth.tsx";
-import googleIcon from "/assets/google-icon.svg"
-import toast from "react-hot-toast"
+import googleIcon from "/assets/google-icon.svg";
+import toast from "react-hot-toast";
 
 const formSchema = z.object({
-    email: z.string().email().nonempty("Email is required"),
-    password: z.string().nonempty("Password is required"),
-    displayName: z.string().nonempty("Name is required"),
-})
+  email: z.string().email().nonempty("Email is required"),
+  password: z.string().nonempty("Password is required"),
+  displayName: z.string().nonempty("Name is required"),
+});
 
 export default function AuthRegisterComponent() {
-    const navigate = useNavigate();
-
-    const { user, loading, loginWithGoogle, signUpWithEmailPassword } = useAuth();
-    const [error, setError] = useState<string>("");
-
-
-    const form = useForm<z.infer<typeof formSchema>>({
-        resolver: zodResolver(formSchema),
-        defaultValues: {
-            email: "",
-            password: "",
-            displayName: "",
-        },
-    })
-
-    async function onSubmit(values: z.infer<typeof formSchema>) {
-        setError("");
-        try {
-            const passwordPolicy = /^(?=.*[A-Z])(?=.*[!@#$%^&*])(?=.{8,})/;
-            if (!passwordPolicy.test(values.password)) {
-                setError(
-                    "Password must be at least 8 characters, contain an uppercase letter and a special character."
-                );
-                return;
-            }
-
-            const { error } = await signUpWithEmailPassword({
-                displayName: values.displayName,
-                email: values.email,
-                password: values.password,
-            });
-
-            if (error) {
-                setError(error.message || "Signup failed");
-                return;
-            } else {
-                toast.success("Successfully signed up!");
-                navigate("/home");
-            }
-        } catch (err) {
-            setError("An unexpected error occurred");
-            console.error(err);
-        }
+  const navigate = useNavigate();
+
+  const { user, loading, loginWithGoogle, signUpWithEmailPassword } = useAuth();
+  const [error, setError] = useState<string>("");
+
+  const form = useForm<z.infer<typeof formSchema>>({
+    resolver: zodResolver(formSchema),
+    defaultValues: {
+      email: "",
+      password: "",
+      displayName: "",
+    },
+  });
+
+  async function onSubmit(values: z.infer<typeof formSchema>) {
+    setError("");
+    try {
+      const passwordPolicy = /^(?=.*[A-Z])(?=.*[!@#$%^&*])(?=.{8,})/;
+      if (!passwordPolicy.test(values.password)) {
+        setError(
+          "Password must be at least 8 characters, contain an uppercase letter and a special character."
+        );
+        return;
+      }
+
+      const { error } = await signUpWithEmailPassword({
+        displayName: values.displayName,
+        email: values.email,
+        password: values.password,
+      });
+
+      if (error) {
+        setError(error.message || "Signup failed");
+        return;
+      } else {
+        toast.success("Successfully signed up! Please verify your email.");
+        navigate("/auth?mode=verify-email");
+      }
+    } catch (err) {
+      setError("An unexpected error occurred");
+      console.error(err);
     }
-
-    const handleGoogleRegister = async () => {
-        try {
-            const provider = new GoogleAuthProvider();
-            const result = await signInWithPopup(auth, provider);
-            const user = result.user;
-
-            // Check if user exists in Firestore
-            const userRef = doc(db, 'users', user.uid);
-            const userDoc = await getDoc(userRef);
-
-            // If user doesn't exist, create a new document
-            if (!userDoc.exists()) {
-                await setDoc(userRef, {
-                    email: user.email,
-                    displayName: user.displayName,
-                    createdAt: new Date().toISOString(),
-                    updatedAt: new Date().toISOString()
-                });
-            }
-
-            const token = await user.getIdToken()
-            const { error } = await loginWithGoogle(token);
-
-            if (error) {
-                setError(error.message || "Sign Up failed");
-            } else {
-                toast.success("Successfully signed up!");
-                await auth.signOut()
-                navigate("/home");
-            }
-        } catch (error: any) {
-            console.error(error);
-            setError(getAuthErrorMessage(error));
-        }
-    };
-
-
-    return <>
-        <div>
-            <h2 className="text-2xl font-semibold">
-                Sign up for an account
-            </h2>
-            <p className="text-sm mt-2">
-                Join 400+ hackers at SEA’s largest hackathon.
-            </p>
-
-            <div className={`pt-4`}>
-                <Form {...form}>
-                    <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
-                        <FormField
-                            control={form.control}
-                            name="displayName"
-                            render={({ field }) => (
-                                <FormItem>
-                                    <FormLabel className={`text-white`}>Name</FormLabel>
-                                    <FormControl>
-                                        <Input placeholder="Enter your name" type={`text`}
-                                            className={`text-white`} {...field} />
-                                    </FormControl>
-                                    <FormMessage className={`text-white`} />
-                                </FormItem>
-                            )}
-                        />
-                        <FormField
-                            control={form.control}
-                            name="email"
-                            render={({ field }) => (
-                                <FormItem>
-                                    <FormLabel className={`text-white`}>Email Address</FormLabel>
-                                    <FormControl>
-                                        <Input placeholder="Enter your email" type={`email`}
-                                            className={`text-white`} {...field} />
-                                    </FormControl>
-                                    <FormMessage className={`text-white`} />
-                                </FormItem>
-                            )}
-                        />
-                        <FormField
-                            control={form.control}
-                            name="password"
-                            render={({ field }) => (
-                                <FormItem>
-                                    <FormLabel>Password</FormLabel>
-                                    <FormControl>
-                                        <Input placeholder="Enter your password" type={`password`}
-                                            className={`text-white`} {...field} />
-                                    </FormControl>
-                                    <FormMessage className={`text-white`} />
-                                </FormItem>
-                            )}
-                        />
-
-                        {error && <p className="text-red-100 text-sm text-center mt-4">{error}</p>}
-
-                        <Button
-                            type="submit"
-                            className={`w-full font-semibold text-white`}
-                        >
-                            {loading && <LoaderCircle className={"animate-spin"} />}
-                            Sign up
-                        </Button>
-
-
-                        {/* Google Sign-up */}
-                        <Button
-                            type={`button`}
-                            variant={"ghost"}
-                            onClick={handleGoogleRegister}
-                            className="w-full flex items-center justify-center text-black bg-white hover:text-black"
-                        >
-                            <img src={googleIcon} width={20} height={20} />
-                            Sign up with Google
-                        </Button>
-                    </form>
-                </Form>
-            </div>
-        </div></>;
-}
\ No newline at end of file
+  }
+
+  const handleGoogleRegister = async () => {
+    try {
+      const provider = new GoogleAuthProvider();
+      const result = await signInWithPopup(auth, provider);
+      const user = result.user;
+
+      // Check if user exists in Firestore
+      const userRef = doc(db, "users", user.uid);
+      const userDoc = await getDoc(userRef);
+
+      // If user doesn't exist, create a new document
+      if (!userDoc.exists()) {
+        await setDoc(userRef, {
+          email: user.email,
+          displayName: user.displayName,
+          createdAt: new Date().toISOString(),
+          updatedAt: new Date().toISOString(),
+        });
+      }
+
+      const token = await user.getIdToken();
+      const { error } = await loginWithGoogle(token);
+
+      if (error) {
+        setError(error.message || "Sign Up failed");
+      } else {
+        toast.success("Successfully signed up!");
+        await auth.signOut();
path: src/components/AuthRegisterComponent.tsx
user: login: Copilot
body: In the Google sign-up branch, the user is redirected to '/home' despite the email verification requirement, which is inconsistent with the email/password flow redirecting to '/auth?mode=verify-email'. Consider unifying the behavior to ensure a consistent user experience.
created_at: 2025-05-23T08:11:11Z
author_association: NONE

PR comments:
user: login: vercel[bot]
created_at: 2025-05-23T08:10:37Z
body: [vc]: #yT01DAi4AdQAtpJ9a2mvw1aBkWmC7Q1BJcLVhvtupTc=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJwb3J0YWwiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoicG9ydGFsLWdpdC1mZWF0LWVtYWlsLXZlcmlmaWNhdGlvbi1nYXJ1ZGEtaGFja3MudmVyY2VsLmFwcCJ9LCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vZ2FydWRhLWhhY2tzL3BvcnRhbC84NkVDNDZCR2Y5ZFlFN3NYeU5EVHhKUGhoVzVXIiwicHJldmlld1VybCI6InBvcnRhbC1naXQtZmVhdC1lbWFpbC12ZXJpZmljYXRpb24tZ2FydWRhLWhhY2tzLnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiREVQTE9ZRUQifV19
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **portal** | ✅ Ready ([Inspect](https://vercel.com/garuda-hacks/portal/86EC46BGf9dYE7sXyNDTxJPhhW5W)) | [Visit Preview](https://portal-git-feat-email-verification-garuda-hacks.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/portal-git-feat-email-verification-garuda-hacks.vercel.app?via=pr-comment-feedback-link) | May 23, 2025 8:10am |


author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-23T08:11:53Z
body: ## PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

<table>
<tr><td>⏱️&nbsp;<strong>Estimated effort to review</strong>: 4 🔵🔵🔵🔵⚪</td></tr>
<tr><td>🧪&nbsp;<strong>No relevant tests</strong></td></tr>
<tr><td>🔒&nbsp;<strong>No security concerns identified</strong></td></tr>
<tr><td>⚡&nbsp;<strong>Recommended focus areas for review</strong><br><br>

<details><summary><a href='https://github.com/GarudaHacks/gh-portal/pull/32/files#diff-18818acbefb343d45b10cdd411ea121ebbf4b9dee1824a3dc449fa1bdbbb29f9R144-R156'><strong>Inconsistent Data Parsing</strong></a>

The code assumes different JSON shapes (`data.data.user` vs `data.user`), which may cause incorrect user state initialization.
</summary>

```typescript

if (!response.ok) {
  return {
    error: { message: data.message || "Login failed" },
    data: null,
  };
}

if (data.user?.emailVerified || data.emailVerified) {
  setUser(data.user || data);
} else {
  setUser(null);
}
```

</details>

<details><summary><a href='https://github.com/GarudaHacks/gh-portal/pull/32/files#diff-0d4ceb1343e69af643ae118c65226cb608bdb3d5c62e020aa47b839f9064e9d6R76-R105'><strong>Google Sign-up Logout</strong></a>

After successful Google sign-up, the user is signed out immediately before navigation, likely leaving the user not logged in on `/home`.
</summary>

```typescript
const handleGoogleRegister = async () => {
  try {
    const provider = new GoogleAuthProvider();
    const result = await signInWithPopup(auth, provider);
    const user = result.user;

    // Check if user exists in Firestore
    const userRef = doc(db, "users", user.uid);
    const userDoc = await getDoc(userRef);

    // If user doesn't exist, create a new document
    if (!userDoc.exists()) {
      await setDoc(userRef, {
        email: user.email,
        displayName: user.displayName,
        createdAt: new Date().toISOString(),
        updatedAt: new Date().toISOString(),
      });
    }

    const token = await user.getIdToken();
    const { error } = await loginWithGoogle(token);

    if (error) {
      setError(error.message || "Sign Up failed");
    } else {
      toast.success("Successfully signed up!");
      await auth.signOut();
      navigate("/home");
    }
```

</details>

<details><summary><a href='https://github.com/GarudaHacks/gh-portal/pull/32/files#diff-64efe290f74bd3ddbbe451ed5eb405817010c42485e2d8244b1ef4bf5b6fdc3dR77-R96'><strong>Mode Toggle Missing</strong></a>

UI toggle logic does not handle the SIGNUP mode and may not provide navigation options for users on verification or signup screens.
</summary>

```typescript
      Already have an account?
      <span
        className="underline font-semibold cursor-pointer ml-1"
        onClick={() => setMode("LOGIN")}
      >
        Log in
      </span>
    </>
  ) : mode === "FORGOT_PASSWORD" ? (
    <>
      Remember your password?
      <span
        className="underline font-semibold cursor-pointer ml-1"
        onClick={() => setMode("LOGIN")}
      >
        Log in
      </span>
    </>
  ) : null}
</p>
```

</details>

</td></tr>
</table>

author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-23T08:12:59Z
body: ## PR Code Suggestions ✨

<!-- a2284e4 -->

Explore these optional code suggestions:

<table><thead><tr><td><strong>Category</strong></td><td align=left><strong>Suggestion&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </strong></td><td align=center><strong>Impact</strong></td></tr><tbody><tr><td rowspan=1>Possible issue</td>
<td>



<details><summary>Handle fetch failures</summary>

___


**Guard against a failed session-check fetch before assuming a valid response. Add a <br>check for <code>!response.ok</code> and handle errors to avoid navigating on a bad request.**

[src/components/AuthLoginComponent.tsx [58-72]](https://github.com/GarudaHacks/gh-portal/pull/32/files#diff-b522ad906186573f5eed86fe96739beeffada5ca731e6d5fdcd245f323b6c5e8R58-R72)

```diff
 const response = await fetch("/api/auth/session-check", {
   method: "GET",
-  headers: {
-    "Content-Type": "application/json",
-  },
+  headers: { "Content-Type": "application/json" },
   credentials: "include",
 });
 const sessionData = await response.json();
-
-if (response.ok && !sessionData.data.user?.emailVerified) {
+if (!response.ok) {
+  toast.error(sessionData.message || "Session check failed");
+  return;
+}
+if (!sessionData.data.user?.emailVerified) {
   // ...
 }
```
<details><summary>Suggestion importance[1-10]: 6</summary>

__

Why: Adding a `!response.ok` check before processing `sessionData` prevents unexpected behavior on failed fetch requests and improves error handling.


</details></details></td><td align=center>Low

</td></tr><tr><td rowspan=1>General</td>
<td>



<details><summary>Sync mode with URL</summary>

___


**Update <code>mode</code> when URL search params change by syncing state in an effect. This <br>ensures the component correctly switches if the query string is updated without a <br>full reload.**

[src/components/Auth.tsx [42-44]](https://github.com/GarudaHacks/gh-portal/pull/32/files#diff-64efe290f74bd3ddbbe451ed5eb405817010c42485e2d8244b1ef4bf5b6fdc3dR42-R44)

```diff
-const [mode, setMode] = useState<
-  "LOGIN" | "SIGNUP" | "FORGOT_PASSWORD" | "VERIFY_EMAIL"
->(searchParams.get("mode") === "verify-email" ? "VERIFY_EMAIL" : "LOGIN");
+const [mode, setMode] = useState<"LOGIN" | "SIGNUP" | "FORGOT_PASSWORD" | "VERIFY_EMAIL">(
+  searchParams.get("mode") === "verify-email" ? "VERIFY_EMAIL" : "LOGIN"
+);
 
+useEffect(() => {
+  setMode(
+    searchParams.get("mode") === "verify-email"
+      ? "VERIFY_EMAIL"
+      : "LOGIN"
+  );
+}, [searchParams]);
+
```
<details><summary>Suggestion importance[1-10]: 5</summary>

__

Why: Synchronizing `mode` with `searchParams` ensures the component updates when URL parameters change, enhancing UX but is a minor feature.


</details></details></td><td align=center>Low

</td></tr></tr></tbody></table>


author_association: NONE