diff --git a/frontend/src/components/layout/BackstoryRoutes.tsx b/frontend/src/components/layout/BackstoryRoutes.tsx index a279367..c7eb709 100644 --- a/frontend/src/components/layout/BackstoryRoutes.tsx +++ b/frontend/src/components/layout/BackstoryRoutes.tsx @@ -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 = () => (Profile); const BackstoryPage = () => (Backstory); @@ -58,9 +59,11 @@ const getBackstoryDynamicRoutes = (props: BackstoryDynamicRoutesProps): ReactNod if (!user) { routes.push()} />); routes.push(} />); + routes.push(} />); routes.push(} />); } else { routes.push(} />); + routes.push(} />); routes.push(} />); if (user.userType === 'candidate') { diff --git a/frontend/src/pages/BetaPage.tsx b/frontend/src/pages/BetaPage.tsx index d2f2b41..7555fd9 100644 --- a/frontend/src/pages/BetaPage.tsx +++ b/frontend/src/pages/BetaPage.tsx @@ -38,7 +38,7 @@ const BetaPage: React.FC = ({ const location = useLocation(); if (!children) { - children = (The page you requested ({location.pathname.replace(/^\//, '')}) is not yet read.); + children = (The page you requested ({location.pathname.replace(/^\//, '')}) is not yet ready.); } console.log("BetaPage", children); diff --git a/frontend/src/pages/LoginPage.tsx b/frontend/src/pages/LoginPage.tsx index 041b633..735425e 100644 --- a/frontend/src/pages/LoginPage.tsx +++ b/frontend/src/pages/LoginPage.tsx @@ -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 = (props: BackstoryPageProps) => { - const navigate = useNavigate(); const { setSnack } = props; const [tabValue, setTabValue] = useState(0); const [loading, setLoading] = useState(false); const [success, setSuccess] = useState(null); - const [phone, setPhone] = useState(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>({}); const [errorMessage, setErrorMessage] = useState(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) => { - setLocation(location); - console.log('Location updated:', location); - }; - - // Login form state - const [loginForm, setLoginForm] = useState({ - login: '', - password: '' - }); - - // Register form state - const [registerForm, setRegisterForm] = useState({ - 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 = (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) => { - 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: , - 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: , - 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 = (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) => { - const { value } = event.target; - setLoginForm({ ...loginForm, login: value }); - }; - return ( @@ -416,328 +168,10 @@ const LoginPage: React.FC = (props: BackstoryPageProps) => { {tabValue === 0 && ( - // - // - // Sign In - // - - // - - // setLoginForm({ ...loginForm, password: e.target.value })} - // margin="normal" - // required - // disabled={loading} - // variant="outlined" - // autoComplete='current-password' - // slotProps={{ - // input: { - // endAdornment: ( - // - // - // {showLoginPassword ? : } - // - // - // ) - // } - // }} - // /> - - // - // )} {tabValue === 1 && ( - // - // - // Create Account - // - - // {/* User Type Selection */} - // - // - // Select Account Type - // - // - // {(['candidate', 'employer'] as UserRegistrationType[]).map((userType) => { - // const info = getUserTypeInfo(userType); - // return ( - // } - // label={ - // - // {info.icon} - // - // - // {info.title} - // {userType === 'employer' && ( - // - // )} - // - // - // {info.description} - // - // - // - // } - // 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 - // }} - // /> - // ); - // })} - // - // - - // {/* Employer Placeholder */} - // {registerForm.userType === 'employer' && ( - // - // - // Employer Registration Coming Soon - // - // - // 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. - // - // - // )} - - // {/* Basic Information Fields */} - // {registerForm.userType !== 'employer' && ( - // <> - // - // - // setRegisterForm({ ...registerForm, firstName: e.target.value })} - // required - // disabled={loading} - // variant="outlined" - // /> - // - - // - // setRegisterForm({ ...registerForm, lastName: e.target.value })} - // required - // disabled={loading} - // variant="outlined" - // /> - // - // - - // setRegisterForm({ ...registerForm, username: e.target.value })} - // margin="normal" - // required - // disabled={loading} - // variant="outlined" - // /> - - // setRegisterForm({ ...registerForm, email: e.target.value })} - // margin="normal" - // required - // disabled={loading} - // variant="outlined" - // /> - - // {/* Conditional fields based on user type */} - // {registerForm.userType === 'candidate' && ( - // <> - // setPhone(v as E164Number)} - // /> - - // - // - // )} - - // handlePasswordChange(e.target.value)} - // margin="normal" - // required - // disabled={loading} - // variant="outlined" - // slotProps={{ - // input: { - // endAdornment: ( - // - // - // {showRegisterPassword ? : } - // - // - // ) - // } - // }} - // /> - - // {/* Password Requirements */} - // {registerForm.password.length > 0 && ( - // - // - // - // - // - // {passwordRequirements.map((requirement, index) => ( - // - // - // {requirement.met ? ( - // - // ) : ( - // - // )} - // - // - // - // ))} - // - // - // - // - // )} - - // setRegisterForm({ ...registerForm, confirmPassword: e.target.value })} - // margin="normal" - // required - // disabled={loading} - // variant="outlined" - // error={hasPasswordMatchError} - // helperText={hasPasswordMatchError ? 'Passwords do not match' : ''} - // slotProps={{ - // input: { - // endAdornment: ( - // - // - // {showConfirmPassword ? : } - // - // - // ) - // } - // }} - // /> - - // - // - // )} - // )} diff --git a/src/backend/email_service.py b/src/backend/email_service.py index 156adfe..46e1e22 100644 --- a/src/backend/email_service.py +++ b/src/backend/email_service.py @@ -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"]) diff --git a/src/backend/email_templates.py b/src/backend/email_templates.py index 7c81b79..3f714c3 100644 --- a/src/backend/email_templates.py +++ b/src/backend/email_templates.py @@ -124,7 +124,7 @@ EMAIL_TEMPLATES = { @@ -269,7 +269,7 @@ EMAIL_TEMPLATES = { @@ -381,7 +381,7 @@ EMAIL_TEMPLATES = { diff --git a/src/backend/main.py b/src/backend/main.py index e07107b..e51e3b4 100644 --- a/src/backend/main.py +++ b/src/backend/main.py @@ -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}") diff --git a/src/backend/models.py b/src/backend/models.py index f562460..d9ea954 100644 --- a/src/backend/models.py +++ b/src/backend/models.py @@ -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