From 2dca407aeba6f083660fd126172dfa8f954734f7 Mon Sep 17 00:00:00 2001 From: Shashank Date: Sun, 15 Mar 2026 19:35:37 +0530 Subject: [PATCH 1/3] feat: Added Email/Password signin and signup --- components/AuthCard.tsx | 307 +++++++++++++++++++++++++++++++++------ components/Navbar.tsx | 6 +- src/lib/firebase/auth.ts | 60 ++++++++ 3 files changed, 324 insertions(+), 49 deletions(-) diff --git a/components/AuthCard.tsx b/components/AuthCard.tsx index 16e4864..c6309a6 100644 --- a/components/AuthCard.tsx +++ b/components/AuthCard.tsx @@ -1,15 +1,16 @@ "use client"; -import { useState, useCallback } from "react"; +import { useState, useCallback, useEffect } from "react"; import { useRouter } from "next/navigation"; import Link from "next/link"; import Image from "next/image"; -import { signInWithGoogle, signInWithGitHub } from "../src/lib/firebase/auth"; +import { signInWithGoogle, signInWithGitHub, signInWithEmail, signUpWithEmail, resetPassword } from "../src/lib/firebase/auth"; +import { useAuth } from "../src/lib/firebase/AuthContext"; import { User } from "firebase/auth"; import { Spinner } from "./ui/Spinner"; // Sync Firebase user with MongoDB - throws on failure -async function syncUserWithDB(user: User, provider: 'google' | 'github') { +async function syncUserWithDB(user: User, provider: 'google' | 'github' | 'email') { // Need to get fresh token after sign-in const token = await user.getIdToken(); @@ -40,9 +41,25 @@ interface AuthCardProps { export default function AuthCard({ mode = 'login' }: AuthCardProps) { const router = useRouter(); + const { user: currentUser, isLoading: authLoading } = useAuth(); const [isLoadingGoogle, setIsLoadingGoogle] = useState(false); const [isLoadingGitHub, setIsLoadingGitHub] = useState(false); + const [isLoadingEmail, setIsLoadingEmail] = useState(false); const [signInError, setSignInError] = useState(null); + const [successMessage, setSuccessMessage] = useState(null); + + // Email/password form state + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [displayName, setDisplayName] = useState(''); + const [showResetPassword, setShowResetPassword] = useState(false); + + // If user is already logged in, redirect to dashboard + useEffect(() => { + if (!authLoading && currentUser) { + router.push('/dashboard'); + } + }, [authLoading, currentUser, router]); const handleProviderSignIn = useCallback(async ( signInFn: () => Promise, @@ -50,6 +67,7 @@ export default function AuthCard({ mode = 'login' }: AuthCardProps) { provider: 'google' | 'github' ) => { setSignInError(null); + setSuccessMessage(null); setLoading(true); try { const user = await signInFn(); @@ -70,17 +88,93 @@ export default function AuthCard({ mode = 'login' }: AuthCardProps) { } }, [router]); - const isLoading = isLoadingGoogle || isLoadingGitHub; + const handleEmailSubmit = useCallback(async (e: React.FormEvent) => { + e.preventDefault(); + setSignInError(null); + setSuccessMessage(null); + setIsLoadingEmail(true); + + try { + let user: User | null = null; + + if (mode === 'signup') { + if (!displayName.trim()) { + setSignInError('Please enter your name.'); + return; + } + user = await signUpWithEmail(email, password, displayName.trim()); + } else { + user = await signInWithEmail(email, password); + } + + if (!user) { + setSignInError('Authentication failed. Please try again.'); + return; + } + + await syncUserWithDB(user, 'email'); + router.push('/dashboard'); + } catch (error) { + setSignInError( + error instanceof Error + ? error.message + : 'Authentication failed. Please try again.' + ); + } finally { + setIsLoadingEmail(false); + } + }, [mode, email, password, displayName, router]); + + const handlePasswordReset = useCallback(async (e: React.FormEvent) => { + e.preventDefault(); + setSignInError(null); + setSuccessMessage(null); + + if (!email.trim()) { + setSignInError('Please enter your email address.'); + return; + } + + try { + await resetPassword(email); + setSuccessMessage('Password reset email sent! Check your inbox.'); + setShowResetPassword(false); + } catch (error) { + setSignInError( + error instanceof Error + ? error.message + : 'Failed to send reset email.' + ); + } + }, [email]); + + const isLoading = isLoadingGoogle || isLoadingGitHub || isLoadingEmail; + + // Show nothing while checking auth (prevents flash) + if (authLoading) { + return ( +
+ +
+ ); + } + + // If already logged in, don't render the card (redirect is happening) + if (currentUser) return null; return (

