MFA is working
This commit is contained in:
parent
d7a81481a2
commit
4919da84d6
@ -19,6 +19,7 @@ import { GenerateCandidate } from "pages/GenerateCandidate";
|
|||||||
import { ControlsPage } from 'pages/ControlsPage';
|
import { ControlsPage } from 'pages/ControlsPage';
|
||||||
import { LoginPage } from "pages/LoginPage";
|
import { LoginPage } from "pages/LoginPage";
|
||||||
import { CandidateDashboardPage } from "pages/CandidateDashboardPage"
|
import { CandidateDashboardPage } from "pages/CandidateDashboardPage"
|
||||||
|
import { EmailVerificationPage } from "components/EmailVerificationComponents";
|
||||||
|
|
||||||
const ProfilePage = () => (<BetaPage><Typography variant="h4">Profile</Typography></BetaPage>);
|
const ProfilePage = () => (<BetaPage><Typography variant="h4">Profile</Typography></BetaPage>);
|
||||||
const BackstoryPage = () => (<BetaPage><Typography variant="h4">Backstory</Typography></BetaPage>);
|
const BackstoryPage = () => (<BetaPage><Typography variant="h4">Backstory</Typography></BetaPage>);
|
||||||
@ -58,9 +59,11 @@ const getBackstoryDynamicRoutes = (props: BackstoryDynamicRoutesProps): ReactNod
|
|||||||
if (!user) {
|
if (!user) {
|
||||||
routes.push(<Route key={`${index++}`} path="/register" element={(<BetaPage><CreateProfilePage /></BetaPage>)} />);
|
routes.push(<Route key={`${index++}`} path="/register" element={(<BetaPage><CreateProfilePage /></BetaPage>)} />);
|
||||||
routes.push(<Route key={`${index++}`} path="/login" element={<LoginPage {...backstoryProps} />} />);
|
routes.push(<Route key={`${index++}`} path="/login" element={<LoginPage {...backstoryProps} />} />);
|
||||||
|
routes.push(<Route key={`${index++}`} path="/login/verify-email" element={<EmailVerificationPage {...backstoryProps} />} />);
|
||||||
routes.push(<Route key={`${index++}`} path="*" element={<BetaPage />} />);
|
routes.push(<Route key={`${index++}`} path="*" element={<BetaPage />} />);
|
||||||
} else {
|
} else {
|
||||||
routes.push(<Route key={`${index++}`} path="/login" element={<LoginPage {...backstoryProps} />} />);
|
routes.push(<Route key={`${index++}`} path="/login" element={<LoginPage {...backstoryProps} />} />);
|
||||||
|
routes.push(<Route key={`${index++}`} path="/login/verify-email" element={<EmailVerificationPage {...backstoryProps} />} />);
|
||||||
routes.push(<Route key={`${index++}`} path="/logout" element={<LogoutPage />} />);
|
routes.push(<Route key={`${index++}`} path="/logout" element={<LogoutPage />} />);
|
||||||
|
|
||||||
if (user.userType === 'candidate') {
|
if (user.userType === 'candidate') {
|
||||||
|
@ -38,7 +38,7 @@ const BetaPage: React.FC<BetaPageProps> = ({
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
if (!children) {
|
if (!children) {
|
||||||
children = (<Box sx={{ width: "100%", display: "flex", justifyContent: "center" }}>The page you requested (<b>{location.pathname.replace(/^\//, '')}</b>) is not yet read.</Box>);
|
children = (<Box sx={{ width: "100%", display: "flex", justifyContent: "center" }}>The page you requested (<b>{location.pathname.replace(/^\//, '')}</b>) is not yet ready.</Box>);
|
||||||
}
|
}
|
||||||
console.log("BetaPage", children);
|
console.log("BetaPage", children);
|
||||||
|
|
||||||
|
@ -3,174 +3,43 @@ import {
|
|||||||
Box,
|
Box,
|
||||||
Container,
|
Container,
|
||||||
Paper,
|
Paper,
|
||||||
TextField,
|
|
||||||
Button,
|
|
||||||
Typography,
|
Typography,
|
||||||
Grid,
|
Grid,
|
||||||
Alert,
|
Alert,
|
||||||
CircularProgress,
|
|
||||||
Tabs,
|
Tabs,
|
||||||
Tab,
|
Tab,
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
Divider,
|
Divider,
|
||||||
Avatar,
|
Avatar,
|
||||||
IconButton,
|
|
||||||
InputAdornment,
|
|
||||||
List,
|
|
||||||
ListItem,
|
|
||||||
ListItemIcon,
|
|
||||||
ListItemText,
|
|
||||||
Collapse,
|
|
||||||
FormControl,
|
|
||||||
FormLabel,
|
|
||||||
RadioGroup,
|
|
||||||
FormControlLabel,
|
|
||||||
Radio,
|
|
||||||
Chip
|
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import {
|
import {
|
||||||
Person,
|
Person,
|
||||||
PersonAdd,
|
PersonAdd,
|
||||||
AccountCircle,
|
AccountCircle,
|
||||||
Visibility,
|
|
||||||
VisibilityOff,
|
|
||||||
CheckCircle,
|
|
||||||
Cancel,
|
|
||||||
ExpandLess,
|
|
||||||
ExpandMore,
|
|
||||||
Business
|
|
||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
import 'react-phone-number-input/style.css';
|
import 'react-phone-number-input/style.css';
|
||||||
import PhoneInput from 'react-phone-number-input';
|
|
||||||
import { E164Number } from 'libphonenumber-js/core';
|
|
||||||
import './LoginPage.css';
|
import './LoginPage.css';
|
||||||
|
|
||||||
import { ApiClient } from 'services/api-client';
|
|
||||||
import { useAuth } from 'hooks/AuthContext';
|
import { useAuth } from 'hooks/AuthContext';
|
||||||
import { LocationInput } from 'components/LocationInput';
|
|
||||||
import { Location } from 'types/types';
|
|
||||||
import { BackstoryLogo } from 'components/ui/BackstoryLogo';
|
import { BackstoryLogo } from 'components/ui/BackstoryLogo';
|
||||||
|
|
||||||
import { BackstoryPageProps } from 'components/BackstoryTab';
|
import { BackstoryPageProps } from 'components/BackstoryTab';
|
||||||
import { Navigate, useNavigate } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { LoginForm } from "components/EmailVerificationComponents";
|
import { LoginForm } from "components/EmailVerificationComponents";
|
||||||
import { CandidateRegistrationForm, EmployerRegistrationForm } from "components/RegistrationForms";
|
import { CandidateRegistrationForm } from "components/RegistrationForms";
|
||||||
|
|
||||||
type UserRegistrationType = 'candidate' | 'employer';
|
|
||||||
|
|
||||||
interface LoginRequest {
|
|
||||||
login: string;
|
|
||||||
password: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RegisterRequest {
|
|
||||||
userType: UserRegistrationType;
|
|
||||||
username: string;
|
|
||||||
email: string;
|
|
||||||
firstName: string;
|
|
||||||
lastName: string;
|
|
||||||
password: string;
|
|
||||||
confirmPassword: string;
|
|
||||||
phone?: string;
|
|
||||||
// Employer specific fields (placeholder)
|
|
||||||
companyName?: string;
|
|
||||||
industry?: string;
|
|
||||||
companySize?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PasswordRequirement {
|
|
||||||
label: string;
|
|
||||||
met: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
||||||
const [success, setSuccess] = useState<string | null>(null);
|
const [success, setSuccess] = useState<string | null>(null);
|
||||||
const [phone, setPhone] = useState<E164Number | null>(null);
|
const { guest, user, login, isLoading, error } = useAuth();
|
||||||
const { createCandidateAccount, guest, user, login, isLoading, error } = useAuth();
|
|
||||||
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 [errorMessage, setErrorMessage] = useState<string | null>(null);
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||||
|
|
||||||
const showGuest: boolean = false;
|
const showGuest: boolean = false;
|
||||||
|
|
||||||
// Password visibility states
|
|
||||||
const [showLoginPassword, setShowLoginPassword] = useState(false);
|
|
||||||
const [showRegisterPassword, setShowRegisterPassword] = useState(false);
|
|
||||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
|
||||||
const [showPasswordRequirements, setShowPasswordRequirements] = useState(false);
|
|
||||||
|
|
||||||
const handleLocationChange = (location: Partial<Location>) => {
|
|
||||||
setLocation(location);
|
|
||||||
console.log('Location updated:', location);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Login form state
|
|
||||||
const [loginForm, setLoginForm] = useState<LoginRequest>({
|
|
||||||
login: '',
|
|
||||||
password: ''
|
|
||||||
});
|
|
||||||
|
|
||||||
// Register form state
|
|
||||||
const [registerForm, setRegisterForm] = useState<RegisterRequest>({
|
|
||||||
userType: 'candidate',
|
|
||||||
username: '',
|
|
||||||
email: '',
|
|
||||||
firstName: '',
|
|
||||||
lastName: '',
|
|
||||||
password: '',
|
|
||||||
confirmPassword: '',
|
|
||||||
phone: '',
|
|
||||||
companyName: '',
|
|
||||||
industry: '',
|
|
||||||
companySize: ''
|
|
||||||
});
|
|
||||||
|
|
||||||
// Password requirements validation
|
|
||||||
const getPasswordRequirements = (password: string): PasswordRequirement[] => {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
label: 'At least 8 characters long',
|
|
||||||
met: password.length >= 8
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Contains uppercase letter',
|
|
||||||
met: /[A-Z]/.test(password)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Contains lowercase letter',
|
|
||||||
met: /[a-z]/.test(password)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Contains number',
|
|
||||||
met: /\d/.test(password)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Contains special character (!@#$%^&*)',
|
|
||||||
met: /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password)
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
const passwordRequirements = getPasswordRequirements(registerForm.password);
|
|
||||||
const passwordsMatch = registerForm.password === registerForm.confirmPassword;
|
|
||||||
const hasPasswordMatchError = registerForm.confirmPassword.length > 0 && !passwordsMatch;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (phone !== registerForm.phone && phone) {
|
|
||||||
console.log({ phone });
|
|
||||||
setRegisterForm({ ...registerForm, phone });
|
|
||||||
}
|
|
||||||
}, [phone, registerForm]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!loading || !error) {
|
if (!loading || !error) {
|
||||||
return;
|
return;
|
||||||
@ -188,110 +57,11 @@ const LoginPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
|
|||||||
}
|
}
|
||||||
}, [error, loading]);
|
}, [error, loading]);
|
||||||
|
|
||||||
const handleLogin = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setLoading(true);
|
|
||||||
setSuccess(null);
|
|
||||||
|
|
||||||
const success = await login(loginForm);
|
|
||||||
if (success) {
|
|
||||||
setSuccess('Login successful!');
|
|
||||||
setLoading(false);
|
|
||||||
navigate('/chat');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePasswordChange = (password: string) => {
|
|
||||||
setRegisterForm(prev => ({ ...prev, password }));
|
|
||||||
setPasswordValidation(apiClient.validatePasswordStrength(password));
|
|
||||||
|
|
||||||
// Show requirements if password has content and isn't valid
|
|
||||||
if (password.length > 0) {
|
|
||||||
const requirements = getPasswordRequirements(password);
|
|
||||||
const allMet = requirements.every(req => req.met);
|
|
||||||
if (!allMet && !showPasswordRequirements) {
|
|
||||||
setShowPasswordRequirements(true);
|
|
||||||
}
|
|
||||||
if (allMet && showPasswordRequirements) {
|
|
||||||
setShowPasswordRequirements(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUserTypeChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const userType = event.target.value as UserRegistrationType;
|
|
||||||
setRegisterForm(prev => ({ ...prev, userType }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRegister = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
// Check if employer registration is attempted
|
|
||||||
if (registerForm.userType === 'employer') {
|
|
||||||
setSnack('Employer registration is not yet supported. Please contact support for employer account setup.', "warning");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate passwords match
|
|
||||||
if (!passwordsMatch) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate password requirements
|
|
||||||
const allRequirementsMet = passwordRequirements.every(req => req.met);
|
|
||||||
if (!allRequirementsMet) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(true);
|
|
||||||
setSuccess(null);
|
|
||||||
|
|
||||||
// For now, all non-employer registrations go through candidate creation
|
|
||||||
// This would need to be updated when employer APIs are available
|
|
||||||
let success;
|
|
||||||
switch (registerForm.userType) {
|
|
||||||
case 'candidate':
|
|
||||||
success = await createCandidateAccount(registerForm);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
// Redirect based on user type
|
|
||||||
if (registerForm.userType === 'candidate') {
|
|
||||||
window.location.href = '/candidate/dashboard';
|
|
||||||
}
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
|
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
|
||||||
setTabValue(newValue);
|
setTabValue(newValue);
|
||||||
setSuccess(null);
|
setSuccess(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Toggle password visibility functions
|
|
||||||
const toggleLoginPasswordVisibility = () => setShowLoginPassword(!showLoginPassword);
|
|
||||||
const toggleRegisterPasswordVisibility = () => setShowRegisterPassword(!showRegisterPassword);
|
|
||||||
const toggleConfirmPasswordVisibility = () => setShowConfirmPassword(!showConfirmPassword);
|
|
||||||
|
|
||||||
// Get user type icon and description
|
|
||||||
const getUserTypeInfo = (userType: UserRegistrationType) => {
|
|
||||||
switch (userType) {
|
|
||||||
case 'candidate':
|
|
||||||
return {
|
|
||||||
icon: <Person />,
|
|
||||||
title: 'Candidate',
|
|
||||||
description: 'Use Backstory to generate your resume and help you manage your career. Optionally let people interact with your profile.'
|
|
||||||
};
|
|
||||||
case 'employer':
|
|
||||||
return {
|
|
||||||
icon: <Business />,
|
|
||||||
title: 'Employer',
|
|
||||||
description: 'Post jobs and find talent (Coming Soon.)'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// If user is logged in, show their profile
|
// If user is logged in, show their profile
|
||||||
if (user) {
|
if (user) {
|
||||||
return (
|
return (
|
||||||
@ -356,24 +126,6 @@ const LoginPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const validateInput = (value: string) => {
|
|
||||||
if (!value) return 'This field is required';
|
|
||||||
|
|
||||||
// Username: alphanumeric, 3-20 characters, no @
|
|
||||||
const usernameRegex = /^[a-zA-Z0-9]{3,20}$/;
|
|
||||||
// Email: basic email format
|
|
||||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
||||||
|
|
||||||
if (usernameRegex.test(value)) return '';
|
|
||||||
if (emailRegex.test(value)) return '';
|
|
||||||
return 'Enter a valid username (3-20 alphanumeric characters) or email';
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLoginChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const { value } = event.target;
|
|
||||||
setLoginForm({ ...loginForm, login: value });
|
|
||||||
};
|
|
||||||
|
|
||||||
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 }}>
|
||||||
@ -416,328 +168,10 @@ const LoginPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
|
|||||||
|
|
||||||
{tabValue === 0 && (
|
{tabValue === 0 && (
|
||||||
<LoginForm />
|
<LoginForm />
|
||||||
// <Box component="form" onSubmit={handleLogin}>
|
|
||||||
// <Typography variant="h5" gutterBottom>
|
|
||||||
// Sign In
|
|
||||||
// </Typography>
|
|
||||||
|
|
||||||
// <TextField
|
|
||||||
// fullWidth
|
|
||||||
// label="Username or Email"
|
|
||||||
// type="text"
|
|
||||||
// value={loginForm.login}
|
|
||||||
// onChange={handleLoginChange}
|
|
||||||
// margin="normal"
|
|
||||||
// required
|
|
||||||
// disabled={loading}
|
|
||||||
// variant="outlined"
|
|
||||||
// placeholder="Enter username or email"
|
|
||||||
// />
|
|
||||||
|
|
||||||
// <TextField
|
|
||||||
// fullWidth
|
|
||||||
// label="Password"
|
|
||||||
// type={showLoginPassword ? 'text' : 'password'}
|
|
||||||
// value={loginForm.password}
|
|
||||||
// onChange={(e) => setLoginForm({ ...loginForm, password: e.target.value })}
|
|
||||||
// margin="normal"
|
|
||||||
// required
|
|
||||||
// disabled={loading}
|
|
||||||
// variant="outlined"
|
|
||||||
// autoComplete='current-password'
|
|
||||||
// slotProps={{
|
|
||||||
// input: {
|
|
||||||
// endAdornment: (
|
|
||||||
// <InputAdornment position="end">
|
|
||||||
// <IconButton
|
|
||||||
// aria-label="toggle password visibility"
|
|
||||||
// onClick={toggleLoginPasswordVisibility}
|
|
||||||
// edge="end"
|
|
||||||
// disabled={loading}
|
|
||||||
// >
|
|
||||||
// {showLoginPassword ? <VisibilityOff /> : <Visibility />}
|
|
||||||
// </IconButton>
|
|
||||||
// </InputAdornment>
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
// }}
|
|
||||||
// />
|
|
||||||
|
|
||||||
// <Button
|
|
||||||
// type="submit"
|
|
||||||
// fullWidth
|
|
||||||
// variant="contained"
|
|
||||||
// sx={{ mt: 3, mb: 2 }}
|
|
||||||
// disabled={loading}
|
|
||||||
// startIcon={loading ? <CircularProgress size={20} color="inherit" /> : <Person />}
|
|
||||||
// >
|
|
||||||
// {loading ? 'Signing In...' : 'Sign In'}
|
|
||||||
// </Button>
|
|
||||||
// </Box>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{tabValue === 1 && (
|
{tabValue === 1 && (
|
||||||
<CandidateRegistrationForm />
|
<CandidateRegistrationForm />
|
||||||
// <Box component="form" onSubmit={handleRegister}>
|
|
||||||
// <Typography variant="h5" gutterBottom>
|
|
||||||
// Create Account
|
|
||||||
// </Typography>
|
|
||||||
|
|
||||||
// {/* User Type Selection */}
|
|
||||||
// <FormControl component="fieldset" sx={{ mb: 3, width: '100%' }}>
|
|
||||||
// <FormLabel component="legend" sx={{ mb: 2 }}>
|
|
||||||
// <Typography variant="h6">Select Account Type</Typography>
|
|
||||||
// </FormLabel>
|
|
||||||
// <RadioGroup
|
|
||||||
// value={registerForm.userType}
|
|
||||||
// onChange={handleUserTypeChange}
|
|
||||||
// sx={{ gap: 1 }}
|
|
||||||
// >
|
|
||||||
// {(['candidate', 'employer'] as UserRegistrationType[]).map((userType) => {
|
|
||||||
// const info = getUserTypeInfo(userType);
|
|
||||||
// return (
|
|
||||||
// <FormControlLabel
|
|
||||||
// key={userType}
|
|
||||||
// value={userType}
|
|
||||||
// disabled={loading || userType === 'employer'}
|
|
||||||
// control={<Radio />}
|
|
||||||
// label={
|
|
||||||
// <Box sx={{ display: 'flex', alignItems: 'center', gap: 1, py: 0.5 }}>
|
|
||||||
// {info.icon}
|
|
||||||
// <Box>
|
|
||||||
// <Typography variant="subtitle1" sx={{ fontWeight: 'medium' }}>
|
|
||||||
// {info.title}
|
|
||||||
// {userType === 'employer' && (
|
|
||||||
// <Chip
|
|
||||||
// label="Coming Soon"
|
|
||||||
// size="small"
|
|
||||||
// color="warning"
|
|
||||||
// sx={{ ml: 1 }}
|
|
||||||
// />
|
|
||||||
// )}
|
|
||||||
// </Typography>
|
|
||||||
// <Typography variant="body2" color="text.secondary">
|
|
||||||
// {info.description}
|
|
||||||
// </Typography>
|
|
||||||
// </Box>
|
|
||||||
// </Box>
|
|
||||||
// }
|
|
||||||
// sx={{
|
|
||||||
// border: '1px solid',
|
|
||||||
// borderColor: registerForm.userType === userType ? 'primary.main' : 'divider',
|
|
||||||
// borderRadius: 1,
|
|
||||||
// p: 1,
|
|
||||||
// m: 0,
|
|
||||||
// bgcolor: registerForm.userType === userType ? 'primary.50' : 'transparent',
|
|
||||||
// '&:hover': {
|
|
||||||
// bgcolor: userType === 'employer' ? 'grey.100' : 'action.hover'
|
|
||||||
// },
|
|
||||||
// opacity: userType === 'employer' ? 0.6 : 1
|
|
||||||
// }}
|
|
||||||
// />
|
|
||||||
// );
|
|
||||||
// })}
|
|
||||||
// </RadioGroup>
|
|
||||||
// </FormControl>
|
|
||||||
|
|
||||||
// {/* Employer Placeholder */}
|
|
||||||
// {registerForm.userType === 'employer' && (
|
|
||||||
// <Alert severity="info" sx={{ mb: 3 }}>
|
|
||||||
// <Typography variant="h6" gutterBottom>
|
|
||||||
// Employer Registration Coming Soon
|
|
||||||
// </Typography>
|
|
||||||
// <Typography variant="body2">
|
|
||||||
// We're currently building our employer features. If you're interested in posting jobs
|
|
||||||
// and finding talent, please contact our support team at support@backstory.com for
|
|
||||||
// early access.
|
|
||||||
// </Typography>
|
|
||||||
// </Alert>
|
|
||||||
// )}
|
|
||||||
|
|
||||||
// {/* Basic Information Fields */}
|
|
||||||
// {registerForm.userType !== 'employer' && (
|
|
||||||
// <>
|
|
||||||
// <Grid container spacing={2} sx={{ mb: 2 }}>
|
|
||||||
// <Grid size={{ xs: 12, sm: 6 }}>
|
|
||||||
// <TextField
|
|
||||||
// fullWidth
|
|
||||||
// label="First Name"
|
|
||||||
// value={registerForm.firstName}
|
|
||||||
// onChange={(e) => setRegisterForm({ ...registerForm, firstName: e.target.value })}
|
|
||||||
// required
|
|
||||||
// disabled={loading}
|
|
||||||
// variant="outlined"
|
|
||||||
// />
|
|
||||||
// </Grid>
|
|
||||||
|
|
||||||
// <Grid size={{ xs: 12, sm: 6 }}>
|
|
||||||
// <TextField
|
|
||||||
// fullWidth
|
|
||||||
// label="Last Name"
|
|
||||||
// value={registerForm.lastName}
|
|
||||||
// onChange={(e) => setRegisterForm({ ...registerForm, lastName: e.target.value })}
|
|
||||||
// required
|
|
||||||
// disabled={loading}
|
|
||||||
// variant="outlined"
|
|
||||||
// />
|
|
||||||
// </Grid>
|
|
||||||
// </Grid>
|
|
||||||
|
|
||||||
// <TextField
|
|
||||||
// fullWidth
|
|
||||||
// label="Username"
|
|
||||||
// value={registerForm.username}
|
|
||||||
// onChange={(e) => setRegisterForm({ ...registerForm, username: e.target.value })}
|
|
||||||
// margin="normal"
|
|
||||||
// required
|
|
||||||
// disabled={loading}
|
|
||||||
// variant="outlined"
|
|
||||||
// />
|
|
||||||
|
|
||||||
// <TextField
|
|
||||||
// fullWidth
|
|
||||||
// label="Email"
|
|
||||||
// type="email"
|
|
||||||
// value={registerForm.email}
|
|
||||||
// onChange={(e) => setRegisterForm({ ...registerForm, email: e.target.value })}
|
|
||||||
// margin="normal"
|
|
||||||
// required
|
|
||||||
// disabled={loading}
|
|
||||||
// variant="outlined"
|
|
||||||
// />
|
|
||||||
|
|
||||||
// {/* Conditional fields based on user type */}
|
|
||||||
// {registerForm.userType === 'candidate' && (
|
|
||||||
// <>
|
|
||||||
// <PhoneInput
|
|
||||||
// label="Phone (Optional)"
|
|
||||||
// placeholder="Enter phone number"
|
|
||||||
// defaultCountry='US'
|
|
||||||
// value={registerForm.phone}
|
|
||||||
// disabled={loading}
|
|
||||||
// onChange={(v) => setPhone(v as E164Number)}
|
|
||||||
// />
|
|
||||||
|
|
||||||
// <LocationInput
|
|
||||||
// value={location}
|
|
||||||
// onChange={handleLocationChange}
|
|
||||||
// showCity
|
|
||||||
// helperText="Include your city for more specific job matches"
|
|
||||||
// />
|
|
||||||
// </>
|
|
||||||
// )}
|
|
||||||
|
|
||||||
// <TextField
|
|
||||||
// fullWidth
|
|
||||||
// label="Password"
|
|
||||||
// type={showRegisterPassword ? 'text' : 'password'}
|
|
||||||
// value={registerForm.password}
|
|
||||||
// onChange={(e) => handlePasswordChange(e.target.value)}
|
|
||||||
// margin="normal"
|
|
||||||
// required
|
|
||||||
// disabled={loading}
|
|
||||||
// variant="outlined"
|
|
||||||
// slotProps={{
|
|
||||||
// input: {
|
|
||||||
// endAdornment: (
|
|
||||||
// <InputAdornment position="end">
|
|
||||||
// <IconButton
|
|
||||||
// aria-label="toggle password visibility"
|
|
||||||
// onClick={toggleRegisterPasswordVisibility}
|
|
||||||
// edge="end"
|
|
||||||
// disabled={loading}
|
|
||||||
// >
|
|
||||||
// {showRegisterPassword ? <VisibilityOff /> : <Visibility />}
|
|
||||||
// </IconButton>
|
|
||||||
// </InputAdornment>
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
// }}
|
|
||||||
// />
|
|
||||||
|
|
||||||
// {/* Password Requirements */}
|
|
||||||
// {registerForm.password.length > 0 && (
|
|
||||||
// <Box sx={{ mt: 1, mb: 1 }}>
|
|
||||||
// <Button
|
|
||||||
// onClick={() => setShowPasswordRequirements(!showPasswordRequirements)}
|
|
||||||
// startIcon={showPasswordRequirements ? <ExpandLess /> : <ExpandMore />}
|
|
||||||
// size="small"
|
|
||||||
// sx={{ mb: 1 }}
|
|
||||||
// >
|
|
||||||
// Password Requirements
|
|
||||||
// </Button>
|
|
||||||
// <Collapse in={showPasswordRequirements}>
|
|
||||||
// <Paper variant="outlined" sx={{ p: 2 }}>
|
|
||||||
// <List dense>
|
|
||||||
// {passwordRequirements.map((requirement, index) => (
|
|
||||||
// <ListItem key={index} sx={{ py: 0.5 }}>
|
|
||||||
// <ListItemIcon sx={{ minWidth: 36 }}>
|
|
||||||
// {requirement.met ? (
|
|
||||||
// <CheckCircle color="success" fontSize="small" />
|
|
||||||
// ) : (
|
|
||||||
// <Cancel color="error" fontSize="small" />
|
|
||||||
// )}
|
|
||||||
// </ListItemIcon>
|
|
||||||
// <ListItemText
|
|
||||||
// primary={requirement.label}
|
|
||||||
// sx={{
|
|
||||||
// '& .MuiListItemText-primary': {
|
|
||||||
// fontSize: '0.875rem',
|
|
||||||
// color: requirement.met ? 'success.main' : 'error.main'
|
|
||||||
// }
|
|
||||||
// }}
|
|
||||||
// />
|
|
||||||
// </ListItem>
|
|
||||||
// ))}
|
|
||||||
// </List>
|
|
||||||
// </Paper>
|
|
||||||
// </Collapse>
|
|
||||||
// </Box>
|
|
||||||
// )}
|
|
||||||
|
|
||||||
// <TextField
|
|
||||||
// fullWidth
|
|
||||||
// label="Confirm Password"
|
|
||||||
// type={showConfirmPassword ? 'text' : 'password'}
|
|
||||||
// value={registerForm.confirmPassword}
|
|
||||||
// onChange={(e) => setRegisterForm({ ...registerForm, confirmPassword: e.target.value })}
|
|
||||||
// margin="normal"
|
|
||||||
// required
|
|
||||||
// disabled={loading}
|
|
||||||
// variant="outlined"
|
|
||||||
// error={hasPasswordMatchError}
|
|
||||||
// helperText={hasPasswordMatchError ? 'Passwords do not match' : ''}
|
|
||||||
// slotProps={{
|
|
||||||
// input: {
|
|
||||||
// endAdornment: (
|
|
||||||
// <InputAdornment position="end">
|
|
||||||
// <IconButton
|
|
||||||
// aria-label="toggle confirm password visibility"
|
|
||||||
// onClick={toggleConfirmPasswordVisibility}
|
|
||||||
// edge="end"
|
|
||||||
// disabled={loading}
|
|
||||||
// >
|
|
||||||
// {showConfirmPassword ? <VisibilityOff /> : <Visibility />}
|
|
||||||
// </IconButton>
|
|
||||||
// </InputAdornment>
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
// }}
|
|
||||||
// />
|
|
||||||
|
|
||||||
// <Button
|
|
||||||
// type="submit"
|
|
||||||
// fullWidth
|
|
||||||
// variant="contained"
|
|
||||||
// sx={{ mt: 3, mb: 2 }}
|
|
||||||
// disabled={loading || hasPasswordMatchError || !passwordRequirements.every(req => req.met)}
|
|
||||||
// startIcon={loading ? <CircularProgress size={20} color="inherit" /> : <PersonAdd />}
|
|
||||||
// >
|
|
||||||
// {loading ? 'Creating Account...' : `Create ${getUserTypeInfo(registerForm.userType).title} Account`}
|
|
||||||
// </Button>
|
|
||||||
// </>
|
|
||||||
// )}
|
|
||||||
// </Box>
|
|
||||||
)}
|
)}
|
||||||
</Paper>
|
</Paper>
|
||||||
</Container>
|
</Container>
|
||||||
|
@ -46,7 +46,7 @@ class EmailService:
|
|||||||
"""Send email verification email using template"""
|
"""Send email verification email using template"""
|
||||||
try:
|
try:
|
||||||
template = self._get_template("verification")
|
template = self._get_template("verification")
|
||||||
verification_link = f"{self.frontend_url}/verify-email?token={verification_token}"
|
verification_link = f"{self.frontend_url}/login/verify-email?token={verification_token}"
|
||||||
|
|
||||||
subject = self._format_template(
|
subject = self._format_template(
|
||||||
template["subject"],
|
template["subject"],
|
||||||
@ -110,7 +110,7 @@ class EmailService:
|
|||||||
"""Send password reset email using template"""
|
"""Send password reset email using template"""
|
||||||
try:
|
try:
|
||||||
template = self._get_template("password_reset")
|
template = self._get_template("password_reset")
|
||||||
reset_link = f"{self.frontend_url}/reset-password?token={reset_token}"
|
reset_link = f"{self.frontend_url}/login/reset-password?token={reset_token}"
|
||||||
|
|
||||||
subject = self._format_template(template["subject"])
|
subject = self._format_template(template["subject"])
|
||||||
|
|
||||||
|
@ -124,7 +124,7 @@ EMAIL_TEMPLATES = {
|
|||||||
<div class="footer">
|
<div class="footer">
|
||||||
<p><strong>This email was sent to:</strong> {to_email}</p>
|
<p><strong>This email was sent to:</strong> {to_email}</p>
|
||||||
<p>If you have any questions, please contact our support team.</p>
|
<p>If you have any questions, please contact our support team.</p>
|
||||||
<p>© 2024 {app_name}. All rights reserved.</p>
|
<p>© 2025 {app_name}. All rights reserved.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
@ -269,7 +269,7 @@ EMAIL_TEMPLATES = {
|
|||||||
<div class="footer">
|
<div class="footer">
|
||||||
<p><strong>This email was sent to:</strong> {to_email}</p>
|
<p><strong>This email was sent to:</strong> {to_email}</p>
|
||||||
<p>For security questions, please contact our support team immediately.</p>
|
<p>For security questions, please contact our support team immediately.</p>
|
||||||
<p>© 2024 {app_name}. All rights reserved.</p>
|
<p>© 2025 {app_name}. All rights reserved.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
@ -381,7 +381,7 @@ EMAIL_TEMPLATES = {
|
|||||||
<div class="footer">
|
<div class="footer">
|
||||||
<p><strong>This email was sent to:</strong> {to_email}</p>
|
<p><strong>This email was sent to:</strong> {to_email}</p>
|
||||||
<p>If you have any questions, please contact our support team.</p>
|
<p>If you have any questions, please contact our support team.</p>
|
||||||
<p>© 2024 {app_name}. All rights reserved.</p>
|
<p>© 2025 {app_name}. All rights reserved.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
@ -56,7 +56,7 @@ from device_manager import DeviceManager
|
|||||||
# =============================
|
# =============================
|
||||||
from models import (
|
from models import (
|
||||||
# API
|
# API
|
||||||
LoginRequest,
|
LoginRequest, CreateCandidateRequest, CreateEmployerRequest,
|
||||||
|
|
||||||
# User models
|
# User models
|
||||||
Candidate, Employer, BaseUserWithType, BaseUser, Guest, Authentication, AuthResponse,
|
Candidate, Employer, BaseUserWithType, BaseUser, Guest, Authentication, AuthResponse,
|
||||||
@ -160,7 +160,8 @@ ALGORITHM = "HS256"
|
|||||||
# ============================
|
# ============================
|
||||||
@app.exception_handler(RequestValidationError)
|
@app.exception_handler(RequestValidationError)
|
||||||
async def validation_exception_handler(request: Request, exc: RequestValidationError):
|
async def validation_exception_handler(request: Request, exc: RequestValidationError):
|
||||||
print("Validation error:", exc.errors())
|
logger.error(traceback.format_exc())
|
||||||
|
logger.error("❌ Validation error:", exc.errors())
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
status_code=HTTP_422_UNPROCESSABLE_ENTITY,
|
status_code=HTTP_422_UNPROCESSABLE_ENTITY,
|
||||||
content=json.dumps({"detail": exc.errors()}),
|
content=json.dumps({"detail": exc.errors()}),
|
||||||
@ -172,53 +173,6 @@ async def validation_exception_handler(request: Request, exc: RequestValidationE
|
|||||||
|
|
||||||
# Request/Response Models
|
# Request/Response Models
|
||||||
|
|
||||||
class CreateCandidateRequest(BaseModel):
|
|
||||||
email: EmailStr
|
|
||||||
username: str
|
|
||||||
password: str
|
|
||||||
firstName: str
|
|
||||||
lastName: str
|
|
||||||
# Add other required candidate fields as needed
|
|
||||||
phone: Optional[str] = None
|
|
||||||
|
|
||||||
@field_validator('username')
|
|
||||||
def validate_username(cls, v):
|
|
||||||
if not v or len(v.strip()) < 3:
|
|
||||||
raise ValueError('Username must be at least 3 characters long')
|
|
||||||
return v.strip().lower()
|
|
||||||
|
|
||||||
@field_validator('password')
|
|
||||||
def validate_password_strength(cls, v):
|
|
||||||
is_valid, issues = validate_password_strength(v)
|
|
||||||
if not is_valid:
|
|
||||||
raise ValueError('; '.join(issues))
|
|
||||||
return v
|
|
||||||
|
|
||||||
# Create Employer Endpoint (similar pattern)
|
|
||||||
class CreateEmployerRequest(BaseModel):
|
|
||||||
email: EmailStr
|
|
||||||
username: str
|
|
||||||
password: str
|
|
||||||
companyName: str
|
|
||||||
industry: str
|
|
||||||
companySize: str
|
|
||||||
companyDescription: str
|
|
||||||
# Add other required employer fields
|
|
||||||
websiteUrl: Optional[str] = None
|
|
||||||
phone: Optional[str] = None
|
|
||||||
|
|
||||||
@field_validator('username')
|
|
||||||
def validate_username(cls, v):
|
|
||||||
if not v or len(v.strip()) < 3:
|
|
||||||
raise ValueError('Username must be at least 3 characters long')
|
|
||||||
return v.strip().lower()
|
|
||||||
|
|
||||||
@field_validator('password')
|
|
||||||
def validate_password_strength(cls, v):
|
|
||||||
is_valid, issues = validate_password_strength(v)
|
|
||||||
if not is_valid:
|
|
||||||
raise ValueError('; '.join(issues))
|
|
||||||
return v
|
|
||||||
|
|
||||||
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
|
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
|
||||||
to_encode = data.copy()
|
to_encode = data.copy()
|
||||||
@ -613,9 +567,9 @@ async def create_candidate_with_verification(
|
|||||||
"userType": "candidate",
|
"userType": "candidate",
|
||||||
"email": request.email,
|
"email": request.email,
|
||||||
"username": request.username,
|
"username": request.username,
|
||||||
"firstName": request.firstName,
|
"firstName": request.first_name,
|
||||||
"lastName": request.lastName,
|
"lastName": request.last_name,
|
||||||
"fullName": f"{request.firstName} {request.lastName}",
|
"fullName": f"{request.first_name} {request.last_name}",
|
||||||
"phone": request.phone,
|
"phone": request.phone,
|
||||||
"createdAt": current_time.isoformat(),
|
"createdAt": current_time.isoformat(),
|
||||||
"updatedAt": current_time.isoformat(),
|
"updatedAt": current_time.isoformat(),
|
||||||
@ -642,7 +596,7 @@ async def create_candidate_with_verification(
|
|||||||
email_service.send_verification_email,
|
email_service.send_verification_email,
|
||||||
request.email,
|
request.email,
|
||||||
verification_token,
|
verification_token,
|
||||||
f"{request.firstName} {request.lastName}"
|
f"{request.first_name} {request.last_name}"
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(f"✅ Candidate registration initiated for: {request.email}")
|
logger.info(f"✅ Candidate registration initiated for: {request.email}")
|
||||||
|
@ -799,6 +799,60 @@ class UserPreference(BaseModel):
|
|||||||
# ============================
|
# ============================
|
||||||
# API Request/Response Models
|
# API Request/Response Models
|
||||||
# ============================
|
# ============================
|
||||||
|
class CreateCandidateRequest(BaseModel):
|
||||||
|
email: EmailStr
|
||||||
|
username: str
|
||||||
|
password: str
|
||||||
|
first_name: str = Field(..., alias="firstName")
|
||||||
|
last_name: str = Field(..., alias="lastName")
|
||||||
|
# Add other required candidate fields as needed
|
||||||
|
phone: Optional[str] = None
|
||||||
|
model_config = {
|
||||||
|
"populate_by_name": True # Allow both field names and aliases
|
||||||
|
}
|
||||||
|
|
||||||
|
@field_validator('username')
|
||||||
|
def validate_username(cls, v):
|
||||||
|
if not v or len(v.strip()) < 3:
|
||||||
|
raise ValueError('Username must be at least 3 characters long')
|
||||||
|
return v.strip().lower()
|
||||||
|
|
||||||
|
@field_validator('password')
|
||||||
|
def validate_password_strength(cls, v):
|
||||||
|
is_valid, issues = validate_password_strength(v)
|
||||||
|
if not is_valid:
|
||||||
|
raise ValueError('; '.join(issues))
|
||||||
|
return v
|
||||||
|
|
||||||
|
# Create Employer Endpoint (similar pattern)
|
||||||
|
class CreateEmployerRequest(BaseModel):
|
||||||
|
email: EmailStr
|
||||||
|
username: str
|
||||||
|
password: str
|
||||||
|
company_name: str = Field(..., alias="companyName")
|
||||||
|
industry: str
|
||||||
|
company_size: str = Field(..., alias="companySize")
|
||||||
|
company_description: str = Field(..., alias="companyDescription")
|
||||||
|
# Add other required employer fields
|
||||||
|
website_url: Optional[str] = Field(None, alias="websiteUrl")
|
||||||
|
phone: Optional[str] = None
|
||||||
|
model_config = {
|
||||||
|
"populate_by_name": True # Allow both field names and aliases
|
||||||
|
}
|
||||||
|
|
||||||
|
@field_validator('username')
|
||||||
|
def validate_username(cls, v):
|
||||||
|
if not v or len(v.strip()) < 3:
|
||||||
|
raise ValueError('Username must be at least 3 characters long')
|
||||||
|
return v.strip().lower()
|
||||||
|
|
||||||
|
@field_validator('password')
|
||||||
|
def validate_password_strength(cls, v):
|
||||||
|
is_valid, issues = validate_password_strength(v)
|
||||||
|
if not is_valid:
|
||||||
|
raise ValueError('; '.join(issues))
|
||||||
|
return v
|
||||||
|
|
||||||
class ChatQuery(BaseModel):
|
class ChatQuery(BaseModel):
|
||||||
prompt: str
|
prompt: str
|
||||||
tunables: Optional[Tunables] = None
|
tunables: Optional[Tunables] = None
|
||||||
|
Loading…
x
Reference in New Issue
Block a user