Compare commits

...

2 Commits

10 changed files with 143 additions and 49 deletions

View File

@ -29,10 +29,11 @@ import {
} from '@mui/icons-material'; } from '@mui/icons-material';
import { NavigationLinkType } from 'components/layout/BackstoryLayout'; import { NavigationLinkType } from 'components/layout/BackstoryLayout';
import { Beta } from 'components/Beta'; import { Beta } from 'components/ui/Beta';
import { Candidate, Employer } from 'types/types'; import { Candidate, Employer } from 'types/types';
import { SetSnackType } from 'components/Snack'; import { SetSnackType } from 'components/Snack';
import { CopyBubble } from 'components/CopyBubble'; import { CopyBubble } from 'components/CopyBubble';
import { BackstoryLogo } from 'components/ui/BackstoryLogo';
import 'components/layout/Header.css'; import 'components/layout/Header.css';
import { useAuth } from 'hooks/AuthContext'; import { useAuth } from 'hooks/AuthContext';
@ -189,32 +190,6 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
const name = (user?.firstName || user?.email || ''); const name = (user?.firstName || user?.email || '');
const BackstoryLogo = () => {
return <Typography
variant="h6"
className="BackstoryLogo"
noWrap
sx={{
cursor: "pointer",
fontWeight: 700,
letterSpacing: '.2rem',
color: theme.palette.primary.contrastText,
textDecoration: 'none',
display: "flex",
flexDirection: "row",
alignItems: "center",
gap: 1,
textTransform: "uppercase",
}}
>
<Avatar sx={{ width: 24, height: 24 }}
variant="rounded"
alt="Backstory logo"
src="/logo192.png" />
Backstory
</Typography>
};
const navLinks : NavigationLinkType[] = [ const navLinks : NavigationLinkType[] = [
{name: "Home", path: "/", label: <BackstoryLogo/>}, {name: "Home", path: "/", label: <BackstoryLogo/>},
...navigationLinks ...navigationLinks

View File

@ -0,0 +1,38 @@
import React from 'react';
import {
Typography,
Avatar,
} from '@mui/material';
import { useTheme } from '@mui/material/styles';
import 'components/layout/Header.css';
const BackstoryLogo = () => {
const theme = useTheme();
return <Typography
variant="h6"
className="BackstoryLogo"
noWrap
sx={{
cursor: "pointer",
fontWeight: 700,
letterSpacing: '.2rem',
color: theme.palette.primary.contrastText,
textDecoration: 'none',
display: "flex",
flexDirection: "row",
alignItems: "center",
gap: 1,
textTransform: "uppercase",
}}
>
<Avatar sx={{ width: 24, height: 24 }}
variant="rounded"
alt="Backstory logo"
src="/logo192.png" />
Backstory
</Typography>
};
export { BackstoryLogo };

View File

@ -0,0 +1,30 @@
.ComingSoon {
display: flex;
position: relative;
flex: 1;
pointer-events: none;
z-index: 1101;
cursor: pointer;
font-family: 'Roboto';
line-height: 40px;
overflow: hidden;
padding: 8px;
}
.ComingSoon-label {
display: flex;
position: absolute;
top: 0px;
left: 0px;
width: 100%;
height: 100%;
justify-content: center;
align-items: center;
font-size: 28px;
text-align: center;
font-weight: bold;
color: #d8d8d8;
background: rgba(0, 0, 0, 0.5);
z-index: 11;
pointer-events: none;
}

View File

@ -0,0 +1,25 @@
import React, { useEffect, useRef, useState } from 'react';
import Box from '@mui/material/Box';
import useMediaQuery from '@mui/material/useMediaQuery';
import { SxProps, useTheme } from '@mui/material/styles';
import './ComingSoon.css';
type ComingSoonProps = {
children?: React.ReactNode;
}
const ComingSoon: React.FC<ComingSoonProps> = (props : ComingSoonProps) => {
const { children } = props;
const theme = useTheme();
return (
<Box className="ComingSoon">
<Box className="ComingSoon-label">Coming Soon</Box>
{children}
</Box>
);
};
export {
ComingSoon
};

View File

@ -13,7 +13,7 @@ import {
import { useTheme } from '@mui/material/styles'; import { useTheme } from '@mui/material/styles';
import ConstructionIcon from '@mui/icons-material/Construction'; import ConstructionIcon from '@mui/icons-material/Construction';
import RocketLaunchIcon from '@mui/icons-material/RocketLaunch'; import RocketLaunchIcon from '@mui/icons-material/RocketLaunch';
import { Beta } from '../components/Beta'; import { Beta } from '../components/ui/Beta';
interface BetaPageProps { interface BetaPageProps {
children?: React.ReactNode; children?: React.ReactNode;

View File

@ -19,6 +19,7 @@ import WorkHistoryIcon from '@mui/icons-material/WorkHistory';
import QuestionAnswerIcon from '@mui/icons-material/QuestionAnswer'; import QuestionAnswerIcon from '@mui/icons-material/QuestionAnswer';
import DescriptionIcon from '@mui/icons-material/Description'; import DescriptionIcon from '@mui/icons-material/Description';
import professionalConversationPng from './Conversation.png'; import professionalConversationPng from './Conversation.png';
import { ComingSoon } from 'components/ui/ComingSoon';
// Placeholder for Testimonials component // Placeholder for Testimonials component
const Testimonials = () => { const Testimonials = () => {
@ -302,6 +303,7 @@ const HomePage = () => {
</ActionButton> </ActionButton>
</Box> </Box>
<ComingSoon>
<Box sx={{ flex: 1 }}> <Box sx={{ flex: 1 }}>
<Typography variant="h4" component="h3" gutterBottom sx={{ color: 'primary.main' }}> <Typography variant="h4" component="h3" gutterBottom sx={{ color: 'primary.main' }}>
For Employers For Employers
@ -389,6 +391,7 @@ const HomePage = () => {
Start Recruiting Start Recruiting
</ActionButton> </ActionButton>
</Box> </Box>
</ComingSoon>
</Box> </Box>
</Container> </Container>

View File

@ -11,8 +11,6 @@ import {
CircularProgress, CircularProgress,
Tabs, Tabs,
Tab, Tab,
AppBar,
Toolbar,
Card, Card,
CardContent, CardContent,
Divider, Divider,
@ -35,15 +33,12 @@ import {
Person, Person,
PersonAdd, PersonAdd,
AccountCircle, AccountCircle,
ExitToApp,
Visibility, Visibility,
VisibilityOff, VisibilityOff,
CheckCircle, CheckCircle,
Cancel, Cancel,
ExpandLess, ExpandLess,
ExpandMore, ExpandMore,
Visibility as ViewIcon,
Work,
Business Business
} from '@mui/icons-material'; } from '@mui/icons-material';
import 'react-phone-number-input/style.css'; import 'react-phone-number-input/style.css';
@ -55,10 +50,10 @@ import { ApiClient } from 'services/api-client';
import { useAuth } from 'hooks/AuthContext'; import { useAuth } from 'hooks/AuthContext';
import { LocationInput } from 'components/LocationInput'; import { LocationInput } from 'components/LocationInput';
import { Location } from 'types/types'; import { Location } from 'types/types';
import { BackstoryLogo } from 'components/ui/BackstoryLogo';
import { Candidate } from 'types/types'
import { useNavigate } from 'react-router-dom';
import { BackstoryPageProps } from 'components/BackstoryTab'; import { BackstoryPageProps } from 'components/BackstoryTab';
import { Navigate, useNavigate } from 'react-router-dom';
type UserRegistrationType = 'candidate' | 'employer'; type UserRegistrationType = 'candidate' | 'employer';
@ -90,6 +85,7 @@ interface PasswordRequirement {
const apiClient = new ApiClient(); const apiClient = new ApiClient();
const LoginPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => { const LoginPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
const navigate = useNavigate();
const { setSnack } = props; const { setSnack } = props;
const [tabValue, setTabValue] = useState(0); const [tabValue, setTabValue] = useState(0);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@ -99,6 +95,9 @@ 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);
const showGuest: boolean = false;
// Password visibility states // Password visibility states
const [showLoginPassword, setShowLoginPassword] = useState(false); const [showLoginPassword, setShowLoginPassword] = useState(false);
@ -169,6 +168,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 +193,8 @@ 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);
navigate('/chat');
} }
}; };
@ -239,8 +257,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) => {
@ -356,11 +374,9 @@ const LoginPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
return ( return (
<Container maxWidth="sm" sx={{ mt: 4 }}> <Container maxWidth="sm" sx={{ mt: 4 }}>
<Paper elevation={3} sx={{ p: 4 }}> <Paper elevation={3} sx={{ p: 4 }}>
<Typography variant="h4" component="h1" gutterBottom align="center" color="primary"> <BackstoryLogo />
Backstory
</Typography>
{guest && ( {showGuest && guest && (
<Card sx={{ mb: 3, bgcolor: 'grey.50' }} elevation={1}> <Card sx={{ mb: 3, bgcolor: 'grey.50' }} elevation={1}>
<CardContent> <CardContent>
<Typography variant="h6" gutterBottom color="primary"> <Typography variant="h6" gutterBottom color="primary">
@ -383,9 +399,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"