From 4358ef19429bf91fca771bd54417470de8826cf5 Mon Sep 17 00:00:00 2001 From: Naseem Ali <34807727+Naseem77@users.noreply.github.com> Date: Wed, 10 Dec 2025 15:54:44 +0200 Subject: [PATCH 1/6] Add login with email --- app/src/components/modals/LoginModal.tsx | 307 ++++++++++++++++---- app/src/components/modals/SignupModal.tsx | 327 ++++++++++++++++++++++ app/src/config/api.ts | 2 + app/src/contexts/AuthContext.tsx | 8 + app/src/pages/Index.tsx | 40 ++- app/src/services/auth.ts | 59 ++++ 6 files changed, 673 insertions(+), 70 deletions(-) create mode 100644 app/src/components/modals/SignupModal.tsx diff --git a/app/src/components/modals/LoginModal.tsx b/app/src/components/modals/LoginModal.tsx index dbc75748..6c219c7c 100644 --- a/app/src/components/modals/LoginModal.tsx +++ b/app/src/components/modals/LoginModal.tsx @@ -1,22 +1,152 @@ +import { useState, useEffect } from 'react'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; import { buildApiUrl, API_CONFIG } from "@/config/api"; +import { useAuth } from "@/contexts/AuthContext"; +import { Eye, EyeOff } from "lucide-react"; interface LoginModalProps { open: boolean; onOpenChange: (open: boolean) => void; - canClose?: boolean; // Whether user can close the modal (false for required login) + canClose?: boolean; + startInSignupMode?: boolean; } -const LoginModal = ({ open, onOpenChange, canClose = true }: LoginModalProps) => { - const handleGoogleLogin = () => { +const LoginModal = ({ open, onOpenChange, canClose = true, startInSignupMode = false }: LoginModalProps) => { + const { login, signup, refreshAuth, isAuthenticated } = useAuth(); + + const [mode, setMode] = useState<'signin' | 'signup'>(startInSignupMode ? 'signup' : 'signin'); + const [firstName, setFirstName] = useState(''); + const [lastName, setLastName] = useState(''); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [showPassword, setShowPassword] = useState(false); + const [showConfirmPassword, setShowConfirmPassword] = useState(false); + const [error, setError] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [showEmailForm, setShowEmailForm] = useState(false); + const [justAuthenticated, setJustAuthenticated] = useState(false); + + useEffect(() => { + if (open) { + setMode(startInSignupMode ? 'signup' : 'signin'); + setShowEmailForm(false); + setError(''); + setFirstName(''); + setLastName(''); + setEmail(''); + setPassword(''); + setConfirmPassword(''); + setShowPassword(false); + setShowConfirmPassword(false); + setJustAuthenticated(false); + } + }, [open, startInSignupMode]); + + // Close modal automatically when authentication succeeds + useEffect(() => { + if (justAuthenticated && isAuthenticated && open) { + onOpenChange(false); + setJustAuthenticated(false); + } + }, [justAuthenticated, isAuthenticated, open, onOpenChange]); + + const handleGoogleAuth = () => { window.location.href = buildApiUrl(API_CONFIG.ENDPOINTS.LOGIN_GOOGLE); }; - const handleGithubLogin = () => { + const handleGithubAuth = () => { window.location.href = buildApiUrl(API_CONFIG.ENDPOINTS.LOGIN_GITHUB); }; + const handleEmailLogin = async (e: React.FormEvent) => { + e.preventDefault(); + setError(''); + if (!email.trim() || !password) { + setError('Email and password are required'); + return; + } + setIsLoading(true); + try { + const result = await login.email(email, password); + if (result.success) { + // Refresh auth state and mark as authenticated + await refreshAuth(); + setJustAuthenticated(true); + // Clear form fields + setEmail(''); + setPassword(''); + setShowEmailForm(false); + setShowPassword(false); + } else { + setError(result.error || 'Login failed'); + } + } catch (err) { + setError('An unexpected error occurred'); + } finally { + setIsLoading(false); + } + }; + + const handleEmailSignUp = async (e: React.FormEvent) => { + e.preventDefault(); + setError(''); + if (!firstName.trim() || !lastName.trim() || !email.trim() || !password) { + setError('All fields are required'); + return; + } + if (password.length < 8) { + setError('Password must be at least 8 characters long'); + return; + } + if (password !== confirmPassword) { + setError('Passwords do not match'); + return; + } + setIsLoading(true); + try { + const result = await signup.email(firstName, lastName, email, password); + if (result.success) { + // Refresh auth state and mark as authenticated + await refreshAuth(); + setJustAuthenticated(true); + // Clear form fields + setFirstName(''); + setLastName(''); + setEmail(''); + setPassword(''); + setConfirmPassword(''); + setShowEmailForm(false); + setShowPassword(false); + setShowConfirmPassword(false); + } else { + setError(result.error || 'Signup failed'); + } + } catch (err) { + setError('An unexpected error occurred'); + } finally { + setIsLoading(false); + } + }; + + const switchMode = () => { + setMode(mode === 'signin' ? 'signup' : 'signin'); + setShowEmailForm(true); // Keep user in email form when switching + setError(''); + setEmail(''); + setPassword(''); + setConfirmPassword(''); + setFirstName(''); + setLastName(''); + setShowPassword(false); + setShowConfirmPassword(false); + }; + + const isSignIn = mode === 'signin'; + return ( > { - if (!canClose) { - e.preventDefault(); - } - }} - onEscapeKeyDown={(e) => { - if (!canClose) { - e.preventDefault(); - } - }} + onInteractOutside={(e) => { if (!canClose) e.preventDefault(); }} + onEscapeKeyDown={(e) => { if (!canClose) e.preventDefault(); }} > - Welcome to QueryWeaver + {isSignIn ? 'Welcome to QueryWeaver' : 'Create Your Account'} - Sign in to access your databases and start querying + {isSignIn ? 'Sign in to access your databases and start querying' : 'Sign up to start using QueryWeaver'} -
- - - -
- - {canClose && ( + {!showEmailForm ? ( +
+ + +
+
+ +
+
+ Or +
+
+ +
+ {isSignIn ? "Don't have an account? " : "Already have an account? "} + +
+
+ ) : isSignIn ? ( +
+
+ + setEmail(e.target.value)} required disabled={isLoading} /> +
+
+ +
+ setPassword(e.target.value)} required disabled={isLoading} className="pr-10" /> + +
+
+ {error &&
{error}
} +
+ + +
+
+ Don't have an account? +
+
+ ) : ( +
+
+
+ + setFirstName(e.target.value)} required disabled={isLoading} /> +
+
+ + setLastName(e.target.value)} required disabled={isLoading} /> +
+
+
+ + setEmail(e.target.value)} required disabled={isLoading} /> +
+
+ +
+ setPassword(e.target.value)} required disabled={isLoading} minLength={8} className="pr-10" /> + +
+

Must be at least 8 characters long

