Fix #7 -- backend now uses and stores hashed password

This commit is contained in:
James Ketr 2025-05-30 12:19:28 -07:00
parent a03497a552
commit 7280672726
2 changed files with 33 additions and 7 deletions

View File

@ -99,6 +99,7 @@ const LoginPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
const [passwordValidation, setPasswordValidation] = useState<{ isValid: boolean; issues: string[] }>({ isValid: true, issues: [] }); const [passwordValidation, setPasswordValidation] = useState<{ isValid: boolean; issues: string[] }>({ isValid: true, issues: [] });
const name = (user?.userType === 'candidate') ? user.username : user?.email || ''; const name = (user?.userType === 'candidate') ? user.username : user?.email || '';
const [location, setLocation] = useState<Partial<Location>>({}); const [location, setLocation] = useState<Partial<Location>>({});
const [errorMessage, setErrorMessage] = useState<string | null>(null);
// Password visibility states // Password visibility states
const [showLoginPassword, setShowLoginPassword] = useState(false); const [showLoginPassword, setShowLoginPassword] = useState(false);
@ -169,6 +170,23 @@ const LoginPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
} }
}, [phone, registerForm]); }, [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) => { const handleLogin = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
setLoading(true); setLoading(true);
@ -177,6 +195,7 @@ const LoginPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
const success = await login(loginForm); const success = await login(loginForm);
if (success) { if (success) {
setSuccess('Login successful!'); setSuccess('Login successful!');
setLoading(false);
} }
}; };
@ -239,8 +258,8 @@ const LoginPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
if (registerForm.userType === 'candidate') { if (registerForm.userType === 'candidate') {
window.location.href = '/candidate/dashboard'; window.location.href = '/candidate/dashboard';
} }
}
setLoading(false); setLoading(false);
}
}; };
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => { const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
@ -383,9 +402,9 @@ const LoginPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
</Tabs> </Tabs>
</Box> </Box>
{error && ( {errorMessage && (
<Alert severity="error" sx={{ mb: 2 }}> <Alert severity="error" sx={{ mb: 2 }}>
{error} {errorMessage}
</Alert> </Alert>
)} )}

View File

@ -4,10 +4,11 @@ Secure Authentication Utilities
Provides password hashing, verification, and security features Provides password hashing, verification, and security features
""" """
import traceback
import bcrypt # type: ignore import bcrypt # type: ignore
import secrets import secrets
import logging import logging
from datetime import datetime, timezone from datetime import datetime, timezone, timedelta
from typing import Dict, Any, Optional, Tuple from typing import Dict, Any, Optional, Tuple
from pydantic import BaseModel # type: ignore from pydantic import BaseModel # type: ignore
@ -156,8 +157,14 @@ class AuthenticationManager:
# Check if account is locked # Check if account is locked
if auth_data.locked_until and auth_data.locked_until > datetime.now(timezone.utc): if auth_data.locked_until and auth_data.locked_until > datetime.now(timezone.utc):
logger.warning(f"🔒 Account locked for user {login}") time_until_unlock = auth_data.locked_until - datetime.now(timezone.utc)
return False, None, "Account is temporarily locked due to too many failed attempts" # 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 # Verify password
if not self.password_security.verify_password(password, auth_data.password_hash): if not self.password_security.verify_password(password, auth_data.password_hash):
@ -166,7 +173,6 @@ class AuthenticationManager:
# Lock account if too many attempts # Lock account if too many attempts
if auth_data.login_attempts >= SecurityConfig.MAX_LOGIN_ATTEMPTS: if auth_data.login_attempts >= SecurityConfig.MAX_LOGIN_ATTEMPTS:
from datetime import timedelta
auth_data.locked_until = datetime.now(timezone.utc) + timedelta( auth_data.locked_until = datetime.now(timezone.utc) + timedelta(
minutes=SecurityConfig.ACCOUNT_LOCKOUT_DURATION_MINUTES minutes=SecurityConfig.ACCOUNT_LOCKOUT_DURATION_MINUTES
) )
@ -188,6 +194,7 @@ class AuthenticationManager:
return True, user_data, None return True, user_data, None
except Exception as e: except Exception as e:
logger.error(traceback.format_exc())
logger.error(f"❌ Authentication error for user {login}: {e}") logger.error(f"❌ Authentication error for user {login}: {e}")
return False, None, "Authentication failed" return False, None, "Authentication failed"