- {mode === 'signup' ? 'Create an Account' : 'Welcome Back'} + {showResetPassword + ? 'Reset Password' + : mode === 'signup' ? 'Create an Account' : 'Welcome Back'}

- {mode === 'signup' - ? 'Get started designing system architectures for free.' - : 'Sign in to start designing system architectures.'} + {showResetPassword + ? 'Enter your email to receive a reset link.' + : mode === 'signup' + ? 'Get started designing system architectures for free.' + : 'Sign in to start designing system architectures.'}

{/* Error message */} @@ -90,45 +184,164 @@ export default function AuthCard({ mode = 'login' }: AuthCardProps) {
)} - {/* Google */} - - - {/* GitHub */} - + {/* Success message */} + {successMessage && ( +
+ {successMessage} +
+ )} + + {showResetPassword ? ( + /* Password Reset Form */ +
+
+ + setEmail(e.target.value)} + placeholder="you@example.com" + required + className="w-full rounded-lg bg-[#1f1b33] border border-white/10 px-4 py-3 text-white placeholder-slate-500 text-sm focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary/50 transition" + /> +
+ + +
+ ) : ( + <> + {/* Email/Password Form */} +
+ {mode === 'signup' && ( +
+ + setDisplayName(e.target.value)} + placeholder="John Doe" + required + disabled={isLoading} + className="w-full rounded-lg bg-[#1f1b33] border border-white/10 px-4 py-3 text-white placeholder-slate-500 text-sm focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary/50 transition disabled:opacity-50" + /> +
+ )} +
+ + setEmail(e.target.value)} + placeholder="you@example.com" + required + disabled={isLoading} + className="w-full rounded-lg bg-[#1f1b33] border border-white/10 px-4 py-3 text-white placeholder-slate-500 text-sm focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary/50 transition disabled:opacity-50" + /> +
+
+
+ + {mode === 'login' && ( + + )} +
+ setPassword(e.target.value)} + placeholder={mode === 'signup' ? 'At least 6 characters' : '••••••••'} + required + minLength={6} + disabled={isLoading} + className="w-full rounded-lg bg-[#1f1b33] border border-white/10 px-4 py-3 text-white placeholder-slate-500 text-sm focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary/50 transition disabled:opacity-50" + /> +
+ +
+ + {/* Divider */} +
+
+ or continue with +
+
+ + {/* Google */} + + + {/* GitHub */} + + + )} {/* Footer text */}