+
+
+ +
+ setConfirmPassword(e.target.value)} required disabled={isLoading} className="pr-10" /> + +
+
+ {error &&
{error}
} +
+ + +
+
+ Already have an account? +
+
+ )} + {canClose && !showEmailForm && (
-

By signing in, you agree to our Terms of Service and Privacy Policy

+

By {isSignIn ? 'signing in' : 'signing up'}, you agree to our Terms of Service and Privacy Policy

)}
diff --git a/app/src/components/modals/SignupModal.tsx b/app/src/components/modals/SignupModal.tsx new file mode 100644 index 00000000..7cfd6b46 --- /dev/null +++ b/app/src/components/modals/SignupModal.tsx @@ -0,0 +1,327 @@ +import { useState, useEffect } from 'react'; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { useAuth } from "@/contexts/AuthContext"; +import { buildApiUrl, API_CONFIG } from "@/config/api"; + +interface SignupModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; + onSwitchToLogin?: () => void; + canClose?: boolean; + startWithEmailForm?: boolean; // Start with email form already open +} + +const SignupModal = ({ open, onOpenChange, onSwitchToLogin, canClose = true, startWithEmailForm = false }: SignupModalProps) => { + const { signup, refreshAuth } = useAuth(); + const [firstName, setFirstName] = useState(''); + const [lastName, setLastName] = useState(''); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [error, setError] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [showEmailForm, setShowEmailForm] = useState(startWithEmailForm); + + // Reset form when modal opens/closes or startWithEmailForm changes + useEffect(() => { + if (open) { + setShowEmailForm(startWithEmailForm); + setError(''); + setFirstName(''); + setLastName(''); + setEmail(''); + setPassword(''); + setConfirmPassword(''); + } else { + // Reset when closing + setShowEmailForm(false); + setError(''); + setFirstName(''); + setLastName(''); + setEmail(''); + setPassword(''); + setConfirmPassword(''); + } + }, [open, startWithEmailForm]); + + const handleGoogleSignup = () => { + window.location.href = buildApiUrl(API_CONFIG.ENDPOINTS.LOGIN_GOOGLE); + }; + + const handleGithubSignup = () => { + window.location.href = buildApiUrl(API_CONFIG.ENDPOINTS.LOGIN_GITHUB); + }; + + const handleEmailSignup = async (e: React.FormEvent) => { + e.preventDefault(); + setError(''); + + // Validation + if (!firstName.trim() || !lastName.trim() || !email.trim() || !password) { + setError('All fields are required'); + return; + } + + if (password.length < 8) { + setError('Password must be at least 8 characters long'); + return; + } + + if (password !== confirmPassword) { + setError('Passwords do not match'); + return; + } + + setIsLoading(true); + try { + const result = await signup.email(firstName, lastName, email, password); + + if (result.success) { + // Refresh auth to get user data + await refreshAuth(); + onOpenChange(false); + // Reset form + setFirstName(''); + setLastName(''); + setEmail(''); + setPassword(''); + setConfirmPassword(''); + setShowEmailForm(false); + } else { + setError(result.error || 'Signup failed'); + } + } catch (err) { + setError('An unexpected error occurred'); + } finally { + setIsLoading(false); + } + }; + + return ( + + { + if (!canClose) { + e.preventDefault(); + } + }} + onEscapeKeyDown={(e) => { + if (!canClose) { + e.preventDefault(); + } + }} + > + + + Create Your Account + + + Sign up to start using QueryWeaver + + + + {!showEmailForm ? ( +
+ + + + +
+
+ +
+
+ Or +
+
+ + + + {onSwitchToLogin && ( +
+ Already have an account?{' '} + +
+ )} +
+ ) : ( +
+
+
+ + setFirstName(e.target.value)} + required + disabled={isLoading} + /> +
+
+ + setLastName(e.target.value)} + required + disabled={isLoading} + /> +
+
+ +
+ + setEmail(e.target.value)} + required + disabled={isLoading} + /> +
+ +
+ + setPassword(e.target.value)} + required + disabled={isLoading} + minLength={8} + /> +

+ Must be at least 8 characters long +

