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 { LoginPage } from "pages/LoginPage";
|
||||
import { CandidateDashboardPage } from "pages/CandidateDashboardPage"
|
||||
import { EmailVerificationPage } from "components/EmailVerificationComponents";
|
||||
|
||||
const ProfilePage = () => (<BetaPage><Typography variant="h4">Profile</Typography></BetaPage>);
|
||||
const BackstoryPage = () => (<BetaPage><Typography variant="h4">Backstory</Typography></BetaPage>);
|
||||
@ -58,9 +59,11 @@ const getBackstoryDynamicRoutes = (props: BackstoryDynamicRoutesProps): ReactNod
|
||||
if (!user) {
|
||||
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/verify-email" element={<EmailVerificationPage {...backstoryProps} />} />);
|
||||
routes.push(<Route key={`${index++}`} path="*" element={<BetaPage />} />);
|
||||
} else {
|
||||
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 />} />);
|
||||
|
||||
if (user.userType === 'candidate') {
|
||||
|
@ -38,7 +38,7 @@ const BetaPage: React.FC<BetaPageProps> = ({
|
||||
const location = useLocation();
|
||||
|
||||
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);
|
||||
|
||||
|
@ -3,174 +3,43 @@ import {
|
||||
Box,
|
||||
Container,
|
||||
Paper,
|
||||
TextField,
|
||||
Button,
|
||||
Typography,
|
||||
Grid,
|
||||
Alert,
|
||||
CircularProgress,
|
||||
Tabs,
|
||||
Tab,
|
||||
Card,
|
||||
CardContent,
|
||||
Divider,
|
||||
Avatar,
|
||||
IconButton,
|
||||
InputAdornment,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Collapse,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
RadioGroup,
|
||||
FormControlLabel,
|
||||
Radio,
|
||||
Chip
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Person,
|
||||
PersonAdd,
|
||||
AccountCircle,
|
||||
Visibility,
|
||||
VisibilityOff,
|
||||
CheckCircle,
|
||||
Cancel,
|
||||
ExpandLess,
|
||||
ExpandMore,
|
||||
Business
|
||||
} from '@mui/icons-material';
|
||||
import 'react-phone-number-input/style.css';
|
||||
import PhoneInput from 'react-phone-number-input';
|
||||
import { E164Number } from 'libphonenumber-js/core';
|
||||
import './LoginPage.css';
|
||||
|
||||
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 { BackstoryPageProps } from 'components/BackstoryTab';
|
||||
import { Navigate, useNavigate } from 'react-router-dom';
|
||||
|
||||
import { LoginForm } from "components/EmailVerificationComponents";
|
||||
import { CandidateRegistrationForm, EmployerRegistrationForm } 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();
|
||||
import { CandidateRegistrationForm } from "components/RegistrationForms";
|
||||
|
||||
const LoginPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
|
||||
const navigate = useNavigate();
|
||||
const { setSnack } = props;
|
||||
const [tabValue, setTabValue] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [success, setSuccess] = useState<string | null>(null);
|
||||
const [phone, setPhone] = useState<E164Number | null>(null);
|
||||
const { createCandidateAccount, guest, user, login, isLoading, error } = useAuth();
|
||||
const [passwordValidation, setPasswordValidation] = useState<{ isValid: boolean; issues: string[] }>({ isValid: true, issues: [] });
|
||||
const { guest, user, login, isLoading, error } = useAuth();
|
||||
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);
|
||||
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(() => {
|
||||
if (!loading || !error) {
|
||||
return;
|
||||
@ -188,110 +57,11 @@ const LoginPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
|
||||
}
|
||||
}, [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) => {
|
||||
setTabValue(newValue);
|
||||
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) {
|
||||
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 (
|
||||
<Container maxWidth="sm" sx={{ mt: 4 }}>
|
||||
<Paper elevation={3} sx={{ p: 4 }}>
|
||||
@ -416,328 +168,10 @@ const LoginPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
|
||||
|
||||
{tabValue === 0 && (
|
||||
<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 && (
|
||||
<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>
|
||||
</Container>
|
||||
|
@ -46,7 +46,7 @@ class EmailService:
|
||||
"""Send email verification email using template"""
|
||||
try:
|
||||
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(
|
||||
template["subject"],
|
||||
@ -110,7 +110,7 @@ class EmailService:
|
||||
"""Send password reset email using template"""
|
||||
try:
|
||||
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"])
|
||||
|
||||
|
@ -124,7 +124,7 @@ EMAIL_TEMPLATES = {
|
||||
<div class="footer">
|
||||
<p><strong>This email was sent to:</strong> {to_email}</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>
|
||||
</body>
|
||||
@ -269,7 +269,7 @@ EMAIL_TEMPLATES = {
|
||||
<div class="footer">
|
||||
<p><strong>This email was sent to:</strong> {to_email}</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>
|
||||
</body>
|
||||
@ -381,7 +381,7 @@ EMAIL_TEMPLATES = {
|
||||
<div class="footer">
|
||||
<p><strong>This email was sent to:</strong> {to_email}</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>
|
||||
</body>
|
||||
|
@ -56,7 +56,7 @@ from device_manager import DeviceManager
|
||||
# =============================
|
||||
from models import (
|
||||
# API
|
||||
LoginRequest,
|
||||
LoginRequest, CreateCandidateRequest, CreateEmployerRequest,
|
||||
|
||||
# User models
|
||||
Candidate, Employer, BaseUserWithType, BaseUser, Guest, Authentication, AuthResponse,
|
||||
@ -160,7 +160,8 @@ ALGORITHM = "HS256"
|
||||
# ============================
|
||||
@app.exception_handler(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(
|
||||
status_code=HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
content=json.dumps({"detail": exc.errors()}),
|
||||
@ -172,53 +173,6 @@ async def validation_exception_handler(request: Request, exc: RequestValidationE
|
||||
|
||||
# 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):
|
||||
to_encode = data.copy()
|
||||
@ -613,9 +567,9 @@ async def create_candidate_with_verification(
|
||||
"userType": "candidate",
|
||||
"email": request.email,
|
||||
"username": request.username,
|
||||
"firstName": request.firstName,
|
||||
"lastName": request.lastName,
|
||||
"fullName": f"{request.firstName} {request.lastName}",
|
||||
"firstName": request.first_name,
|
||||
"lastName": request.last_name,
|
||||
"fullName": f"{request.first_name} {request.last_name}",
|
||||
"phone": request.phone,
|
||||
"createdAt": current_time.isoformat(),
|
||||
"updatedAt": current_time.isoformat(),
|
||||
@ -642,7 +596,7 @@ async def create_candidate_with_verification(
|
||||
email_service.send_verification_email,
|
||||
request.email,
|
||||
verification_token,
|
||||
f"{request.firstName} {request.lastName}"
|
||||
f"{request.first_name} {request.last_name}"
|
||||
)
|
||||
|
||||
logger.info(f"✅ Candidate registration initiated for: {request.email}")
|
||||
|
@ -799,6 +799,60 @@ class UserPreference(BaseModel):
|
||||
# ============================
|
||||
# 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):
|
||||
prompt: str
|
||||
tunables: Optional[Tunables] = None
|
||||
|
Loading…
x
Reference in New Issue
Block a user