Added missing verify-email route

This commit is contained in:
James Ketr 2025-06-20 14:09:51 -07:00
parent ff3e4605a1
commit 98092e12d6
9 changed files with 131 additions and 25 deletions

2
frontend/.gitignore vendored
View File

@ -1,4 +1,4 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
.prettied-it
build
deployed

0
frontend/.prettied-it Normal file
View File

7
frontend/pretty-it Executable file
View File

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

View File

@ -612,14 +612,7 @@ const LoginForm = (): JSX.Element => {
// Device Management Component
const TrustedDevicesManager = (): JSX.Element => {
const [devices, _setDevices] = useState<MFAData[]>([]);
const [_loading, setLoading] = useState(true);
// This would need API endpoints to manage trusted devices
useEffect(() => {
// Load trusted devices
setLoading(false);
}, []);
const devices: MFAData[] = [];
return (
<Card>

View File

@ -18,7 +18,6 @@ const GenerateImage = (props: GenerateImageProps): JSX.Element => {
const { setSnack } = useAppState();
const [processing, setProcessing] = useState<boolean>(false);
const [status, setStatus] = useState<string>('');
const [image, _setImage] = useState<string>('');
const controllerRef = useRef<string>(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 !== '' && <img alt={prompt} src={`${image}/${chatSession.id}`} />}
{prompt && (
<Quote
size={processing ? 'normal' : 'small'}

View File

@ -20,7 +20,7 @@ import CheckCircleIcon from '@mui/icons-material/CheckCircle';
import ErrorIcon from '@mui/icons-material/Error';
import PendingIcon from '@mui/icons-material/Pending';
import WarningIcon from '@mui/icons-material/Warning';
import { Candidate, ChatMessage, SkillAssessment, SkillStatus } from 'types/types';
import { Candidate, SkillAssessment, SkillStatus } from 'types/types';
import { useAuth } from 'hooks/AuthContext';
import { BackstoryPageProps } from './BackstoryTab';
import { Job } from 'types/types';
@ -49,11 +49,9 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
const [loadingRequirements, setLoadingRequirements] = useState<boolean>(false);
const [expanded, setExpanded] = useState<string | false>(false);
const [overallScore, setOverallScore] = useState<number>(0);
const [_statusMessage, setStatusMessage] = useState<ChatMessage | null>(null);
const [startAnalysis, setStartAnalysis] = useState<boolean>(false);
const [analyzing, setAnalyzing] = useState<boolean>(false);
const [matchStatus, setMatchStatus] = useState<string>('');
const [_matchStatusType, setMatchStatusType] = useState<Types.ApiActivityType | null>(null);
const [percentage, setPercentage] = useState<number>(0);
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
@ -137,7 +135,6 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
setRequirements(requirements);
setSkillMatches(initialSkillMatches);
setStatusMessage(null);
setLoadingRequirements(false);
setOverallScore(0);
},
@ -151,11 +148,10 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (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<JobAnalysisProps> = (props: JobAnalysisProps) =
break;
}
if (
skillMatch.evidenceStrength == 'none' &&
skillMatch.evidenceStrength === 'none' &&
skillMatch.evidenceDetails &&
skillMatch.evidenceDetails.length > 3
) {

View File

@ -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<HeaderProps> = (props: HeaderProps) => {
const userMenuGroups = getUserMenuItemsByGroup(user?.userType || null, isAdmin);
// Create user menu items array with proper actions
const createUserMenuItems = (): MenuItem[] => {
const items: Array<MenuItem> = [];
const createUserMenuItems = (): NavigationMenuItem[] => {
const items: Array<NavigationMenuItem> = [];
// Add profile group items
userMenuGroups.profile.forEach(item => {

View File

@ -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: <WorkIcon /> },
{ requiredState: ['job'], title: 'Select Candidate', icon: <PersonIcon /> },
{
@ -110,7 +110,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (_props: BackstoryPageProp
const { selectedCandidate, setSelectedCandidate } = useSelectedCandidate();
const { selectedJob, setSelectedJob } = useSelectedJob();
const [activeStep, setActiveStep] = useState<Step>(steps[0]);
const [activeStep, setActiveStep] = useState<StepData>(steps[0]);
const [error, setError] = useState<string | null>(null);
const [jobTab, setJobTab] = useState<string>('select');
const [analysisState, setAnalysisState] = useState<AnalysisState | null>(null);
@ -119,7 +119,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (_props: BackstoryPageProp
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const canAccessStep = useCallback(
(step: Step) => {
(step: StepData) => {
if (!analysisState) {
return;
}

View File

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