+
+ +
+ + setConfirmPassword(e.target.value)} + required + disabled={isLoading} + /> +
+ + {error && ( +
+ {error} +
+ )} + +
+ + +
+ + {onSwitchToLogin && ( +
+ Already have an account?{' '} + +
+ )} +
+ )} + + {canClose && !showEmailForm && ( +
+

By signing up, you agree to our Terms of Service and Privacy Policy

+
+ )} +
+
+ ); +}; + +export default SignupModal; diff --git a/app/src/config/api.ts b/app/src/config/api.ts index f4671e21..6bd31b15 100644 --- a/app/src/config/api.ts +++ b/app/src/config/api.ts @@ -18,6 +18,8 @@ export const API_CONFIG = { AUTH_STATUS: '/auth-status', LOGIN_GOOGLE: '/login/google', LOGIN_GITHUB: '/login/github', + LOGIN_EMAIL: '/login/email', + SIGNUP_EMAIL: '/signup/email', LOGOUT: '/logout', // Graph/Database management diff --git a/app/src/contexts/AuthContext.tsx b/app/src/contexts/AuthContext.tsx index 53e41d78..ae7da677 100644 --- a/app/src/contexts/AuthContext.tsx +++ b/app/src/contexts/AuthContext.tsx @@ -9,6 +9,10 @@ interface AuthContextType { login: { google: () => Promise; github: () => Promise; + email: (email: string, password: string) => Promise<{ success: boolean; error?: string }>; + }; + signup: { + email: (firstName: string, lastName: string, email: string, password: string) => Promise<{ success: boolean; error?: string }>; }; logout: () => Promise; refreshAuth: () => Promise; @@ -55,6 +59,10 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children login: { google: AuthService.loginWithGoogle, github: AuthService.loginWithGithub, + email: AuthService.loginWithEmail, + }, + signup: { + email: AuthService.signupWithEmail, }, logout: handleLogout, refreshAuth: checkAuth, diff --git a/app/src/pages/Index.tsx b/app/src/pages/Index.tsx index 7c3b2976..4503e166 100644 --- a/app/src/pages/Index.tsx +++ b/app/src/pages/Index.tsx @@ -30,6 +30,8 @@ const Index = () => { const { toast } = useToast(); const [showDatabaseModal, setShowDatabaseModal] = useState(false); const [showLoginModal, setShowLoginModal] = useState(false); + const [loginModalCanClose, setLoginModalCanClose] = useState(true); + const [showSignupMode, setShowSignupMode] = useState(false); const [showDeleteModal, setShowDeleteModal] = useState(false); const [showSchemaViewer, setShowSchemaViewer] = useState(false); const [showTokensModal, setShowTokensModal] = useState(false); @@ -114,14 +116,11 @@ const Index = () => { // Show login modal when not authenticated after loading completes useEffect(() => { - // Only auto-open the login modal once per user/session to avoid locking - // the SPA when the backend is down or in demo mode. Allow users to - // dismiss it and remember that choice in sessionStorage. + // Auto-open the login modal and keep it open until user authenticates if (!authLoading && !isAuthenticated) { - const dismissed = sessionStorage.getItem('loginModalDismissed'); - if (!dismissed) { - setShowLoginModal(true); - } + setShowSignupMode(false); + setLoginModalCanClose(false); // Don't allow closing until authenticated + setShowLoginModal(true); } }, [authLoading, isAuthenticated]); @@ -416,7 +415,11 @@ const Index = () => { @@ -473,7 +476,11 @@ const Index = () => { variant="outline" size="sm" className="bg-purple-600 border-purple-500 text-white hover:bg-purple-700" - onClick={() => setShowLoginModal(true)} + onClick={() => { + setShowSignupMode(false); + setLoginModalCanClose(true); // Allow closing when manually opened + setShowLoginModal(true); + }} > Sign In @@ -609,13 +616,18 @@ const Index = () => { { - setShowLoginModal(open); - if (!open) { - // Remember dismissal for this session to avoid pinning the modal - sessionStorage.setItem('loginModalDismissed', '1'); + // Allow closing if: + // 1. User is authenticated (successful login/signup), OR + // 2. Modal was manually opened (loginModalCanClose is true) + if (isAuthenticated || loginModalCanClose) { + setShowLoginModal(open); + if (!open) { + setShowSignupMode(false); + } } }} - canClose={true} + canClose={loginModalCanClose} + startInSignupMode={showSignupMode} /> { + try { + const response = await fetch(buildApiUrl(API_CONFIG.ENDPOINTS.LOGIN_EMAIL), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', + body: JSON.stringify({ email, password }), + }); + + const data = await response.json(); + + if (!response.ok) { + return { success: false, error: data.error || 'Login failed' }; + } + + return { success: true }; + } catch (error) { + console.error('Failed to login with email:', error); + return { success: false, error: 'Failed to connect to authentication service' }; + } + } + + /** + * Sign up with email and password + */ + static async signupWithEmail( + firstName: string, + lastName: string, + email: string, + password: string + ): Promise<{ success: boolean; error?: string }> { + try { + const response = await fetch(buildApiUrl(API_CONFIG.ENDPOINTS.SIGNUP_EMAIL), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', + body: JSON.stringify({ firstName, lastName, email, password }), + }); + + const data = await response.json(); + + if (!response.ok) { + return { success: false, error: data.error || 'Signup failed' }; + } + + return { success: true }; + } catch (error) { + console.error('Failed to signup with email:', error); + return { success: false, error: 'Failed to connect to authentication service' }; + } + } + /** * Logout current user */ From 31acaa1efd5ad68b9cdbf44509d44a0c58ba3425 Mon Sep 17 00:00:00 2001 From: Naseem Ali <34807727+Naseem77@users.noreply.github.com> Date: Wed, 10 Dec 2025 15:55:33 +0200 Subject: [PATCH 2/6] fix allowing duplicate signups --- api/routes/auth.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/api/routes/auth.py b/api/routes/auth.py index 01613eb7..e7f5e8af 100644 --- a/api/routes/auth.py +++ b/api/routes/auth.py @@ -266,7 +266,12 @@ async def email_signup(request: Request, signup_data: EmailSignupRequest) -> JSO await _set_mail_hash(email, password_hash) else: - logging.info("User already exists: %s", _sanitize_for_log(email)) + # User already exists - return error instead of success + logging.info("Signup attempt for existing user: %s", _sanitize_for_log(email)) + return JSONResponse( + {"success": False, "error": "An account with this email already exists"}, + status_code=status.HTTP_409_CONFLICT + ) logging.info("User registration successful: %s", _sanitize_for_log(email)) From 7dba88fa2fc5e5f2eb79aea3b1e311553a480b13 Mon Sep 17 00:00:00 2001 From: Naseem Ali <34807727+Naseem77@users.noreply.github.com> Date: Wed, 10 Dec 2025 15:59:35 +0200 Subject: [PATCH 3/6] fix ai comments --- api/routes/auth.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/api/routes/auth.py b/api/routes/auth.py index e7f5e8af..28e91f17 100644 --- a/api/routes/auth.py +++ b/api/routes/auth.py @@ -114,10 +114,18 @@ def _verify_password(password: str, stored_password_hex: str) -> bool: return False def _sanitize_for_log(value: str) -> str: - """Sanitize user input for logging by removing newlines and carriage returns.""" + """Sanitize user input for logging by removing all newlines and separator characters.""" if not isinstance(value, str): - return str(value) - return value.replace('\r\n', '').replace('\n', '').replace('\r', '') + value = str(value) + # Remove CR, LF, CRLF, and Unicode line/paragraph separators + return ( + value + .replace('\r\n', '') + .replace('\n', '') + .replace('\r', '') + .replace('\u2028', '') + .replace('\u2029', '') + ) def _validate_email(email: str) -> bool: """Basic email validation.""" @@ -257,7 +265,7 @@ async def email_signup(request: Request, signup_data: EmailSignupRequest) -> JSO f"{first_name} {last_name}", "email", api_token) if success and user_info and user_info["new_identity"]: - logging.info("New user created: %s", _sanitize_for_log(email)) + logging.info("New user created: [%s]", _sanitize_for_log(email)) # Hash password password_hash = _hash_password(password) @@ -267,13 +275,13 @@ async def email_signup(request: Request, signup_data: EmailSignupRequest) -> JSO else: # User already exists - return error instead of success - logging.info("Signup attempt for existing user: %s", _sanitize_for_log(email)) + logging.info("Signup attempt for existing user: [%s]", _sanitize_for_log(email)) return JSONResponse( {"success": False, "error": "An account with this email already exists"}, status_code=status.HTTP_409_CONFLICT ) - logging.info("User registration successful: %s", _sanitize_for_log(email)) + logging.info("User registration successful: [%s]", _sanitize_for_log(email)) response = JSONResponse({ "success": True, From f9ade42e038cb8004fc7cbb0f55b769ce32df6d1 Mon Sep 17 00:00:00 2001 From: Naseem Ali <34807727+Naseem77@users.noreply.github.com> Date: Sun, 4 Jan 2026 13:30:37 +0200 Subject: [PATCH 4/6] fix comments --- app/src/components/modals/LoginModal.tsx | 40 +-- app/src/components/modals/SignupModal.tsx | 327 ---------------------- app/src/types/api.ts | 2 +- 3 files changed, 21 insertions(+), 348 deletions(-) delete mode 100644 app/src/components/modals/SignupModal.tsx diff --git a/app/src/components/modals/LoginModal.tsx b/app/src/components/modals/LoginModal.tsx index c982e06c..c15b6a8d 100644 --- a/app/src/components/modals/LoginModal.tsx +++ b/app/src/components/modals/LoginModal.tsx @@ -221,61 +221,61 @@ const LoginModal = ({ open, onOpenChange, canClose = true, startInSignupMode = f Or -
{isSignIn ? "Don't have an account? " : "Already have an account? "} -
) : isSignIn ? ( -
+
- setEmail(e.target.value)} required disabled={isLoading} /> + setEmail(e.target.value)} required disabled={isLoading} data-testid="email-signin-input" />
- setPassword(e.target.value)} required disabled={isLoading} className="pr-10" /> -
- {error &&
{error}
} + {error &&
{error}
}
- +
- Don't have an account? + Don't have an account?
) : ( -
+
- setFirstName(e.target.value)} required disabled={isLoading} /> + setFirstName(e.target.value)} required disabled={isLoading} data-testid="firstname-input" />
- setLastName(e.target.value)} required disabled={isLoading} /> + setLastName(e.target.value)} required disabled={isLoading} data-testid="lastname-input" />
- setEmail(e.target.value)} required disabled={isLoading} /> + setEmail(e.target.value)} required disabled={isLoading} data-testid="email-signup-input" />
- setPassword(e.target.value)} required disabled={isLoading} minLength={8} className="pr-10" /> -
@@ -284,19 +284,19 @@ const LoginModal = ({ open, onOpenChange, canClose = true, startInSignupMode = f
- setConfirmPassword(e.target.value)} required disabled={isLoading} className="pr-10" /> -
- {error &&
{error}
} + {error &&
{error}
}
- +
- Already have an account? + Already have an account?
)} diff --git a/app/src/components/modals/SignupModal.tsx b/app/src/components/modals/SignupModal.tsx deleted file mode 100644 index 7cfd6b46..00000000 --- a/app/src/components/modals/SignupModal.tsx +++ /dev/null @@ -1,327 +0,0 @@ -import { useState, useEffect } from 'react'; -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { useAuth } from "@/contexts/AuthContext"; -import { buildApiUrl, API_CONFIG } from "@/config/api"; - -interface SignupModalProps { - open: boolean; - onOpenChange: (open: boolean) => void; - onSwitchToLogin?: () => void; - canClose?: boolean; - startWithEmailForm?: boolean; // Start with email form already open -} - -const SignupModal = ({ open, onOpenChange, onSwitchToLogin, canClose = true, startWithEmailForm = false }: SignupModalProps) => { - const { signup, refreshAuth } = useAuth(); - const [firstName, setFirstName] = useState(''); - const [lastName, setLastName] = useState(''); - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - const [confirmPassword, setConfirmPassword] = useState(''); - const [error, setError] = useState(''); - const [isLoading, setIsLoading] = useState(false); - const [showEmailForm, setShowEmailForm] = useState(startWithEmailForm); - - // Reset form when modal opens/closes or startWithEmailForm changes - useEffect(() => { - if (open) { - setShowEmailForm(startWithEmailForm); - setError(''); - setFirstName(''); - setLastName(''); - setEmail(''); - setPassword(''); - setConfirmPassword(''); - } else { - // Reset when closing - setShowEmailForm(false); - setError(''); - setFirstName(''); - setLastName(''); - setEmail(''); - setPassword(''); - setConfirmPassword(''); - } - }, [open, startWithEmailForm]); - - const handleGoogleSignup = () => { - window.location.href = buildApiUrl(API_CONFIG.ENDPOINTS.LOGIN_GOOGLE); - }; - - const handleGithubSignup = () => { - window.location.href = buildApiUrl(API_CONFIG.ENDPOINTS.LOGIN_GITHUB); - }; - - const handleEmailSignup = async (e: React.FormEvent) => { - e.preventDefault(); - setError(''); - - // Validation - if (!firstName.trim() || !lastName.trim() || !email.trim() || !password) { - setError('All fields are required'); - return; - } - - if (password.length < 8) { - setError('Password must be at least 8 characters long'); - return; - } - - if (password !== confirmPassword) { - setError('Passwords do not match'); - return; - } - - setIsLoading(true); - try { - const result = await signup.email(firstName, lastName, email, password); - - if (result.success) { - // Refresh auth to get user data - await refreshAuth(); - onOpenChange(false); - // Reset form - setFirstName(''); - setLastName(''); - setEmail(''); - setPassword(''); - setConfirmPassword(''); - setShowEmailForm(false); - } else { - setError(result.error || 'Signup failed'); - } - } catch (err) { - setError('An unexpected error occurred'); - } finally { - setIsLoading(false); - } - }; - - return ( - - { - if (!canClose) { - e.preventDefault(); - } - }} - onEscapeKeyDown={(e) => { - if (!canClose) { - e.preventDefault(); - } - }} - > - - - Create Your Account - - - Sign up to start using QueryWeaver - - - - {!showEmailForm ? ( -
- - - - -
-
- -
-
- Or -
-
- - - - {onSwitchToLogin && ( -
- Already have an account?{' '} - -
- )} -
- ) : ( -
-
-
- - setFirstName(e.target.value)} - required - disabled={isLoading} - /> -
-
- - setLastName(e.target.value)} - required - disabled={isLoading} - /> -
-
- -
- - setEmail(e.target.value)} - required - disabled={isLoading} - /> -
- -
- - setPassword(e.target.value)} - required - disabled={isLoading} - minLength={8} - /> -

