Compare commits

...

2 Commits

10 changed files with 143 additions and 49 deletions

View File

@ -29,10 +29,11 @@ import {
} from '@mui/icons-material';
import { NavigationLinkType } from 'components/layout/BackstoryLayout';
import { Beta } from 'components/Beta';
import { Beta } from 'components/ui/Beta';
import { Candidate, Employer } from 'types/types';
import { SetSnackType } from 'components/Snack';
import { CopyBubble } from 'components/CopyBubble';
import { BackstoryLogo } from 'components/ui/BackstoryLogo';
import 'components/layout/Header.css';
import { useAuth } from 'hooks/AuthContext';
@ -189,32 +190,6 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
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[] = [
{name: "Home", path: "/", label: <BackstoryLogo/>},
...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 ConstructionIcon from '@mui/icons-material/Construction';
import RocketLaunchIcon from '@mui/icons-material/RocketLaunch';
import { Beta } from '../components/Beta';
import { Beta } from '../components/ui/Beta';
interface BetaPageProps {
children?: React.ReactNode;

View File

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

View File

@ -11,8 +11,6 @@ import {
CircularProgress,
Tabs,
Tab,
AppBar,
Toolbar,
Card,
CardContent,
Divider,
@ -35,15 +33,12 @@ import {
Person,
PersonAdd,
AccountCircle,
ExitToApp,
Visibility,
VisibilityOff,
CheckCircle,
Cancel,
ExpandLess,
ExpandMore,
Visibility as ViewIcon,
Work,
Business
} from '@mui/icons-material';
import 'react-phone-number-input/style.css';
@ -55,10 +50,10 @@ import { ApiClient } from 'services/api-client';
import { useAuth } from 'hooks/AuthContext';
import { LocationInput } from 'components/LocationInput';
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 { Navigate, useNavigate } from 'react-router-dom';
type UserRegistrationType = 'candidate' | 'employer';
@ -90,6 +85,7 @@ interface PasswordRequirement {
const apiClient = new ApiClient();
const LoginPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
const navigate = useNavigate();
const { setSnack } = props;
const [tabValue, setTabValue] = useState(0);
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 name = (user?.userType === 'candidate') ? user.username : user?.email || '';
const [location, setLocation] = useState<Partial<Location>>({});
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const showGuest: boolean = false;
// Password visibility states
const [showLoginPassword, setShowLoginPassword] = useState(false);
@ -169,6 +168,23 @@ const LoginPage: React.FC<BackstoryPageProps> = (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 +193,8 @@ const LoginPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
const success = await login(loginForm);
if (success) {
setSuccess('Login successful!');
setLoading(false);
navigate('/chat');
}
};
@ -239,8 +257,8 @@ const LoginPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
if (registerForm.userType === 'candidate') {
window.location.href = '/candidate/dashboard';
}
}
setLoading(false);
}
};
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
@ -356,11 +374,9 @@ const LoginPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
return (
<Container maxWidth="sm" sx={{ mt: 4 }}>
<Paper elevation={3} sx={{ p: 4 }}>
<Typography variant="h4" component="h1" gutterBottom align="center" color="primary">
Backstory
</Typography>
<BackstoryLogo />
{guest && (
{showGuest && guest && (
<Card sx={{ mb: 3, bgcolor: 'grey.50' }} elevation={1}>
<CardContent>
<Typography variant="h6" gutterBottom color="primary">
@ -383,9 +399,9 @@ const LoginPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
</Tabs>
</Box>
{error && (
{errorMessage && (
<Alert severity="error" sx={{ mb: 2 }}>
{error}
{errorMessage}
</Alert>
)}

View File

@ -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"