Compare commits
No commits in common. "4a80004363cd02d9742809b1c5a1eb21a6ee05b0" and "a03497a552d0a8189398bfb915b065fd6a169e84" have entirely different histories.
4a80004363
...
a03497a552
@ -29,11 +29,10 @@ 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/ui/Beta';
|
import { Beta } from 'components/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';
|
||||||
@ -190,6 +189,32 @@ 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
|
||||||
|
@ -1,38 +0,0 @@
|
|||||||
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 };
|
|
@ -1,30 +0,0 @@
|
|||||||
.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;
|
|
||||||
}
|
|
@ -1,25 +0,0 @@
|
|||||||
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
|
|
||||||
};
|
|
@ -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/ui/Beta';
|
import { Beta } from '../components/Beta';
|
||||||
|
|
||||||
interface BetaPageProps {
|
interface BetaPageProps {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
@ -19,7 +19,6 @@ 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 = () => {
|
||||||
@ -301,9 +300,8 @@ const HomePage = () => {
|
|||||||
>
|
>
|
||||||
Create Your Profile
|
Create Your Profile
|
||||||
</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
|
||||||
@ -382,16 +380,15 @@ const HomePage = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<ActionButton
|
<ActionButton
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
sx={{ mt: 4 }}
|
sx={{ mt: 4 }}
|
||||||
endIcon={<ArrowForwardIcon />}
|
endIcon={<ArrowForwardIcon />}
|
||||||
>
|
>
|
||||||
Start Recruiting
|
Start Recruiting
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</Box>
|
</Box>
|
||||||
</ComingSoon>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Container>
|
</Container>
|
||||||
|
|
||||||
|
@ -11,6 +11,8 @@ import {
|
|||||||
CircularProgress,
|
CircularProgress,
|
||||||
Tabs,
|
Tabs,
|
||||||
Tab,
|
Tab,
|
||||||
|
AppBar,
|
||||||
|
Toolbar,
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
Divider,
|
Divider,
|
||||||
@ -33,12 +35,15 @@ 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';
|
||||||
@ -50,10 +55,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';
|
||||||
|
|
||||||
@ -85,7 +90,6 @@ 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);
|
||||||
@ -95,9 +99,6 @@ 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);
|
||||||
@ -168,23 +169,6 @@ 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);
|
||||||
@ -193,8 +177,6 @@ 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');
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -257,8 +239,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) => {
|
||||||
@ -374,9 +356,11 @@ 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 }}>
|
||||||
<BackstoryLogo />
|
<Typography variant="h4" component="h1" gutterBottom align="center" color="primary">
|
||||||
|
Backstory
|
||||||
|
</Typography>
|
||||||
|
|
||||||
{showGuest && guest && (
|
{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">
|
||||||
@ -399,9 +383,9 @@ const LoginPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
|
|||||||
</Tabs>
|
</Tabs>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{errorMessage && (
|
{error && (
|
||||||
<Alert severity="error" sx={{ mb: 2 }}>
|
<Alert severity="error" sx={{ mb: 2 }}>
|
||||||
{errorMessage}
|
{error}
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@ -4,11 +4,10 @@ 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, timedelta
|
from datetime import datetime, timezone
|
||||||
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
|
||||||
|
|
||||||
@ -157,14 +156,8 @@ 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):
|
||||||
time_until_unlock = auth_data.locked_until - datetime.now(timezone.utc)
|
logger.warning(f"🔒 Account locked for user {login}")
|
||||||
# Convert time_until_unlock to minutes:seconds format
|
return False, None, "Account is temporarily locked due to too many failed attempts"
|
||||||
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):
|
||||||
@ -173,6 +166,7 @@ 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
|
||||||
)
|
)
|
||||||
@ -194,7 +188,6 @@ 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"
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user