- Must be at least 8 characters long -

-
- -
- - setConfirmPassword(e.target.value)} - required - disabled={isLoading} - /> -
- - {error && ( -
- {error} -
- )} - -
- - -
- - {onSwitchToLogin && ( -
- Already have an account?{' '} - -
- )} -
- )} - - {canClose && !showEmailForm && ( -
-

By signing up, you agree to our Terms of Service and Privacy Policy

-
- )} -
-
- ); -}; - -export default SignupModal; diff --git a/app/src/types/api.ts b/app/src/types/api.ts index 04e46ede..d901f2a8 100644 --- a/app/src/types/api.ts +++ b/app/src/types/api.ts @@ -6,7 +6,7 @@ export interface User { email: string; name?: string; picture?: string; - provider?: 'google' | 'github'; + provider?: 'google' | 'github' | 'email'; } // Authentication types From 4742e246077a1647a9ca4ef1570108fd6ed782d1 Mon Sep 17 00:00:00 2001 From: Naseem Ali <34807727+Naseem77@users.noreply.github.com> Date: Sun, 4 Jan 2026 14:41:59 +0200 Subject: [PATCH 5/6] add tests for signing with email --- .github/workflows/playwright.yml | 10 +- app/src/components/modals/LoginModal.tsx | 2 +- e2e/logic/pom/emailAuthPage.ts | 315 +++++++++++++++++ e2e/logic/pom/homePage.ts | 17 + e2e/tests/emailAuth.spec.ts | 412 +++++++++++++++++++++++ playwright.config.ts | 22 ++ 6 files changed, 774 insertions(+), 4 deletions(-) create mode 100644 e2e/logic/pom/emailAuthPage.ts create mode 100644 e2e/tests/emailAuth.spec.ts diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index a205e2b0..ffd2e7b7 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -141,17 +141,21 @@ jobs: - name: Create auth directory run: mkdir -p e2e/.auth + # List Playwright projects (for visibility) + - name: List Playwright projects + run: npx playwright test --list --reporter=list + # Run Playwright tests - name: Run Playwright tests run: npx playwright test --reporter=list env: CI: true - # Upload test results on failure + # Upload Playwright HTML report - uses: actions/upload-artifact@v4 - if: failure() + if: always() with: - name: playwright-report + name: playwright-html-report path: playwright-report/ retention-days: 30 diff --git a/app/src/components/modals/LoginModal.tsx b/app/src/components/modals/LoginModal.tsx index c15b6a8d..985ff6fa 100644 --- a/app/src/components/modals/LoginModal.tsx +++ b/app/src/components/modals/LoginModal.tsx @@ -221,7 +221,7 @@ const LoginModal = ({ open, onOpenChange, canClose = true, startInSignupMode = f Or
-
diff --git a/e2e/logic/pom/emailAuthPage.ts b/e2e/logic/pom/emailAuthPage.ts new file mode 100644 index 00000000..8283e9ff --- /dev/null +++ b/e2e/logic/pom/emailAuthPage.ts @@ -0,0 +1,315 @@ +import { Locator, Page } from "@playwright/test"; +import { waitForElementToBeVisible } from "../../infra/utils"; +import BasePage from "../../infra/ui/basePage"; + +/** + * Email Authentication Page Object Model + * Handles all email signup and signin functionality + */ +export class EmailAuthPage extends BasePage { + // ==================== LOCATORS ==================== + + // Email Authentication Buttons + private get emailAuthBtn(): Locator { + return this.page.getByTestId("email-auth-btn"); + } + + // Email Signup Form Elements + private get emailSignupForm(): Locator { + return this.page.getByTestId("email-signup-form"); + } + + private get firstNameInput(): Locator { + return this.page.getByTestId("firstname-input"); + } + + private get lastNameInput(): Locator { + return this.page.getByTestId("lastname-input"); + } + + private get emailSignupInput(): Locator { + return this.page.getByTestId("email-signup-input"); + } + + private get passwordSignupInput(): Locator { + return this.page.getByTestId("password-signup-input"); + } + + private get confirmPasswordInput(): Locator { + return this.page.getByTestId("confirm-password-input"); + } + + private get createAccountBtn(): Locator { + return this.page.getByTestId("create-account-btn"); + } + + private get signupError(): Locator { + return this.page.getByTestId("signup-error"); + } + + // Email Signin Form Elements + private get emailSigninForm(): Locator { + return this.page.getByTestId("email-signin-form"); + } + + private get emailSigninInput(): Locator { + return this.page.getByTestId("email-signin-input"); + } + + private get passwordSigninInput(): Locator { + return this.page.getByTestId("password-signin-input"); + } + + private get signinSubmitBtn(): Locator { + return this.page.getByTestId("signin-submit-btn"); + } + + private get signinError(): Locator { + return this.page.getByTestId("signin-error"); + } + + // Form Toggle Links + private get switchToSignupLink(): Locator { + return this.page.getByTestId("switch-to-signup-link"); + } + + private get switchToSigninLink(): Locator { + return this.page.getByTestId("switch-to-signin-link"); + } + + private get switchModeLink(): Locator { + return this.page.getByTestId("switch-mode-link"); + } + + // User Display Elements + private get userEmailDisplay(): Locator { + return this.page.getByTestId("user-email-display"); + } + + // ==================== INTERACTION HELPERS ==================== + + // Email Authentication Button - InteractWhenVisible + private async interactWithEmailAuthBtn(): Promise { + const isVisible = await waitForElementToBeVisible(this.emailAuthBtn); + if (!isVisible) throw new Error("Email auth button is not visible!"); + return this.emailAuthBtn; + } + + // Email Signup Form Elements - InteractWhenVisible + private async interactWithEmailSignupForm(): Promise { + const isVisible = await waitForElementToBeVisible(this.emailSignupForm); + if (!isVisible) throw new Error("Email signup form is not visible!"); + return this.emailSignupForm; + } + + private async interactWithFirstNameInput(): Promise { + const isVisible = await waitForElementToBeVisible(this.firstNameInput); + if (!isVisible) throw new Error("First name input is not visible!"); + return this.firstNameInput; + } + + private async interactWithLastNameInput(): Promise { + const isVisible = await waitForElementToBeVisible(this.lastNameInput); + if (!isVisible) throw new Error("Last name input is not visible!"); + return this.lastNameInput; + } + + private async interactWithEmailSignupInput(): Promise { + const isVisible = await waitForElementToBeVisible(this.emailSignupInput); + if (!isVisible) throw new Error("Email signup input is not visible!"); + return this.emailSignupInput; + } + + private async interactWithPasswordSignupInput(): Promise { + const isVisible = await waitForElementToBeVisible(this.passwordSignupInput); + if (!isVisible) throw new Error("Password signup input is not visible!"); + return this.passwordSignupInput; + } + + private async interactWithConfirmPasswordInput(): Promise { + const isVisible = await waitForElementToBeVisible(this.confirmPasswordInput); + if (!isVisible) throw new Error("Confirm password input is not visible!"); + return this.confirmPasswordInput; + } + + private async interactWithCreateAccountBtn(): Promise { + const isVisible = await waitForElementToBeVisible(this.createAccountBtn); + if (!isVisible) throw new Error("Create account button is not visible!"); + return this.createAccountBtn; + } + + private async interactWithSignupError(): Promise { + const isVisible = await waitForElementToBeVisible(this.signupError); + if (!isVisible) throw new Error("Signup error is not visible!"); + return this.signupError; + } + + // Email Signin Form Elements - InteractWhenVisible + private async interactWithEmailSigninForm(): Promise { + const isVisible = await waitForElementToBeVisible(this.emailSigninForm); + if (!isVisible) throw new Error("Email signin form is not visible!"); + return this.emailSigninForm; + } + + private async interactWithEmailSigninInput(): Promise { + const isVisible = await waitForElementToBeVisible(this.emailSigninInput); + if (!isVisible) throw new Error("Email signin input is not visible!"); + return this.emailSigninInput; + } + + private async interactWithPasswordSigninInput(): Promise { + const isVisible = await waitForElementToBeVisible(this.passwordSigninInput); + if (!isVisible) throw new Error("Password signin input is not visible!"); + return this.passwordSigninInput; + } + + private async interactWithSigninSubmitBtn(): Promise { + const isVisible = await waitForElementToBeVisible(this.signinSubmitBtn); + if (!isVisible) throw new Error("Signin submit button is not visible!"); + return this.signinSubmitBtn; + } + + private async interactWithSigninError(): Promise { + const isVisible = await waitForElementToBeVisible(this.signinError); + if (!isVisible) throw new Error("Signin error is not visible!"); + return this.signinError; + } + + // Form Toggle Links - InteractWhenVisible + private async interactWithSwitchToSignupLink(): Promise { + const isVisible = await waitForElementToBeVisible(this.switchToSignupLink); + if (!isVisible) throw new Error("Switch to signup link is not visible!"); + return this.switchToSignupLink; + } + + private async interactWithSwitchToSigninLink(): Promise { + const isVisible = await waitForElementToBeVisible(this.switchToSigninLink); + if (!isVisible) throw new Error("Switch to signin link is not visible!"); + return this.switchToSigninLink; + } + + private async interactWithSwitchModeLink(): Promise { + const isVisible = await waitForElementToBeVisible(this.switchModeLink); + if (!isVisible) throw new Error("Switch mode link is not visible!"); + return this.switchModeLink; + } + + // ==================== PUBLIC METHODS ==================== + + // Email Authentication Button Actions + async clickEmailAuthBtn(): Promise { + const element = await this.interactWithEmailAuthBtn(); + await element.click(); + } + + // Email Signup Form Functions + async fillFirstName(firstName: string): Promise { + const element = await this.interactWithFirstNameInput(); + await element.fill(firstName); + } + + async fillLastName(lastName: string): Promise { + const element = await this.interactWithLastNameInput(); + await element.fill(lastName); + } + + async fillEmailField(email: string): Promise { + const element = await this.interactWithEmailSignupInput(); + await element.fill(email); + } + + async fillPasswordField(password: string): Promise { + const element = await this.interactWithPasswordSignupInput(); + await element.fill(password); + } + + async fillConfirmPasswordField(password: string): Promise { + const element = await this.interactWithConfirmPasswordInput(); + await element.fill(password); + } + + async clickCreateAccountBtn(): Promise { + const element = await this.interactWithCreateAccountBtn(); + await element.click(); + } + + async getSignupErrorText(): Promise { + const element = await this.interactWithSignupError(); + return await element.textContent() || ''; + } + + async waitForSignupErrorVisible(): Promise { + await this.interactWithSignupError(); + } + + // Email Signin Form Functions + async fillEmailFieldSignin(email: string): Promise { + const element = await this.interactWithEmailSigninInput(); + await element.fill(email); + } + + async fillPasswordFieldSignin(password: string): Promise { + const element = await this.interactWithPasswordSigninInput(); + await element.fill(password); + } + + async clickSignInBtn(): Promise { + const element = await this.interactWithSigninSubmitBtn(); + await element.click(); + } + + async getSigninErrorText(): Promise { + const element = await this.interactWithSigninError(); + return await element.textContent() || ''; + } + + async waitForSigninErrorVisible(): Promise { + await this.interactWithSigninError(); + } + + // Form State Functions + async waitForEmailSignupFormVisible(): Promise { + await this.interactWithEmailSignupForm(); + } + + async waitForEmailSigninFormVisible(): Promise { + await this.interactWithEmailSigninForm(); + } + + async isSignupFormVisible(): Promise { + try { + return await this.emailSignupForm.isVisible(); + } catch { + return false; + } + } + + async isSigninFormVisible(): Promise { + try { + return await this.emailSigninForm.isVisible(); + } catch { + return false; + } + } + + // Form Toggle Actions + async clickSwitchToSigninLink(): Promise { + const element = await this.interactWithSwitchToSigninLink(); + await element.click(); + } + + async clickSwitchToSignupLink(): Promise { + const element = await this.interactWithSwitchToSignupLink(); + await element.click(); + } + + async clickSwitchModeLink(): Promise { + const element = await this.interactWithSwitchModeLink(); + await element.click(); + } + + // User Information + async getUserEmail(): Promise { + return await this.userEmailDisplay.textContent() || ''; + } +} diff --git a/e2e/logic/pom/homePage.ts b/e2e/logic/pom/homePage.ts index e7359b25..18db4260 100644 --- a/e2e/logic/pom/homePage.ts +++ b/e2e/logic/pom/homePage.ts @@ -604,6 +604,23 @@ export class HomePage extends BasePage { await element.click(); } + // Form State Functions + async waitForLoginModalVisible(): Promise { + await this.interactWithLoginModal(); + } + + async waitForLoginModalHidden(timeout: number = 5000): Promise { + await this.loginModal.waitFor({ state: 'hidden', timeout }); + } + + async isLoginModalVisible(): Promise { + try { + return await this.loginModal.isVisible(); + } catch { + return false; + } + } + // Database Connection Modal Functions async selectDatabaseType(type: "postgresql" | "mysql"): Promise { const element = await this.interactWithDatabaseTypeSelect(); diff --git a/e2e/tests/emailAuth.spec.ts b/e2e/tests/emailAuth.spec.ts new file mode 100644 index 00000000..b07b1200 --- /dev/null +++ b/e2e/tests/emailAuth.spec.ts @@ -0,0 +1,412 @@ +import { test, expect } from '@playwright/test'; +import { getBaseUrl } from '../config/urls'; +import BrowserWrapper from '../infra/ui/browserWrapper'; +import ApiCalls from '../logic/api/apiCalls'; +import { HomePage } from '../logic/pom/homePage'; +import { EmailAuthPage } from '../logic/pom/emailAuthPage'; + +/** + * Email Authentication Tests + * Tests email signup and signin functionality + * Each test is atomic and independent + * + * IMPORTANT: These tests run WITHOUT pre-authentication + * They use the 'chromium-unauthenticated' and 'firefox-unauthenticated' projects + */ +test.describe('Email Authentication Tests', () => { + let browser: BrowserWrapper; + let homePage: HomePage; + let emailAuthPage: EmailAuthPage; + + // Generate unique email for each test to ensure atomicity + const generateTestEmail = (testName: string) => { + const timestamp = Date.now(); + const random = Math.floor(Math.random() * 10000); + return `test.${testName}.${timestamp}.${random}@example.com`; + }; + + test.beforeEach(async () => { + browser = new BrowserWrapper(); + homePage = await browser.createNewPage(HomePage, getBaseUrl()); + const page = await browser.getPage(); + emailAuthPage = new EmailAuthPage(page); + await browser.setPageToFullScreen(); + }); + + test.afterEach(async () => { + await browser.closeBrowser(); + }); + + test('should successfully sign up with valid email credentials', async () => { + // Generate unique credentials + const email = generateTestEmail('valid-signup'); + const firstName = 'John'; + const lastName = 'Doe'; + const password = 'SecurePass123!'; + + // When unauthenticated, the login modal appears automatically + // Wait for modal to be visible + await homePage.waitForLoginModalVisible(); + + // Modal opens in signin mode by default, switch to signup mode + // Note: switchMode() also shows the email form automatically + await emailAuthPage.clickSwitchModeLink(); + + // Wait for email signup form to appear + await emailAuthPage.waitForEmailSignupFormVisible(); + + // Fill in signup form + await emailAuthPage.fillFirstName(firstName); + await emailAuthPage.fillLastName(lastName); + await emailAuthPage.fillEmailField(email); + await emailAuthPage.fillPasswordField(password); + await emailAuthPage.fillConfirmPasswordField(password); + + // Submit signup form + await emailAuthPage.clickCreateAccountBtn(); + + // Wait for successful signup (modal should close and user should be authenticated) + await homePage.waitForLoginModalHidden(10000); + + // Verify user is logged in + const isLoggedIn = await homePage.isLoggedIn(); + expect(isLoggedIn).toBeTruthy(); + + // Verify user menu shows correct email + await homePage.clickOnUserMenu(); + const userEmail = await emailAuthPage.getUserEmail(); + expect(userEmail).toBe(email); + }); + + test('should fail signup without email', async () => { + const firstName = 'John'; + const lastName = 'Doe'; + const password = 'SecurePass123!'; + + // Login modal appears automatically for unauthenticated users + await homePage.waitForLoginModalVisible(); + + // Switch to signup mode (this also shows the email form) + await emailAuthPage.clickSwitchModeLink(); + + // Wait for email signup form to appear + await emailAuthPage.waitForEmailSignupFormVisible(); + + // Fill form WITHOUT email + await emailAuthPage.fillFirstName(firstName); + await emailAuthPage.fillLastName(lastName); + await emailAuthPage.fillPasswordField(password); + await emailAuthPage.fillConfirmPasswordField(password); + + // Attempt to submit (should be blocked by HTML5 validation) + await emailAuthPage.clickCreateAccountBtn(); + + // Verify modal is still open and error is shown or button is disabled + const isModalVisible = await homePage.isLoginModalVisible(); + expect(isModalVisible).toBeTruthy(); + + // User should still not be logged in + const isLoggedIn = await homePage.isLoggedIn(); + expect(isLoggedIn).toBeFalsy(); + }); + + test('should fail signup without password', async () => { + const email = generateTestEmail('no-password'); + const firstName = 'John'; + const lastName = 'Doe'; + + // Login modal appears automatically for unauthenticated users + await homePage.waitForLoginModalVisible(); + + // Switch to signup mode (this also shows the email form) + await emailAuthPage.clickSwitchModeLink(); + + // Wait for email signup form to appear + await emailAuthPage.waitForEmailSignupFormVisible(); + + // Fill form WITHOUT password + await emailAuthPage.fillFirstName(firstName); + await emailAuthPage.fillLastName(lastName); + await emailAuthPage.fillEmailField(email); + + // Attempt to submit (should be blocked by HTML5 validation) + await emailAuthPage.clickCreateAccountBtn(); + + // Verify modal is still open + const isModalVisible = await homePage.isLoginModalVisible(); + expect(isModalVisible).toBeTruthy(); + + // User should still not be logged in + const isLoggedIn = await homePage.isLoggedIn(); + expect(isLoggedIn).toBeFalsy(); + }); + + test('should fail signup without first name', async () => { + const email = generateTestEmail('no-firstname'); + const lastName = 'Doe'; + const password = 'SecurePass123!'; + + // Login modal appears automatically for unauthenticated users + await homePage.waitForLoginModalVisible(); + + // Switch to signup mode (this also shows the email form) + await emailAuthPage.clickSwitchModeLink(); + + // Wait for email signup form to appear + await emailAuthPage.waitForEmailSignupFormVisible(); + + // Fill form WITHOUT first name + await emailAuthPage.fillLastName(lastName); + await emailAuthPage.fillEmailField(email); + await emailAuthPage.fillPasswordField(password); + await emailAuthPage.fillConfirmPasswordField(password); + + // Attempt to submit (should be blocked by HTML5 validation) + await emailAuthPage.clickCreateAccountBtn(); + + // Verify modal is still open + const isModalVisible = await homePage.isLoginModalVisible(); + expect(isModalVisible).toBeTruthy(); + + // User should still not be logged in + const isLoggedIn = await homePage.isLoggedIn(); + expect(isLoggedIn).toBeFalsy(); + }); + + test('should fail signup without last name', async () => { + const email = generateTestEmail('no-lastname'); + const firstName = 'John'; + const password = 'SecurePass123!'; + + // Login modal appears automatically for unauthenticated users + await homePage.waitForLoginModalVisible(); + + // Switch to signup mode (this also shows the email form) + await emailAuthPage.clickSwitchModeLink(); + + // Wait for email signup form to appear + await emailAuthPage.waitForEmailSignupFormVisible(); + + // Fill form WITHOUT last name + await emailAuthPage.fillFirstName(firstName); + await emailAuthPage.fillEmailField(email); + await emailAuthPage.fillPasswordField(password); + await emailAuthPage.fillConfirmPasswordField(password); + + // Attempt to submit (should be blocked by HTML5 validation) + await emailAuthPage.clickCreateAccountBtn(); + + // Verify modal is still open + const isModalVisible = await homePage.isLoginModalVisible(); + expect(isModalVisible).toBeTruthy(); + + // User should still not be logged in + const isLoggedIn = await homePage.isLoggedIn(); + expect(isLoggedIn).toBeFalsy(); + }); + + test('should fail signup with password mismatch', async () => { + const email = generateTestEmail('password-mismatch'); + const firstName = 'John'; + const lastName = 'Doe'; + const password = 'SecurePass123!'; + const wrongPassword = 'DifferentPass456!'; + + // Login modal appears automatically for unauthenticated users + await homePage.waitForLoginModalVisible(); + + // Switch to signup mode (this also shows the email form) + await emailAuthPage.clickSwitchModeLink(); + + // Wait for email signup form to appear + await emailAuthPage.waitForEmailSignupFormVisible(); + + // Fill form with mismatched passwords + await emailAuthPage.fillFirstName(firstName); + await emailAuthPage.fillLastName(lastName); + await emailAuthPage.fillEmailField(email); + await emailAuthPage.fillPasswordField(password); + await emailAuthPage.fillConfirmPasswordField(wrongPassword); + + // Attempt to submit + await emailAuthPage.clickCreateAccountBtn(); + + // Verify error message is shown + await emailAuthPage.waitForSignupErrorVisible(); + const errorText = await emailAuthPage.getSignupErrorText(); + expect(errorText).toContain('Passwords do not match'); + + // User should still not be logged in + const isLoggedIn = await homePage.isLoggedIn(); + expect(isLoggedIn).toBeFalsy(); + }); + + test('should fail signup with short password (less than 8 characters)', async () => { + const email = generateTestEmail('short-password'); + const firstName = 'John'; + const lastName = 'Doe'; + const shortPassword = 'Pass12!'; + + // Login modal appears automatically for unauthenticated users + await homePage.waitForLoginModalVisible(); + + // Switch to signup mode (this also shows the email form) + await emailAuthPage.clickSwitchModeLink(); + + // Wait for email signup form to appear + await emailAuthPage.waitForEmailSignupFormVisible(); + + // Fill form with short password + await emailAuthPage.fillFirstName(firstName); + await emailAuthPage.fillLastName(lastName); + await emailAuthPage.fillEmailField(email); + await emailAuthPage.fillPasswordField(shortPassword); + await emailAuthPage.fillConfirmPasswordField(shortPassword); + + // Attempt to submit (should be blocked by HTML5 minLength validation) + await emailAuthPage.clickCreateAccountBtn(); + + // Verify modal is still open (form blocked by HTML5 validation) + const isModalVisible = await homePage.isLoginModalVisible(); + expect(isModalVisible).toBeTruthy(); + + // User should still not be logged in + const isLoggedIn = await homePage.isLoggedIn(); + expect(isLoggedIn).toBeFalsy(); + }); + + test('should successfully sign in with email after signup via API', async ({ request }) => { + // Step 1: Create user via API (setup) + const email = generateTestEmail('signin-test'); + const firstName = 'Jane'; + const lastName = 'Smith'; + const password = 'SecurePass456!'; + + const signupApi = new ApiCalls(); + const signupResponse = await signupApi.signupWithEmail(firstName, lastName, email, password, request); + expect(signupResponse.success).toBeTruthy(); + + // Step 2: Test signin via UI + // Login modal appears automatically for unauthenticated users + await homePage.waitForLoginModalVisible(); + + // Navigate to sign in with email form (modal opens in signin mode by default) + await emailAuthPage.clickEmailAuthBtn(); + await emailAuthPage.waitForEmailSigninFormVisible(); + + // Fill in signin credentials + await emailAuthPage.fillEmailFieldSignin(email); + await emailAuthPage.fillPasswordFieldSignin(password); + + // Submit signin form + await emailAuthPage.clickSignInBtn(); + + // Wait for successful signin (modal should close) + await homePage.waitForLoginModalHidden(10000); + + // Verify user is logged in + const isLoggedIn = await homePage.isLoggedIn(); + expect(isLoggedIn).toBeTruthy(); + + // Verify correct user email + await homePage.clickOnUserMenu(); + const userEmail = await emailAuthPage.getUserEmail(); + expect(userEmail).toBe(email); + }); + + test('should fail signin with wrong password', async ({ request }) => { + // Step 1: Create user via API + const email = generateTestEmail('wrong-password'); + const firstName = 'Bob'; + const lastName = 'Johnson'; + const correctPassword = 'CorrectPass789!'; + const wrongPassword = 'WrongPass123!'; + + const signupApi = new ApiCalls(); + await signupApi.signupWithEmail(firstName, lastName, email, correctPassword, request); + + // Step 2: Attempt signin with wrong password + // Login modal appears automatically for unauthenticated users + await homePage.waitForLoginModalVisible(); + + // Navigate to sign in with email form (modal opens in signin mode by default) + await emailAuthPage.clickEmailAuthBtn(); + await emailAuthPage.waitForEmailSigninFormVisible(); + + // Fill with wrong password + await emailAuthPage.fillEmailFieldSignin(email); + await emailAuthPage.fillPasswordFieldSignin(wrongPassword); + + // Attempt signin + await emailAuthPage.clickSignInBtn(); + + // Verify error message is shown + await emailAuthPage.waitForSigninErrorVisible(); + const errorText = await emailAuthPage.getSigninErrorText(); + expect(errorText).toBeTruthy(); + + // User should not be logged in + const isLoggedIn = await homePage.isLoggedIn(); + expect(isLoggedIn).toBeFalsy(); + }); + + test('should fail signin with non-existent email', async () => { + const nonExistentEmail = generateTestEmail('non-existent'); + const password = 'SomePassword123!'; + + // Login modal appears automatically for unauthenticated users + await homePage.waitForLoginModalVisible(); + + // Navigate to sign in with email form (modal opens in signin mode by default) + await emailAuthPage.clickEmailAuthBtn(); + await emailAuthPage.waitForEmailSigninFormVisible(); + + // Fill with non-existent credentials + await emailAuthPage.fillEmailFieldSignin(nonExistentEmail); + await emailAuthPage.fillPasswordFieldSignin(password); + + // Attempt signin + await emailAuthPage.clickSignInBtn(); + + // Verify error message is shown + await emailAuthPage.waitForSigninErrorVisible(); + const errorText = await emailAuthPage.getSigninErrorText(); + expect(errorText).toBeTruthy(); + + // User should not be logged in + const isLoggedIn = await homePage.isLoggedIn(); + expect(isLoggedIn).toBeFalsy(); + }); + + test('should toggle between signup and signin modes', async () => { + // Login modal appears automatically for unauthenticated users + await homePage.waitForLoginModalVisible(); + + // Modal opens in signin mode by default, switch to signup mode + // Note: switchMode() also shows the email form automatically + await emailAuthPage.clickSwitchModeLink(); + + // Wait for signup form (shown automatically after switch) + await emailAuthPage.waitForEmailSignupFormVisible(); + + // Verify signup form is visible + let isSignupFormVisible = await emailAuthPage.isSignupFormVisible(); + expect(isSignupFormVisible).toBeTruthy(); + + // Switch to signin + await emailAuthPage.clickSwitchToSigninLink(); + await emailAuthPage.waitForEmailSigninFormVisible(); + + // Verify signin form is visible + const isSigninFormVisible = await emailAuthPage.isSigninFormVisible(); + expect(isSigninFormVisible).toBeTruthy(); + + // Switch back to signup + await emailAuthPage.clickSwitchToSignupLink(); + await emailAuthPage.waitForEmailSignupFormVisible(); + + // Verify signup form is visible again + isSignupFormVisible = await emailAuthPage.isSignupFormVisible(); + expect(isSignupFormVisible).toBeTruthy(); + }); +}); diff --git a/playwright.config.ts b/playwright.config.ts index 7f9f78fa..fc0a506b 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -43,6 +43,7 @@ export default defineConfig({ testMatch: /.*\.setup\.ts/, }, + // Authenticated tests (most tests) { name: 'chromium', use: { @@ -51,6 +52,7 @@ export default defineConfig({ storageState: 'e2e/.auth/user.json', }, dependencies: ['setup'], + testIgnore: '**/emailAuth.spec.ts', // Exclude email auth tests }, { @@ -61,6 +63,26 @@ export default defineConfig({ storageState: 'e2e/.auth/user.json', }, dependencies: ['setup'], + testIgnore: '**/emailAuth.spec.ts', // Exclude email auth tests + }, + + // Unauthenticated tests (for testing signup/signin flows) + { + name: 'chromium-unauthenticated', + use: { + ...devices['Desktop Chrome'], + // No storageState - tests run without authentication + }, + testMatch: '**/emailAuth.spec.ts', // Only run email auth tests + }, + + { + name: 'firefox-unauthenticated', + use: { + ...devices['Desktop Firefox'], + // No storageState - tests run without authentication + }, + testMatch: '**/emailAuth.spec.ts', // Only run email auth tests }, // { From 3fb5e32ce67014c5757ce90a36d2df0b4f6a4f0b Mon Sep 17 00:00:00 2001 From: Naseem Ali <34807727+Naseem77@users.noreply.github.com> Date: Sun, 4 Jan 2026 15:14:28 +0200 Subject: [PATCH 6/6] fix comments Error Handling - Fixed properly with proper status codes Email Enumeration - Fixed with generic error messages --- api/auth/user_management.py | 12 +++++++----- api/routes/auth.py | 27 ++++++++++++++++++--------- app/src/pages/Index.tsx | 5 +++-- 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/api/auth/user_management.py b/api/auth/user_management.py index c7a82196..c8b1254f 100644 --- a/api/auth/user_management.py +++ b/api/auth/user_management.py @@ -40,7 +40,7 @@ async def _get_user_info(api_token: str) -> Optional[Dict[str, Any]]: """ query = """ MATCH (i:Identity)-[:HAS_TOKEN]->(t:Token {id: $api_token}) - RETURN i.email, i.name, i.picture, (t IS NOT NULL AND timestamp() <= t.expires_at) AS token_valid + RETURN i.provider_user_id, i.email, i.name, i.picture, i.provider, (t IS NOT NULL AND timestamp() <= t.expires_at) AS token_valid """ try: @@ -56,13 +56,15 @@ async def _get_user_info(api_token: str) -> Optional[Dict[str, Any]]: if result.result_set: single_result = result.result_set[0] - token_valid = single_result[3] + token_valid = single_result[5] # Updated index due to new fields if token_valid: return { - "email": single_result[0], - "name": single_result[1], - "picture": single_result[2], + "id": single_result[0], # provider_user_id as id + "email": single_result[1], + "name": single_result[2], + "picture": single_result[3], + "provider": single_result[4], } # Delete invalid/expired token from DB for cleanup await delete_user_token(api_token) diff --git a/api/routes/auth.py b/api/routes/auth.py index 28e91f17..714499b4 100644 --- a/api/routes/auth.py +++ b/api/routes/auth.py @@ -264,7 +264,24 @@ async def email_signup(request: Request, signup_data: EmailSignupRequest) -> JSO success, user_info = await ensure_user_in_organizations(email, email, f"{first_name} {last_name}", "email", api_token) - if success and user_info and user_info["new_identity"]: + # Check for system errors (success=False and user_info=None) + if not success and user_info is None: + logging.error("System error during user creation: [%s]", _sanitize_for_log(email)) + return JSONResponse( + {"success": False, "error": "Registration failed. Please try again later."}, + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + # Check if user already exists (success=False and user_info is not None) + if not success and user_info is not None: + logging.info("Signup attempt for existing user: [%s]", _sanitize_for_log(email)) + return JSONResponse( + {"success": False, "error": "Registration failed. Please try again."}, + status_code=status.HTTP_400_BAD_REQUEST + ) + + # New user created successfully (success=True and user_info["new_identity"]=True) + if success and user_info and user_info.get("new_identity"): logging.info("New user created: [%s]", _sanitize_for_log(email)) # Hash password @@ -273,14 +290,6 @@ async def email_signup(request: Request, signup_data: EmailSignupRequest) -> JSO # Set email hash await _set_mail_hash(email, password_hash) - else: - # User already exists - return error instead of success - logging.info("Signup attempt for existing user: [%s]", _sanitize_for_log(email)) - return JSONResponse( - {"success": False, "error": "An account with this email already exists"}, - status_code=status.HTTP_409_CONFLICT - ) - logging.info("User registration successful: [%s]", _sanitize_for_log(email)) response = JSONResponse({ diff --git a/app/src/pages/Index.tsx b/app/src/pages/Index.tsx index 8661703d..5a1e8c04 100644 --- a/app/src/pages/Index.tsx +++ b/app/src/pages/Index.tsx @@ -116,12 +116,13 @@ const Index = () => { // Show login modal when not authenticated after loading completes useEffect(() => { // Auto-open the login modal and keep it open until user authenticates - if (!authLoading && !isAuthenticated) { + // Only disable closing if we're auto-opening (not if user manually opened it) + if (!authLoading && !isAuthenticated && !showLoginModal) { setShowSignupMode(false); setLoginModalCanClose(false); // Don't allow closing until authenticated setShowLoginModal(true); } - }, [authLoading, isAuthenticated]); + }, [authLoading, isAuthenticated, showLoginModal]); const handleConnectDatabase = () => { if (isRefreshingSchema || isChatProcessing) return;