diff --git a/components/Navbar.tsx b/components/Navbar.tsx index f1d2c04..711a572 100644 --- a/components/Navbar.tsx +++ b/components/Navbar.tsx @@ -1,12 +1,14 @@ "use client"; import Link from "next/link"; +import { usePathname } from "next/navigation"; import { useAuth } from "../src/lib/firebase/AuthContext"; import { logout } from "../src/lib/firebase/auth"; export default function Navbar() { const { user, isLoading } = useAuth(); - + const pathname = usePathname(); + const isAuthPage = pathname === '/login' || pathname === '/signup'; return (

@@ -48,7 +50,7 @@ export default function Navbar() {
- ) : !user ? ( + ) : isAuthPage ? null : !user ? ( <> { + if (!auth) throw new Error("Firebase Auth is not initialized."); + try { + const res = await createUserWithEmailAndPassword(auth, email, password); + await updateProfile(res.user, { displayName }); + return res.user; + } catch (error) { + const authError = error as AuthError; + console.error("Email sign-up error:", { + code: authError.code, + message: authError.message, + }); + if (authError.code === 'auth/email-already-in-use') { + throw new Error("An account with this email already exists."); + } + if (authError.code === 'auth/weak-password') { + throw new Error("Password must be at least 6 characters."); + } + throw new Error(authError.message || "Failed to create account"); + } +}; + +export const signInWithEmail = async (email: string, password: string) => { + if (!auth) throw new Error("Firebase Auth is not initialized."); + try { + const res = await signInWithEmailAndPassword(auth, email, password); + return res.user; + } catch (error) { + const authError = error as AuthError; + console.error("Email sign-in error:", { + code: authError.code, + message: authError.message, + }); + if (authError.code === 'auth/user-not-found' || authError.code === 'auth/wrong-password' || authError.code === 'auth/invalid-credential') { + throw new Error("Invalid email or password."); + } + throw new Error(authError.message || "Failed to sign in"); + } +}; + +export const resetPassword = async (email: string) => { + if (!auth) throw new Error("Firebase Auth is not initialized."); + try { + await sendPasswordResetEmail(auth, email); + } catch (error) { + const authError = error as AuthError; + console.error("Password reset error:", { + code: authError.code, + message: authError.message, + }); + if (authError.code === 'auth/user-not-found') { + throw new Error("No account found with this email."); + } + throw new Error(authError.message || "Failed to send reset email"); + } +}; From 68f5348339cbe91e6062e96e29cbbd07ad17b3f0 Mon Sep 17 00:00:00 2001 From: Shashank Date: Sun, 15 Mar 2026 19:54:19 +0530 Subject: [PATCH 2/3] feat: add email/password auth, fix navbar redirect, harden signup rollback --- components/AuthCard.tsx | 27 +++++++++++++++++++-------- components/Navbar.tsx | 2 +- src/lib/db/models/User.ts | 4 ++-- src/lib/firebase/auth.ts | 14 ++++++++++++-- 4 files changed, 34 insertions(+), 13 deletions(-) diff --git a/components/AuthCard.tsx b/components/AuthCard.tsx index c6309a6..bed838e 100644 --- a/components/AuthCard.tsx +++ b/components/AuthCard.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useCallback, useEffect } from "react"; +import { useState, useCallback, useEffect, useRef } from "react"; import { useRouter } from "next/navigation"; import Link from "next/link"; import Image from "next/image"; @@ -47,6 +47,10 @@ export default function AuthCard({ mode = 'login' }: AuthCardProps) { const [isLoadingEmail, setIsLoadingEmail] = useState(false); const [signInError, setSignInError] = useState(null); const [successMessage, setSuccessMessage] = useState(null); + const [userSynced, setUserSynced] = useState(false); + + // Track whether a sign-in flow is actively running (prevents premature redirect) + const signingInRef = useRef(false); // Email/password form state const [email, setEmail] = useState(''); @@ -54,12 +58,15 @@ export default function AuthCard({ mode = 'login' }: AuthCardProps) { const [displayName, setDisplayName] = useState(''); const [showResetPassword, setShowResetPassword] = useState(false); - // If user is already logged in, redirect to dashboard + // If user is already logged in (not mid-sign-in) OR sync completed, redirect to dashboard useEffect(() => { - if (!authLoading && currentUser) { + if (!authLoading && currentUser && !signingInRef.current) { + router.push('/dashboard'); + } + if (userSynced) { router.push('/dashboard'); } - }, [authLoading, currentUser, router]); + }, [authLoading, currentUser, userSynced, router]); const handleProviderSignIn = useCallback(async ( signInFn: () => Promise, @@ -69,6 +76,7 @@ export default function AuthCard({ mode = 'login' }: AuthCardProps) { setSignInError(null); setSuccessMessage(null); setLoading(true); + signingInRef.current = true; try { const user = await signInFn(); if (!user) { @@ -76,7 +84,7 @@ export default function AuthCard({ mode = 'login' }: AuthCardProps) { return; } await syncUserWithDB(user, provider); - router.push('/dashboard'); + setUserSynced(true); } catch (error) { setSignInError( error instanceof Error @@ -84,15 +92,17 @@ export default function AuthCard({ mode = 'login' }: AuthCardProps) { : `Failed to sign in with ${provider}. Please try again.` ); } finally { + signingInRef.current = false; setLoading(false); } - }, [router]); + }, []); const handleEmailSubmit = useCallback(async (e: React.FormEvent) => { e.preventDefault(); setSignInError(null); setSuccessMessage(null); setIsLoadingEmail(true); + signingInRef.current = true; try { let user: User | null = null; @@ -113,7 +123,7 @@ export default function AuthCard({ mode = 'login' }: AuthCardProps) { } await syncUserWithDB(user, 'email'); - router.push('/dashboard'); + setUserSynced(true); } catch (error) { setSignInError( error instanceof Error @@ -121,9 +131,10 @@ export default function AuthCard({ mode = 'login' }: AuthCardProps) { : 'Authentication failed. Please try again.' ); } finally { + signingInRef.current = false; setIsLoadingEmail(false); } - }, [mode, email, password, displayName, router]); + }, [mode, email, password, displayName]); const handlePasswordReset = useCallback(async (e: React.FormEvent) => { e.preventDefault(); diff --git a/components/Navbar.tsx b/components/Navbar.tsx index 711a572..e83cec2 100644 --- a/components/Navbar.tsx +++ b/components/Navbar.tsx @@ -60,7 +60,7 @@ export default function Navbar() { Get Started diff --git a/src/lib/db/models/User.ts b/src/lib/db/models/User.ts index 40eb3af..f9b1a9b 100644 --- a/src/lib/db/models/User.ts +++ b/src/lib/db/models/User.ts @@ -5,7 +5,7 @@ export interface IUser extends Document { email: string; displayName: string; photoURL?: string; - provider: 'google' | 'github'; + provider: 'google' | 'github' | 'email'; createdAt: Date; updatedAt: Date; lastLoginAt: Date; @@ -42,7 +42,7 @@ const UserSchema = new Schema( }, provider: { type: String, - enum: ['google', 'github'], + enum: ['google', 'github', 'email'], required: true, }, lastLoginAt: { diff --git a/src/lib/firebase/auth.ts b/src/lib/firebase/auth.ts index 5f9ac6d..02c7df0 100644 --- a/src/lib/firebase/auth.ts +++ b/src/lib/firebase/auth.ts @@ -78,10 +78,20 @@ export const updateUserProfile = async (displayName?: string, photoURL?: string) export const signUpWithEmail = async (email: string, password: string, displayName: string) => { if (!auth) throw new Error("Firebase Auth is not initialized."); + let createdUser = null; try { const res = await createUserWithEmailAndPassword(auth, email, password); - await updateProfile(res.user, { displayName }); - return res.user; + createdUser = res.user; + + try { + await updateProfile(createdUser, { displayName }); + } catch (profileError) { + // Rollback: delete the partially created account + await createdUser.delete(); + throw profileError; + } + + return createdUser; } catch (error) { const authError = error as AuthError; console.error("Email sign-up error:", { From 1497e976cc86b58b2941e401a32d38e97d444005 Mon Sep 17 00:00:00 2001 From: Shashank Date: Sun, 15 Mar 2026 20:14:45 +0530 Subject: [PATCH 3/3] fix(auth): handle signup sync failures, fix redirect race, add reset loading state --- components/AuthCard.tsx | 57 +++++++++++++++++++++++++++++------------ 1 file changed, 40 insertions(+), 17 deletions(-) diff --git a/components/AuthCard.tsx b/components/AuthCard.tsx index bed838e..be0697b 100644 --- a/components/AuthCard.tsx +++ b/components/AuthCard.tsx @@ -4,7 +4,7 @@ import { useState, useCallback, useEffect, useRef } from "react"; import { useRouter } from "next/navigation"; import Link from "next/link"; import Image from "next/image"; -import { signInWithGoogle, signInWithGitHub, signInWithEmail, signUpWithEmail, resetPassword } from "../src/lib/firebase/auth"; +import { signInWithGoogle, signInWithGitHub, signInWithEmail, signUpWithEmail, resetPassword, logout } from "../src/lib/firebase/auth"; import { useAuth } from "../src/lib/firebase/AuthContext"; import { User } from "firebase/auth"; import { Spinner } from "./ui/Spinner"; @@ -57,16 +57,18 @@ export default function AuthCard({ mode = 'login' }: AuthCardProps) { const [password, setPassword] = useState(''); const [displayName, setDisplayName] = useState(''); const [showResetPassword, setShowResetPassword] = useState(false); + const [isResetting, setIsResetting] = useState(false); + const [syncFailed, setSyncFailed] = useState(false); - // If user is already logged in (not mid-sign-in) OR sync completed, redirect to dashboard + // If user is already logged in (not mid-sign-in, no sync failure) OR sync completed, redirect useEffect(() => { - if (!authLoading && currentUser && !signingInRef.current) { + if (!authLoading && currentUser && !signingInRef.current && !syncFailed) { router.push('/dashboard'); } - if (userSynced) { + if (userSynced && !syncFailed) { router.push('/dashboard'); } - }, [authLoading, currentUser, userSynced, router]); + }, [authLoading, currentUser, userSynced, syncFailed, router]); const handleProviderSignIn = useCallback(async ( signInFn: () => Promise, @@ -101,30 +103,40 @@ export default function AuthCard({ mode = 'login' }: AuthCardProps) { e.preventDefault(); setSignInError(null); setSuccessMessage(null); + setSyncFailed(false); setIsLoadingEmail(true); signingInRef.current = true; - try { - let user: User | null = null; + let createdUser: User | null = null; + try { if (mode === 'signup') { if (!displayName.trim()) { setSignInError('Please enter your name.'); return; } - user = await signUpWithEmail(email, password, displayName.trim()); + createdUser = await signUpWithEmail(email, password, displayName.trim()); } else { - user = await signInWithEmail(email, password); + createdUser = await signInWithEmail(email, password); } - if (!user) { + if (!createdUser) { setSignInError('Authentication failed. Please try again.'); return; } - await syncUserWithDB(user, 'email'); + await syncUserWithDB(createdUser, 'email'); setUserSynced(true); } catch (error) { + // If signup created a Firebase user but DB sync failed, clean up + if (mode === 'signup' && createdUser) { + try { + await logout(); + } catch { + // Best-effort cleanup + } + } + setSyncFailed(true); setSignInError( error instanceof Error ? error.message @@ -146,6 +158,7 @@ export default function AuthCard({ mode = 'login' }: AuthCardProps) { return; } + setIsResetting(true); try { await resetPassword(email); setSuccessMessage('Password reset email sent! Check your inbox.'); @@ -156,10 +169,12 @@ export default function AuthCard({ mode = 'login' }: AuthCardProps) { ? error.message : 'Failed to send reset email.' ); + } finally { + setIsResetting(false); } }, [email]); - const isLoading = isLoadingGoogle || isLoadingGitHub || isLoadingEmail; + const isLoading = isLoadingGoogle || isLoadingGitHub || isLoadingEmail || isResetting; // Show nothing while checking auth (prevents flash) if (authLoading) { @@ -170,8 +185,8 @@ export default function AuthCard({ mode = 'login' }: AuthCardProps) { ); } - // If already logged in, don't render the card (redirect is happening) - if (currentUser) return null; + // If already logged in and synced (or was already logged in), don't render the card + if (currentUser && !syncFailed && !signingInRef.current) return null; return (
@@ -213,16 +228,24 @@ export default function AuthCard({ mode = 'login' }: AuthCardProps) { value={email} onChange={(e) => setEmail(e.target.value)} placeholder="you@example.com" - required - className="w-full rounded-lg bg-[#1f1b33] border border-white/10 px-4 py-3 text-white placeholder-slate-500 text-sm focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary/50 transition" + disabled={isLoading} + className="w-full rounded-lg bg-[#1f1b33] border border-white/10 px-4 py-3 text-white placeholder-slate-500 text-sm focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary/50 transition disabled:opacity-50" />