MFA is working

This commit is contained in:
James Ketr 2025-06-01 14:09:28 -07:00
parent d7a81481a2
commit 4919da84d6
7 changed files with 72 additions and 627 deletions

View File

@ -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') {

View File

@ -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);

View File

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

View File

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

View File

@ -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>&copy; 2024 {app_name}. All rights reserved.</p>
<p>&copy; 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>&copy; 2024 {app_name}. All rights reserved.</p>
<p>&copy; 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>&copy; 2024 {app_name}. All rights reserved.</p>
<p>&copy; 2025 {app_name}. All rights reserved.</p>
</div>
</div>
</body>

View File

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

View File

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