Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions frontend/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { AuthProvider } from './context/AuthProvider';
import { useAuth } from './hooks/useAuth';
import { SocketProvider } from './context/SocketProvider';
import { ThemeProvider } from './context/ThemeProvider';
import EmailVerificationPrompt from './components/EmailVerificationPrompt';
import AppLayout from './components/AppLayout';
import Footer from './components/ui/Footer';

Expand Down Expand Up @@ -151,10 +152,14 @@ function ProtectedRoute({ children }) {
}

if (!user) {
return <Navigate to="/login" replace />;
}
return <Navigate to="/login" replace />;
}

if (!user.emailVerified) {
return <EmailVerificationPrompt />;
}

return <AppLayout>{children}</AppLayout>;
return <AppLayout>{children}</AppLayout>;
}


Expand Down
88 changes: 88 additions & 0 deletions frontend/src/components/EmailVerificationPrompt.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { useState } from "react";
import { sendEmailVerification } from "firebase/auth";
import { useAuth } from "../hooks/useAuth";
import { useNavigate } from "react-router-dom";

export default function EmailVerificationPrompt() {
const { user } = useAuth();
const navigate = useNavigate();

const [message, setMessage] = useState("");
const [loading, setLoading] = useState(false);

const resendEmail = async () => {
try {
setLoading(true);

await sendEmailVerification(user);

setMessage("Verification email sent successfully.");
} catch (error) {
setMessage(error.message);
} finally {
setLoading(false);
}
};


const checkVerification = async () => {
try {
await user.reload();

if (user.emailVerified) {
navigate("/dashboard");
Comment on lines +13 to +33

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify where EmailVerificationPrompt is rendered and inspect user-shape contracts.
rg -n -C3 "EmailVerificationPrompt|verify.*email|/dashboard" frontend/src
rg -n -C3 "setUser\\(|dev-user-001|emailVerified|reload|getIdToken" frontend/src/context/AuthProvider.jsx

Repository: anurag3407/career-pilot

Length of output: 24364


🏁 Script executed:

cat -n frontend/src/components/EmailVerificationPrompt.jsx

Repository: anurag3407/career-pilot

Length of output: 2544


🏁 Script executed:

rg -n "sendEmailVerification" frontend/src --type jsx --type js -A2 | head -30

Repository: anurag3407/career-pilot

Length of output: 95


🏁 Script executed:

rg -n "sendEmailVerification" frontend/src --type js -B2 -A2

Repository: anurag3407/career-pilot

Length of output: 1560


🏁 Script executed:

# Check if there are any tests or other usages of EmailVerificationPrompt that might reveal expected behavior
rg -n "EmailVerificationPrompt" frontend/src --type jsx --type js

Repository: anurag3407/career-pilot

Length of output: 95


🏁 Script executed:

# Check AuthProvider's full context around setUser and mock user creation
sed -n '25,50p' frontend/src/context/AuthProvider.jsx

Repository: anurag3407/career-pilot

Length of output: 672


🏁 Script executed:

# Look for other components that use Firebase user methods safely
rg -n "user\.(reload|emailVerified)" frontend/src --type js

Repository: anurag3407/career-pilot

Length of output: 236


🏁 Script executed:

# Check if there are guards or utility functions for Firebase user checks elsewhere
rg -n "typeof.*function|reload.*function|emailVerified" frontend/src --type js -B2 -A1

Repository: anurag3407/career-pilot

Length of output: 1180


Guard Firebase-user API calls before invoking verification methods.

Lines 30 and 32 assume user is a Firebase User object with reload() method and emailVerified property. The AuthProvider dev-mode fallback creates a mock user without these members, causing runtime errors in local development. Additionally, line 71's check button lacks a loading disabled state, allowing concurrent invocations.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/EmailVerificationPrompt.jsx` around lines 13 - 33,
Add guard checks in the checkVerification function before invoking user.reload()
and accessing user.emailVerified to verify that the user object is a Firebase
User instance (checking for the presence of required methods/properties) to
prevent runtime errors when running with the AuthProvider dev-mode fallback mock
user. Additionally, add disabled={loading} to the check button element to
prevent concurrent invocations when a verification check is already in progress.

} else {
setMessage("Email is still not verified.");
}

} catch (error) {
setMessage(error.message);
}
};
Comment on lines +28 to +41

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Prevent overlapping async actions for resend/check flows.

checkVerification has no in-flight state while resendEmail does. On Line [71], users can trigger repeated reload() calls and interleave message updates with resend operations.

Suggested fix
   const [message, setMessage] = useState("");
   const [loading, setLoading] = useState(false);
+  const [checking, setChecking] = useState(false);
@@
   const checkVerification = async () => {
     try {
+      setChecking(true);
       await user.reload();
@@
     } catch (error) {
       setMessage(error.message);
+    } finally {
+      setChecking(false);
     }
   };
@@
           <button
             onClick={resendEmail}
-            disabled={loading || !canUseVerificationApis}
+            disabled={loading || checking || !canUseVerificationApis}
           >
             {loading ? "Sending..." : "Resend verification email"}
           </button>
@@
           <button
             onClick={checkVerification}
-            disabled={loading || !canUseVerificationApis}
+            disabled={loading || checking || !canUseVerificationApis}
           >
-            I've verified, continue
+            {checking ? "Checking..." : "I've verified, continue"}
           </button>

Also applies to: 62-74

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/EmailVerificationPrompt.jsx` around lines 28 - 41,
The checkVerification function lacks in-flight state tracking, allowing users to
trigger overlapping reload() calls that can interleave message updates and race
conditions. Add a loading or in-flight state variable (similar to what the
resendEmail function implements) to guard the checkVerification async function
so it cannot execute while already in progress. Set this state to true before
calling user.reload(), and set it to false in both the success path (after
navigating or setting message) and the catch block. Ensure the resendEmail
function at lines 62-74 follows the same pattern to prevent overlapping
operations between both asynchronous flows.



return (
<div className="min-h-screen flex items-center justify-center">
<div className="max-w-md text-center p-6 rounded-lg border">

<h1 className="text-2xl font-bold mb-4">
Verify your email
</h1>

<p className="mb-6">
We sent a verification link to:
</p>

<strong>
{user?.email}
</strong>

<div className="mt-6 flex flex-col gap-3">

<button
onClick={resendEmail}
disabled={loading}
>
{loading ? "Sending..." : "Resend verification email"}
</button>


<button
onClick={checkVerification}
>
I've verified, continue
</button>

</div>


{message && (
<p className="mt-4">
{message}
</p>
)}

</div>
</div>
);
}
96 changes: 75 additions & 21 deletions frontend/src/components/Navbar.jsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { useState, useEffect } from 'react'
import { useState, useEffect, useRef } from 'react'
import { Link, useLocation } from 'react-router-dom'
import { useAuth } from '../hooks/useAuth'
import { useTheme } from '../hooks/useTheme'
import { motion, AnimatePresence } from 'framer-motion'
import useClickOutside from '../hooks/useClickOutside'
import {
Search,
FileText,
Expand Down Expand Up @@ -33,19 +34,44 @@ export default function Navbar() {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
const [scrolled, setScrolled] = useState(false)
const [searchQuery, setSearchQuery] = useState('')
const [showDropdown, setShowDropdown] = useState(false)

// DECOUPLED STATES - Replaced single showDropdown with two distinct states
const [searchDropdownOpen, setSearchDropdownOpen] = useState(false)
const [profileDropdownOpen, setProfileDropdownOpen] = useState(false)

const [notificationCount] = useState(3)

// Refs for click outside handling
const searchRef = useRef(null)
const profileRef = useRef(null)

// Use custom hooks for outside click detection
useClickOutside(searchRef, () => setSearchDropdownOpen(false))
useClickOutside(profileRef, () => setProfileDropdownOpen(false))

useEffect(() => {
const handleScroll = () => {
setScrolled(window.scrollY>20)
setScrolled(window.scrollY > 20)
}

window.addEventListener('scroll', handleScroll)

return () => window.removeEventListener('scroll', handleScroll)
}, [])

// Handle Escape key to close both dropdowns
useEffect(() => {
const handleEscape = (e) => {
if (e.key === 'Escape') {
setSearchDropdownOpen(false)
setProfileDropdownOpen(false)
}
}

window.addEventListener('keydown', handleEscape)
return () => window.removeEventListener('keydown', handleEscape)
}, [])

const handleLogout = async () => {
try {
await logout()
Expand All @@ -70,6 +96,17 @@ export default function Navbar() {
}
}

// Mutual exclusion handlers
const openProfileDropdown = () => {
setProfileDropdownOpen(true)
setSearchDropdownOpen(false) // Close search dropdown if open
}

const openSearchDropdown = () => {
setSearchDropdownOpen(true)
setProfileDropdownOpen(false) // Close profile dropdown if open
}

const isActive = (path) => location.pathname === path

const publicLinks = [
Expand Down Expand Up @@ -131,8 +168,8 @@ export default function Navbar() {
{/* Desktop Navigation */}
<div className="hidden lg:flex items-center gap-2">

{/* Search Bar */}
<div className="relative">
{/* Search Bar - with ref and decoupled state */}
<div className="relative" ref={searchRef}>
<div className="flex items-center bg-muted border border-border rounded-xl px-3 py-2 w-72 focus-within:ring-2 focus-within:ring-primary/40 transition-all">
<Search className="w-4 h-4 text-muted-foreground mr-2" />

Expand All @@ -141,20 +178,34 @@ export default function Navbar() {
placeholder="Search anything..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onFocus={() => setShowDropdown(true)}
onBlur={() => setTimeout(() => setShowDropdown(false), 200)}
onFocus={() => {
if (searchBlurTimeoutRef.current) {
clearTimeout(searchBlurTimeoutRef.current)
searchBlurTimeoutRef.current = null
}
openSearchDropdown()
}}
onBlur={() => {
if (searchBlurTimeoutRef.current) {
clearTimeout(searchBlurTimeoutRef.current)
}
searchBlurTimeoutRef.current = setTimeout(() => {
setSearchDropdownOpen(false)
searchBlurTimeoutRef.current = null
}, 200)
}}
className="bg-transparent outline-none text-sm w-full text-foreground placeholder:text-muted-foreground"
/>
</div>

{/* Suggestions Dropdown */}
{/* Search Suggestions Dropdown - uses searchDropdownOpen only */}
<AnimatePresence>
{showDropdown && (
{searchDropdownOpen && (
<motion.div
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 8 }}
className="absolute top-14 left-0 w-full bg-background border border-border rounded-xl shadow-xl overflow-hidden"
className="absolute top-14 left-0 w-full bg-background border border-border rounded-xl shadow-xl overflow-hidden z-50"
>
{searchSuggestions.map((item, index) => (
<button
Expand Down Expand Up @@ -240,13 +291,13 @@ export default function Navbar() {
)}
</button>

{/* User Dropdown */}
<div className="relative">
{/* User Profile Dropdown - with ref and decoupled state */}
<div className="relative" ref={profileRef}>
<button
onClick={() => setShowDropdown(!showDropdown)}
onClick={() => openProfileDropdown()}
className="flex items-center gap-2 px-3 py-2 bg-muted border border-border rounded-full hover:bg-accent transition-all"
aria-label="User menu"
aria-expanded={showDropdown}
aria-expanded={profileDropdownOpen}
>
<div className="w-8 h-8 rounded-full overflow-hidden bg-primary/20 flex items-center justify-center">
<img
Expand All @@ -263,17 +314,19 @@ export default function Navbar() {
<ChevronDown className="w-4 h-4 text-muted-foreground" />
</button>

{/* Profile Menu Dropdown - uses profileDropdownOpen only */}
<AnimatePresence>
{showDropdown && (
{profileDropdownOpen && (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 10 }}
className="absolute right-0 top-14 w-52 bg-background border border-border rounded-2xl shadow-xl overflow-hidden"
className="absolute right-0 top-14 w-52 bg-background border border-border rounded-2xl shadow-xl overflow-hidden z-50"
>
<Link
to="/profile"
className="flex items-center gap-2 px-4 py-3 hover:bg-muted transition-colors text-sm"
onClick={() => setProfileDropdownOpen(false)}
>
<User className="w-4 h-4" />
Profile
Expand All @@ -282,13 +335,17 @@ export default function Navbar() {
<Link
to="/settings"
className="flex items-center gap-2 px-4 py-3 hover:bg-muted transition-colors text-sm"
onClick={() => setProfileDropdownOpen(false)}
>
<Palette className="w-4 h-4" />
Settings
</Link>

<button
onClick={handleLogout}
onClick={() => {
handleLogout()
setProfileDropdownOpen(false)
}}
className="flex items-center gap-2 w-full px-4 py-3 text-left hover:bg-destructive/10 text-destructive transition-colors text-sm"
>
<LogOut className="w-4 h-4" />
Expand Down Expand Up @@ -396,9 +453,6 @@ export default function Navbar() {
</Link>
))}




{user ? (
<button
onClick={() => {
Expand Down Expand Up @@ -433,4 +487,4 @@ export default function Navbar() {
</AnimatePresence>
</nav>
)
}
}
6 changes: 5 additions & 1 deletion frontend/src/context/AuthProvider.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import {
signOut,
GoogleAuthProvider,
signInWithPopup,
updateProfile
updateProfile,
sendEmailVerification
} from 'firebase/auth'
import { auth } from '../config/firebase'
import { AuthContext } from './AuthContext'
Expand Down Expand Up @@ -57,6 +58,9 @@ export function AuthProvider({ children }) {
const signup = async (email, password, displayName) => {
if (!auth) throw new Error("Authentication service is not configured. Please check your environment variables and authentication provider setup. Refer to the project setup documentation for configuration instructions.")
const result = await createUserWithEmailAndPassword(auth, email, password)

await sendEmailVerification(result.user)

Comment on lines 60 to +63

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

For Firebase JS SDK v11 (firebase/auth), does sendEmailVerification(user)reject on transient/provider errors aftercreateUserWithEmailAndPassword succeeds, and what handling pattern is recommended to avoid treating account creation as failed?

💡 Result:

In the Firebase JS SDK, the sendEmailVerification(user) function is an independent asynchronous operation that is decoupled from the createUserWithEmailAndPassword call [1][2]. Consequently, sendEmailVerification can reject with errors (such as auth/too-many-requests or auth/network-request-failed) even after a successful account creation [3][4][5]. Because createUserWithEmailAndPassword automatically signs the user in [6][7][8], the account is already persisted in Firebase Authentication by the time you attempt to send the verification email [5][9]. If the verification email fails to send, the user's account remains active and the user is still considered logged in [6][8]. Recommended Handling Pattern: To avoid treating the entire sign-up flow as a failure when only the email delivery fails, implement a decoupled error-handling strategy: 1. Separate Concerns: Do not block the application flow or treat account creation as a failure if sendEmailVerification throws an error [9]. 2. User Feedback: Inform the user that their account was created successfully, but that the verification email could not be sent immediately. Provide a UI mechanism (e.g., a "Resend Verification Email" button) to allow the user to retry the operation later [3][4]. 3. Graceful Error Handling: Catch the error from sendEmailVerification specifically, log it, and update the UI to notify the user without affecting their authenticated state [3][5]. Example Pattern: const { user } = await createUserWithEmailAndPassword(auth, email, password); try { await sendEmailVerification(user); // Notify user: "Account created! Please check your email." } catch (error) { console.error("Verification email failed:", error); // Notify user: "Account created, but we couldn't send the verification email. You can request a new one later." // Do not log the user out or delete the account. } This approach ensures that the account is preserved and the user can continue using the application, while still allowing them to eventually verify their email address [4][8].

Citations:


🏁 Script executed:

# First, find the AuthProvider.jsx file
fd -type f -name "AuthProvider.jsx"

Repository: anurag3407/career-pilot

Length of output: 238


🏁 Script executed:

# Find the AuthProvider.jsx file
fd "AuthProvider.jsx"

Repository: anurag3407/career-pilot

Length of output: 105


🏁 Script executed:

# Read the AuthProvider.jsx file to see lines 60-63
cat -n frontend/src/context/AuthProvider.jsx | sed -n '50,75p'

Repository: anurag3407/career-pilot

Length of output: 1284


🏁 Script executed:

# Find and check Register.jsx to see how signup errors are handled
fd "Register.jsx" | head -5

Repository: anurag3407/career-pilot

Length of output: 99


🏁 Script executed:

# Read Register.jsx to see how signup is called and errors are handled
cat -n frontend/src/pages/Register.jsx | head -150

Repository: anurag3407/career-pilot

Length of output: 6024


Don't fail the whole signup on verification-email delivery failure.

At Line 62, sendEmailVerification is in the hard-fail path after account creation at Line 60. If that call throws (due to network issues, rate limiting, or provider errors), the caller sees signup failure even though the user already exists in Firebase, which creates a confusing UX where users retry with the same email and encounter "email already in use" errors.

Suggested fix
   const result = await createUserWithEmailAndPassword(auth, email, password)
-
-  await sendEmailVerification(result.user)
+  try {
+    await sendEmailVerification(result.user)
+  } catch (verificationError) {
+    // Account is already created; keep signup successful and handle notification separately.
+    console.warn('Failed to send verification email:', verificationError)
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const result = await createUserWithEmailAndPassword(auth, email, password)
await sendEmailVerification(result.user)
const result = await createUserWithEmailAndPassword(auth, email, password)
try {
await sendEmailVerification(result.user)
} catch (verificationError) {
// Account is already created; keep signup successful and handle notification separately.
console.warn('Failed to send verification email:', verificationError)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/context/AuthProvider.jsx` around lines 60 - 63, The
sendEmailVerification call at line 62 is in the hard-fail path and will cause
the entire signup to fail if it throws an error, even though the user account
has already been successfully created at line 60 by
createUserWithEmailAndPassword. Wrap the sendEmailVerification call in a
try-catch block so that email verification failures are caught and logged
separately without causing the signup operation to fail. This allows the signup
process to complete successfully for the user even if the verification email
delivery fails due to network issues, rate limiting, or provider errors,
preventing confusing UX where users retry with the same email and encounter
"email already in use" errors.

if (displayName) {
await updateProfile(result.user, { displayName })
}
Expand Down
28 changes: 28 additions & 0 deletions frontend/src/hooks/useClickOutside.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { useEffect } from 'react'

/**
* Custom hook that triggers a callback when clicking outside of a referenced element
* @param {React.RefObject} ref - React ref object pointing to the element to monitor
* @param {Function} callback - Function to call when clicking outside
*/
const useClickOutside = (ref, callback) => {
useEffect(() => {
const handleClickOutside = (event) => {
if (ref.current && !ref.current.contains(event.target)) {
callback()
}
}

// Add event listener when component mounts
document.addEventListener('mousedown', handleClickOutside)
document.addEventListener('touchstart', handleClickOutside)

// Cleanup event listeners when component unmounts
return () => {
document.removeEventListener('mousedown', handleClickOutside)
document.removeEventListener('touchstart', handleClickOutside)
}
}, [ref, callback])
}

export default useClickOutside