diff --git a/frontend/src/pages/LoginPage.tsx b/frontend/src/pages/LoginPage.tsx index a2fa527..92dd82a 100644 --- a/frontend/src/pages/LoginPage.tsx +++ b/frontend/src/pages/LoginPage.tsx @@ -99,6 +99,7 @@ const LoginPage: React.FC = (props: BackstoryPageProps) => { const [passwordValidation, setPasswordValidation] = useState<{ isValid: boolean; issues: string[] }>({ isValid: true, issues: [] }); const name = (user?.userType === 'candidate') ? user.username : user?.email || ''; const [location, setLocation] = useState>({}); + const [errorMessage, setErrorMessage] = useState(null); // Password visibility states const [showLoginPassword, setShowLoginPassword] = useState(false); @@ -169,6 +170,23 @@ const LoginPage: React.FC = (props: BackstoryPageProps) => { } }, [phone, registerForm]); + useEffect(() => { + if (!loading || !error) { + return; + } + if (loading && error) { + /* Remove 'HTTP .*: ' from error string */ + const jsonStr = error.replace(/^[^{]*/, ''); + const data = JSON.parse(jsonStr); + setErrorMessage(data.error.message); + setSnack(data.error.message, "error"); + setTimeout(() => { + setErrorMessage(null); + setLoading(false); + }, 3000); + } + }, [error, loading]); + const handleLogin = async (e: React.FormEvent) => { e.preventDefault(); setLoading(true); @@ -177,6 +195,7 @@ const LoginPage: React.FC = (props: BackstoryPageProps) => { const success = await login(loginForm); if (success) { setSuccess('Login successful!'); + setLoading(false); } }; @@ -239,8 +258,8 @@ const LoginPage: React.FC = (props: BackstoryPageProps) => { if (registerForm.userType === 'candidate') { window.location.href = '/candidate/dashboard'; } + setLoading(false); } - setLoading(false); }; const handleTabChange = (event: React.SyntheticEvent, newValue: number) => { @@ -383,9 +402,9 @@ const LoginPage: React.FC = (props: BackstoryPageProps) => { - {error && ( + {errorMessage && ( - {error} + {errorMessage} )} diff --git a/src/backend/auth_utils.py b/src/backend/auth_utils.py index ddac649..17a487a 100644 --- a/src/backend/auth_utils.py +++ b/src/backend/auth_utils.py @@ -4,10 +4,11 @@ Secure Authentication Utilities Provides password hashing, verification, and security features """ +import traceback import bcrypt # type: ignore import secrets import logging -from datetime import datetime, timezone +from datetime import datetime, timezone, timedelta from typing import Dict, Any, Optional, Tuple from pydantic import BaseModel # type: ignore @@ -156,8 +157,14 @@ class AuthenticationManager: # Check if account is locked if auth_data.locked_until and auth_data.locked_until > datetime.now(timezone.utc): - logger.warning(f"🔒 Account locked for user {login}") - return False, None, "Account is temporarily locked due to too many failed attempts" + time_until_unlock = auth_data.locked_until - datetime.now(timezone.utc) + # Convert time_until_unlock to minutes:seconds format + total_seconds = time_until_unlock.total_seconds() + minutes = int(total_seconds // 60) + seconds = int(total_seconds % 60) + time_until_unlock_str = f"{minutes}m {seconds}s" + logger.warning(f"🔒 Account is locked for user {login} for another {time_until_unlock_str}.") + return False, None, f"Account is temporarily locked due to too many failed attempts. Retry after {time_until_unlock_str}" # Verify password if not self.password_security.verify_password(password, auth_data.password_hash): @@ -166,7 +173,6 @@ class AuthenticationManager: # Lock account if too many attempts if auth_data.login_attempts >= SecurityConfig.MAX_LOGIN_ATTEMPTS: - from datetime import timedelta auth_data.locked_until = datetime.now(timezone.utc) + timedelta( minutes=SecurityConfig.ACCOUNT_LOCKOUT_DURATION_MINUTES ) @@ -188,6 +194,7 @@ class AuthenticationManager: return True, user_data, None except Exception as e: + logger.error(traceback.format_exc()) logger.error(f"❌ Authentication error for user {login}: {e}") return False, None, "Authentication failed"