From 98092e12d6ecef37cae9e06792c6562c3cc382f1 Mon Sep 17 00:00:00 2001 From: James Ketrenos Date: Fri, 20 Jun 2025 14:09:51 -0700 Subject: [PATCH] Added missing verify-email route --- frontend/.gitignore | 2 +- frontend/.prettied-it | 0 frontend/pretty-it | 7 ++ .../EmailVerificationComponents.tsx | 9 +- frontend/src/components/GenerateImage.tsx | 2 - frontend/src/components/JobMatchAnalysis.tsx | 10 +- frontend/src/components/layout/Header.tsx | 6 +- frontend/src/pages/JobAnalysisPage.tsx | 8 +- src/backend/routes/auth.py | 112 ++++++++++++++++++ 9 files changed, 131 insertions(+), 25 deletions(-) create mode 100644 frontend/.prettied-it create mode 100755 frontend/pretty-it diff --git a/frontend/.gitignore b/frontend/.gitignore index 7418c6f..54e01ab 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -1,4 +1,4 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. +.prettied-it build deployed diff --git a/frontend/.prettied-it b/frontend/.prettied-it new file mode 100644 index 0000000..e69de29 diff --git a/frontend/pretty-it b/frontend/pretty-it new file mode 100755 index 0000000..7bff5f5 --- /dev/null +++ b/frontend/pretty-it @@ -0,0 +1,7 @@ +#!/bin/bash +if [[ ! -e .prettied-it ]]; then + find src -name '*tsx' | while read file; do npx prettier --write $file; done +else + find src -name '*tsx' -newer .prettied-it | while read file; do npx prettier --write $file; done +fi +touch .prettied-it diff --git a/frontend/src/components/EmailVerificationComponents.tsx b/frontend/src/components/EmailVerificationComponents.tsx index c286d42..5ce3683 100644 --- a/frontend/src/components/EmailVerificationComponents.tsx +++ b/frontend/src/components/EmailVerificationComponents.tsx @@ -612,14 +612,7 @@ const LoginForm = (): JSX.Element => { // Device Management Component const TrustedDevicesManager = (): JSX.Element => { - const [devices, _setDevices] = useState([]); - const [_loading, setLoading] = useState(true); - - // This would need API endpoints to manage trusted devices - useEffect(() => { - // Load trusted devices - setLoading(false); - }, []); + const devices: MFAData[] = []; return ( diff --git a/frontend/src/components/GenerateImage.tsx b/frontend/src/components/GenerateImage.tsx index 1a9c7db..714c483 100644 --- a/frontend/src/components/GenerateImage.tsx +++ b/frontend/src/components/GenerateImage.tsx @@ -18,7 +18,6 @@ const GenerateImage = (props: GenerateImageProps): JSX.Element => { const { setSnack } = useAppState(); const [processing, setProcessing] = useState(false); const [status, setStatus] = useState(''); - const [image, _setImage] = useState(''); const controllerRef = useRef(null); // Effect to trigger profile generation when user data is ready @@ -97,7 +96,6 @@ const GenerateImage = (props: GenerateImageProps): JSX.Element => { minHeight: 'max-content', }} > - {image !== '' && {prompt}} {prompt && ( = (props: JobAnalysisProps) = const [loadingRequirements, setLoadingRequirements] = useState(false); const [expanded, setExpanded] = useState(false); const [overallScore, setOverallScore] = useState(0); - const [_statusMessage, setStatusMessage] = useState(null); const [startAnalysis, setStartAnalysis] = useState(false); const [analyzing, setAnalyzing] = useState(false); const [matchStatus, setMatchStatus] = useState(''); - const [_matchStatusType, setMatchStatusType] = useState(null); const [percentage, setPercentage] = useState(0); const isMobile = useMediaQuery(theme.breakpoints.down('sm')); @@ -137,7 +135,6 @@ const JobMatchAnalysis: React.FC = (props: JobAnalysisProps) = setRequirements(requirements); setSkillMatches(initialSkillMatches); - setStatusMessage(null); setLoadingRequirements(false); setOverallScore(0); }, @@ -151,11 +148,10 @@ const JobMatchAnalysis: React.FC = (props: JobAnalysisProps) = const skillMatchHandlers = useMemo(() => { return { onStatus: (status: Types.ChatMessageStatus): void => { - setMatchStatusType(status.activity); setMatchStatus(status.content.toLowerCase()); }, }; - }, [setMatchStatus, setMatchStatusType]); + }, [setMatchStatus]); // Fetch match data for each requirement useEffect(() => { @@ -200,7 +196,7 @@ const JobMatchAnalysis: React.FC = (props: JobAnalysisProps) = break; } if ( - skillMatch.evidenceStrength == 'none' && + skillMatch.evidenceStrength === 'none' && skillMatch.evidenceDetails && skillMatch.evidenceDetails.length > 3 ) { diff --git a/frontend/src/components/layout/Header.tsx b/frontend/src/components/layout/Header.tsx index c3ddacc..6631ae0 100644 --- a/frontend/src/components/layout/Header.tsx +++ b/frontend/src/components/layout/Header.tsx @@ -113,7 +113,7 @@ interface HeaderProps { sessionId?: string | null; } -type MenuItem = { +type NavigationMenuItem = { id: string; label: string; icon: React.ReactElement | null; @@ -151,8 +151,8 @@ const Header: React.FC = (props: HeaderProps) => { const userMenuGroups = getUserMenuItemsByGroup(user?.userType || null, isAdmin); // Create user menu items array with proper actions - const createUserMenuItems = (): MenuItem[] => { - const items: Array = []; + const createUserMenuItems = (): NavigationMenuItem[] => { + const items: Array = []; // Add profile group items userMenuGroups.profile.forEach(item => { diff --git a/frontend/src/pages/JobAnalysisPage.tsx b/frontend/src/pages/JobAnalysisPage.tsx index 5870e4b..16ccd18 100644 --- a/frontend/src/pages/JobAnalysisPage.tsx +++ b/frontend/src/pages/JobAnalysisPage.tsx @@ -66,7 +66,7 @@ interface AnalysisState { resume: string | null; } -interface Step { +interface StepData { index: number; label: string; requiredState: string[]; @@ -82,7 +82,7 @@ const initialState: AnalysisState = { }; // Steps in our process -const steps: Step[] = [ +const steps: StepData[] = [ { requiredState: [], title: 'Job Selection', icon: }, { requiredState: ['job'], title: 'Select Candidate', icon: }, { @@ -110,7 +110,7 @@ const JobAnalysisPage: React.FC = (_props: BackstoryPageProp const { selectedCandidate, setSelectedCandidate } = useSelectedCandidate(); const { selectedJob, setSelectedJob } = useSelectedJob(); - const [activeStep, setActiveStep] = useState(steps[0]); + const [activeStep, setActiveStep] = useState(steps[0]); const [error, setError] = useState(null); const [jobTab, setJobTab] = useState('select'); const [analysisState, setAnalysisState] = useState(null); @@ -119,7 +119,7 @@ const JobAnalysisPage: React.FC = (_props: BackstoryPageProp const isMobile = useMediaQuery(theme.breakpoints.down('sm')); const canAccessStep = useCallback( - (step: Step) => { + (step: StepData) => { if (!analysisState) { return; } diff --git a/src/backend/routes/auth.py b/src/backend/routes/auth.py index e086bcd..95fbe9d 100644 --- a/src/backend/routes/auth.py +++ b/src/backend/routes/auth.py @@ -20,6 +20,7 @@ from device_manager import DeviceManager from email_service import VerificationEmailRateLimiter, email_service from logger import logger from models import ( + EmailVerificationRequest, LoginRequest, CreateCandidateRequest, Candidate, @@ -1061,3 +1062,114 @@ async def confirm_password_reset(request: PasswordResetConfirm, database: RedisD return JSONResponse( status_code=500, content=create_error_response("RESET_ERROR", "An error occurred resetting the password") ) + +@router.post("/verify-email") +async def verify_email( + request: EmailVerificationRequest, + database: RedisDatabase = Depends(get_database) +): + """Verify email address and activate account""" + try: + # Get verification data + verification_data = await database.get_email_verification_token(request.token) + + if not verification_data: + logger.warning(f"⚠️ Invalid verification token: {request.token}") + return JSONResponse( + status_code=400, + content=create_error_response("INVALID_TOKEN", "Invalid or expired verification token") + ) + + if verification_data.get("verified"): + logger.warning(f"⚠️ Attempt to verify already verified email: {verification_data['email']}") + return JSONResponse( + status_code=400, + content=create_error_response("ALREADY_VERIFIED", "Email already verified") + ) + + # Check expiration + expires_at = datetime.fromisoformat(verification_data["expires_at"]) + if datetime.now(timezone.utc) > expires_at: + logger.warning(f"⚠️ Verification token expired for: {verification_data['email']}") + return JSONResponse( + status_code=400, + content=create_error_response("TOKEN_EXPIRED", "Verification token has expired") + ) + + # Extract user data + user_type = verification_data["user_type"] + user_data_container = verification_data["user_data"] + + if user_type == "candidate": + candidate_data = user_data_container["candidate_data"] + password = user_data_container["password"] + username = user_data_container["username"] + + # Activate candidate + candidate_data["status"] = "active" + candidate = Candidate.model_validate(candidate_data) + + # Create authentication record + auth_manager = AuthenticationManager(database) + await auth_manager.create_user_authentication(candidate.id, password) + + # Store in database + await database.set_candidate(candidate.id, candidate.model_dump()) + + # Add user lookup records + user_auth_data = { + "id": candidate.id, + "type": "candidate", + "email": candidate.email, + "username": username + } + + await database.set_user(candidate.email, user_auth_data) + await database.set_user(username, user_auth_data) + await database.set_user_by_id(candidate.id, user_auth_data) + + elif user_type == "employer": + employer_data = user_data_container["employer_data"] + password = user_data_container["password"] + username = user_data_container["username"] + + # Activate employer + employer_data["status"] = "active" + employer = Employer.model_validate(employer_data) + + # Create authentication record + auth_manager = AuthenticationManager(database) + await auth_manager.create_user_authentication(employer.id, password) + + # Store in database + await database.set_employer(employer.id, employer.model_dump()) + + # Add user lookup records + user_auth_data = { + "id": employer.id, + "type": "employer", + "email": employer.email, + "username": username + } + + await database.set_user(employer.email, user_auth_data) + await database.set_user(username, user_auth_data) + await database.set_user_by_id(employer.id, user_auth_data) + + # Mark as verified + await database.mark_email_verified(request.token) + + logger.info(f"✅ Email verified and account activated for: {verification_data['email']}") + + return create_success_response({ + "message": "Email verified successfully! Your account is now active.", + "accountActivated": True, + "userType": user_type + }) + + except Exception as e: + logger.error(f"❌ Email verification error: {e}") + return JSONResponse( + status_code=500, + content=create_error_response("VERIFICATION_FAILED", "Failed to verify email") + ) \ No newline at end of file