-
Notifications
You must be signed in to change notification settings - Fork 0
Staging #80
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Staging #80
Changes from 2 commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
2dca407
feat: Added Email/Password signin and signup
Shashank0701-byte b833bdd
Merge pull request #79 from Shashank0701-byte/feature/authContext
Shashank0701-byte 68f5348
feat: add email/password auth, fix navbar redirect, harden signup rol…
Shashank0701-byte b4172a2
Merge pull request #81 from Shashank0701-byte/feature/authContext
Shashank0701-byte 1497e97
fix(auth): handle signup sync failures, fix redirect race, add reset …
Shashank0701-byte 4ab6e8d
Merge pull request #82 from Shashank0701-byte/feature/authContext
Shashank0701-byte File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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,16 +41,33 @@ 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<string | null>(null); | ||
| const [successMessage, setSuccessMessage] = useState<string | null>(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<User | null>, | ||
| setLoading: (v: boolean) => void, | ||
| 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'); | ||
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } 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 ( | ||
| <div className="w-full max-w-md rounded-2xl bg-[#141022]/80 backdrop-blur border border-white/10 p-8 shadow-2xl flex items-center justify-center min-h-[300px]"> | ||
| <Spinner label="Loading" /> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| // If already logged in, don't render the card (redirect is happening) | ||
| if (currentUser) return null; | ||
|
|
||
| return ( | ||
| <div className="w-full max-w-md rounded-2xl bg-[#141022]/80 backdrop-blur border border-white/10 p-8 shadow-2xl"> | ||
| <h1 className="text-2xl font-bold text-white text-center"> | ||
| {mode === 'signup' ? 'Create an Account' : 'Welcome Back'} | ||
| {showResetPassword | ||
| ? 'Reset Password' | ||
| : mode === 'signup' ? 'Create an Account' : 'Welcome Back'} | ||
| </h1> | ||
| <p className="text-slate-400 text-center mt-2"> | ||
| {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.'} | ||
| </p> | ||
|
|
||
| {/* Error message */} | ||
|
|
@@ -90,45 +184,164 @@ export default function AuthCard({ mode = 'login' }: AuthCardProps) { | |
| </div> | ||
| )} | ||
|
|
||
| {/* Google */} | ||
| <button | ||
| onClick={() => handleProviderSignIn(signInWithGoogle, setIsLoadingGoogle, 'google')} | ||
| disabled={isLoading} | ||
| aria-busy={isLoadingGoogle} | ||
| className="mt-6 w-full flex items-center justify-center gap-3 rounded-lg bg-[#1f1b33] hover:bg-[#2a2450] border border-white/10 py-3 text-white font-medium transition disabled:opacity-50 disabled:cursor-not-allowed" | ||
| > | ||
| {isLoadingGoogle ? ( | ||
| <> | ||
| <Spinner label="Signing in" /> | ||
| Signing in... | ||
| </> | ||
| ) : ( | ||
| <> | ||
| <Image src="/google.svg" width={20} height={20} alt="" /> | ||
| Continue with Google | ||
| </> | ||
| )} | ||
| </button> | ||
|
|
||
| {/* GitHub */} | ||
| <button | ||
| onClick={() => handleProviderSignIn(signInWithGitHub, setIsLoadingGitHub, 'github')} | ||
| disabled={isLoading} | ||
| aria-busy={isLoadingGitHub} | ||
| className="mt-3 w-full flex items-center justify-center gap-3 rounded-lg bg-[#1f1b33] hover:bg-[#2a2450] border border-white/10 py-3 text-white font-medium transition disabled:opacity-50 disabled:cursor-not-allowed" | ||
| > | ||
| {isLoadingGitHub ? ( | ||
| <> | ||
| <Spinner label="Signing in" /> | ||
| Signing in... | ||
| </> | ||
| ) : ( | ||
| <> | ||
| <Image src="/github.svg" width={20} height={20} alt="" /> | ||
| Continue with GitHub | ||
| </> | ||
| )} | ||
| </button> | ||
| {/* Success message */} | ||
| {successMessage && ( | ||
| <div className="mt-4 p-3 rounded-lg bg-green-500/10 border border-green-500/20 text-green-400 text-sm text-center"> | ||
| {successMessage} | ||
| </div> | ||
| )} | ||
|
|
||
| {showResetPassword ? ( | ||
| /* Password Reset Form */ | ||
| <form onSubmit={handlePasswordReset} className="mt-6 space-y-4"> | ||
| <div> | ||
| <label htmlFor="reset-email" className="block text-sm font-medium text-slate-300 mb-1.5">Email</label> | ||
| <input | ||
| id="reset-email" | ||
| type="email" | ||
| value={email} | ||
| onChange={(e) => setEmail(e.target.value)} | ||
| placeholder="[email protected]" | ||
| 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" | ||
| /> | ||
| </div> | ||
| <button | ||
| type="submit" | ||
| disabled={isLoading} | ||
| className="w-full flex items-center justify-center gap-2 rounded-lg bg-primary hover:bg-primary-hover py-3 text-white font-semibold transition disabled:opacity-50 disabled:cursor-not-allowed" | ||
| > | ||
| Send Reset Link | ||
| </button> | ||
| <button | ||
| type="button" | ||
| onClick={() => { setShowResetPassword(false); setSignInError(null); }} | ||
| className="w-full text-sm text-slate-400 hover:text-white transition-colors" | ||
| > | ||
| ← Back to sign in | ||
| </button> | ||
| </form> | ||
| ) : ( | ||
| <> | ||
| {/* Email/Password Form */} | ||
| <form onSubmit={handleEmailSubmit} className="mt-6 space-y-4"> | ||
| {mode === 'signup' && ( | ||
| <div> | ||
| <label htmlFor="auth-name" className="block text-sm font-medium text-slate-300 mb-1.5">Full Name</label> | ||
| <input | ||
| id="auth-name" | ||
| type="text" | ||
| value={displayName} | ||
| onChange={(e) => 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" | ||
| /> | ||
| </div> | ||
| )} | ||
| <div> | ||
| <label htmlFor="auth-email" className="block text-sm font-medium text-slate-300 mb-1.5">Email</label> | ||
| <input | ||
| id="auth-email" | ||
| type="email" | ||
| value={email} | ||
| onChange={(e) => setEmail(e.target.value)} | ||
| placeholder="[email protected]" | ||
| 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" | ||
| /> | ||
| </div> | ||
| <div> | ||
| <div className="flex items-center justify-between mb-1.5"> | ||
| <label htmlFor="auth-password" className="block text-sm font-medium text-slate-300">Password</label> | ||
| {mode === 'login' && ( | ||
| <button | ||
| type="button" | ||
| onClick={() => { setShowResetPassword(true); setSignInError(null); setSuccessMessage(null); }} | ||
| className="text-xs text-primary hover:underline" | ||
| > | ||
| Forgot password? | ||
| </button> | ||
| )} | ||
| </div> | ||
| <input | ||
| id="auth-password" | ||
| type="password" | ||
| value={password} | ||
| onChange={(e) => 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" | ||
| /> | ||
| </div> | ||
| <button | ||
| type="submit" | ||
| disabled={isLoading} | ||
| className="w-full flex items-center justify-center gap-2 rounded-lg bg-primary hover:bg-primary-hover py-3 text-white font-semibold transition disabled:opacity-50 disabled:cursor-not-allowed" | ||
| > | ||
| {isLoadingEmail ? ( | ||
| <> | ||
| <Spinner label="Processing" /> | ||
| {mode === 'signup' ? 'Creating account...' : 'Signing in...'} | ||
| </> | ||
| ) : ( | ||
| mode === 'signup' ? 'Create Account' : 'Sign In' | ||
| )} | ||
| </button> | ||
| </form> | ||
|
|
||
| {/* Divider */} | ||
| <div className="mt-5 flex items-center gap-4"> | ||
| <div className="flex-1 h-px bg-white/10" /> | ||
| <span className="text-xs text-slate-500 uppercase tracking-wider">or continue with</span> | ||
| <div className="flex-1 h-px bg-white/10" /> | ||
| </div> | ||
|
|
||
| {/* Google */} | ||
| <button | ||
| onClick={() => handleProviderSignIn(signInWithGoogle, setIsLoadingGoogle, 'google')} | ||
| disabled={isLoading} | ||
| aria-busy={isLoadingGoogle} | ||
| className="mt-4 w-full flex items-center justify-center gap-3 rounded-lg bg-[#1f1b33] hover:bg-[#2a2450] border border-white/10 py-3 text-white font-medium transition disabled:opacity-50 disabled:cursor-not-allowed" | ||
| > | ||
| {isLoadingGoogle ? ( | ||
| <> | ||
| <Spinner label="Signing in" /> | ||
| Signing in... | ||
| </> | ||
| ) : ( | ||
| <> | ||
| <Image src="/google.svg" width={20} height={20} alt="" /> | ||
| </> | ||
| )} | ||
| </button> | ||
|
|
||
| {/* GitHub */} | ||
| <button | ||
| onClick={() => handleProviderSignIn(signInWithGitHub, setIsLoadingGitHub, 'github')} | ||
| disabled={isLoading} | ||
| aria-busy={isLoadingGitHub} | ||
| className="mt-3 w-full flex items-center justify-center gap-3 rounded-lg bg-[#1f1b33] hover:bg-[#2a2450] border border-white/10 py-3 text-white font-medium transition disabled:opacity-50 disabled:cursor-not-allowed" | ||
| > | ||
| {isLoadingGitHub ? ( | ||
| <> | ||
| <Spinner label="Signing in" /> | ||
| Signing in... | ||
| </> | ||
| ) : ( | ||
| <> | ||
| <Image src="/github.svg" width={20} height={20} alt="" /> | ||
| GitHub | ||
| </> | ||
| )} | ||
| </button> | ||
| </> | ||
| )} | ||
|
|
||
| {/* Footer text */} | ||
| <p className="mt-6 text-center text-sm text-slate-500"> | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.