Added missing verify-email route
This commit is contained in:
parent
ff3e4605a1
commit
98092e12d6
2
frontend/.gitignore
vendored
2
frontend/.gitignore
vendored
@ -1,4 +1,4 @@
|
|||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
.prettied-it
|
||||||
build
|
build
|
||||||
deployed
|
deployed
|
||||||
|
|
||||||
|
0
frontend/.prettied-it
Normal file
0
frontend/.prettied-it
Normal file
7
frontend/pretty-it
Executable file
7
frontend/pretty-it
Executable 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
|
@ -612,14 +612,7 @@ const LoginForm = (): JSX.Element => {
|
|||||||
|
|
||||||
// Device Management Component
|
// Device Management Component
|
||||||
const TrustedDevicesManager = (): JSX.Element => {
|
const TrustedDevicesManager = (): JSX.Element => {
|
||||||
const [devices, _setDevices] = useState<MFAData[]>([]);
|
const devices: MFAData[] = [];
|
||||||
const [_loading, setLoading] = useState(true);
|
|
||||||
|
|
||||||
// This would need API endpoints to manage trusted devices
|
|
||||||
useEffect(() => {
|
|
||||||
// Load trusted devices
|
|
||||||
setLoading(false);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
|
@ -18,7 +18,6 @@ const GenerateImage = (props: GenerateImageProps): JSX.Element => {
|
|||||||
const { setSnack } = useAppState();
|
const { setSnack } = useAppState();
|
||||||
const [processing, setProcessing] = useState<boolean>(false);
|
const [processing, setProcessing] = useState<boolean>(false);
|
||||||
const [status, setStatus] = useState<string>('');
|
const [status, setStatus] = useState<string>('');
|
||||||
const [image, _setImage] = useState<string>('');
|
|
||||||
const controllerRef = useRef<string>(null);
|
const controllerRef = useRef<string>(null);
|
||||||
|
|
||||||
// Effect to trigger profile generation when user data is ready
|
// Effect to trigger profile generation when user data is ready
|
||||||
@ -97,7 +96,6 @@ const GenerateImage = (props: GenerateImageProps): JSX.Element => {
|
|||||||
minHeight: 'max-content',
|
minHeight: 'max-content',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{image !== '' && <img alt={prompt} src={`${image}/${chatSession.id}`} />}
|
|
||||||
{prompt && (
|
{prompt && (
|
||||||
<Quote
|
<Quote
|
||||||
size={processing ? 'normal' : 'small'}
|
size={processing ? 'normal' : 'small'}
|
||||||
|
@ -20,7 +20,7 @@ import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
|||||||
import ErrorIcon from '@mui/icons-material/Error';
|
import ErrorIcon from '@mui/icons-material/Error';
|
||||||
import PendingIcon from '@mui/icons-material/Pending';
|
import PendingIcon from '@mui/icons-material/Pending';
|
||||||
import WarningIcon from '@mui/icons-material/Warning';
|
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 { useAuth } from 'hooks/AuthContext';
|
||||||
import { BackstoryPageProps } from './BackstoryTab';
|
import { BackstoryPageProps } from './BackstoryTab';
|
||||||
import { Job } from 'types/types';
|
import { Job } from 'types/types';
|
||||||
@ -49,11 +49,9 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
|
|||||||
const [loadingRequirements, setLoadingRequirements] = useState<boolean>(false);
|
const [loadingRequirements, setLoadingRequirements] = useState<boolean>(false);
|
||||||
const [expanded, setExpanded] = useState<string | false>(false);
|
const [expanded, setExpanded] = useState<string | false>(false);
|
||||||
const [overallScore, setOverallScore] = useState<number>(0);
|
const [overallScore, setOverallScore] = useState<number>(0);
|
||||||
const [_statusMessage, setStatusMessage] = useState<ChatMessage | null>(null);
|
|
||||||
const [startAnalysis, setStartAnalysis] = useState<boolean>(false);
|
const [startAnalysis, setStartAnalysis] = useState<boolean>(false);
|
||||||
const [analyzing, setAnalyzing] = useState<boolean>(false);
|
const [analyzing, setAnalyzing] = useState<boolean>(false);
|
||||||
const [matchStatus, setMatchStatus] = useState<string>('');
|
const [matchStatus, setMatchStatus] = useState<string>('');
|
||||||
const [_matchStatusType, setMatchStatusType] = useState<Types.ApiActivityType | null>(null);
|
|
||||||
const [percentage, setPercentage] = useState<number>(0);
|
const [percentage, setPercentage] = useState<number>(0);
|
||||||
|
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||||
@ -137,7 +135,6 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
|
|||||||
|
|
||||||
setRequirements(requirements);
|
setRequirements(requirements);
|
||||||
setSkillMatches(initialSkillMatches);
|
setSkillMatches(initialSkillMatches);
|
||||||
setStatusMessage(null);
|
|
||||||
setLoadingRequirements(false);
|
setLoadingRequirements(false);
|
||||||
setOverallScore(0);
|
setOverallScore(0);
|
||||||
},
|
},
|
||||||
@ -151,11 +148,10 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
|
|||||||
const skillMatchHandlers = useMemo(() => {
|
const skillMatchHandlers = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
onStatus: (status: Types.ChatMessageStatus): void => {
|
onStatus: (status: Types.ChatMessageStatus): void => {
|
||||||
setMatchStatusType(status.activity);
|
|
||||||
setMatchStatus(status.content.toLowerCase());
|
setMatchStatus(status.content.toLowerCase());
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}, [setMatchStatus, setMatchStatusType]);
|
}, [setMatchStatus]);
|
||||||
|
|
||||||
// Fetch match data for each requirement
|
// Fetch match data for each requirement
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -200,7 +196,7 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
skillMatch.evidenceStrength == 'none' &&
|
skillMatch.evidenceStrength === 'none' &&
|
||||||
skillMatch.evidenceDetails &&
|
skillMatch.evidenceDetails &&
|
||||||
skillMatch.evidenceDetails.length > 3
|
skillMatch.evidenceDetails.length > 3
|
||||||
) {
|
) {
|
||||||
|
@ -113,7 +113,7 @@ interface HeaderProps {
|
|||||||
sessionId?: string | null;
|
sessionId?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
type MenuItem = {
|
type NavigationMenuItem = {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
icon: React.ReactElement | null;
|
icon: React.ReactElement | null;
|
||||||
@ -151,8 +151,8 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
|
|||||||
const userMenuGroups = getUserMenuItemsByGroup(user?.userType || null, isAdmin);
|
const userMenuGroups = getUserMenuItemsByGroup(user?.userType || null, isAdmin);
|
||||||
|
|
||||||
// Create user menu items array with proper actions
|
// Create user menu items array with proper actions
|
||||||
const createUserMenuItems = (): MenuItem[] => {
|
const createUserMenuItems = (): NavigationMenuItem[] => {
|
||||||
const items: Array<MenuItem> = [];
|
const items: Array<NavigationMenuItem> = [];
|
||||||
|
|
||||||
// Add profile group items
|
// Add profile group items
|
||||||
userMenuGroups.profile.forEach(item => {
|
userMenuGroups.profile.forEach(item => {
|
||||||
|
@ -66,7 +66,7 @@ interface AnalysisState {
|
|||||||
resume: string | null;
|
resume: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Step {
|
interface StepData {
|
||||||
index: number;
|
index: number;
|
||||||
label: string;
|
label: string;
|
||||||
requiredState: string[];
|
requiredState: string[];
|
||||||
@ -82,7 +82,7 @@ const initialState: AnalysisState = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Steps in our process
|
// Steps in our process
|
||||||
const steps: Step[] = [
|
const steps: StepData[] = [
|
||||||
{ requiredState: [], title: 'Job Selection', icon: <WorkIcon /> },
|
{ requiredState: [], title: 'Job Selection', icon: <WorkIcon /> },
|
||||||
{ requiredState: ['job'], title: 'Select Candidate', icon: <PersonIcon /> },
|
{ requiredState: ['job'], title: 'Select Candidate', icon: <PersonIcon /> },
|
||||||
{
|
{
|
||||||
@ -110,7 +110,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (_props: BackstoryPageProp
|
|||||||
const { selectedCandidate, setSelectedCandidate } = useSelectedCandidate();
|
const { selectedCandidate, setSelectedCandidate } = useSelectedCandidate();
|
||||||
const { selectedJob, setSelectedJob } = useSelectedJob();
|
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 [error, setError] = useState<string | null>(null);
|
||||||
const [jobTab, setJobTab] = useState<string>('select');
|
const [jobTab, setJobTab] = useState<string>('select');
|
||||||
const [analysisState, setAnalysisState] = useState<AnalysisState | null>(null);
|
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 isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||||
|
|
||||||
const canAccessStep = useCallback(
|
const canAccessStep = useCallback(
|
||||||
(step: Step) => {
|
(step: StepData) => {
|
||||||
if (!analysisState) {
|
if (!analysisState) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ from device_manager import DeviceManager
|
|||||||
from email_service import VerificationEmailRateLimiter, email_service
|
from email_service import VerificationEmailRateLimiter, email_service
|
||||||
from logger import logger
|
from logger import logger
|
||||||
from models import (
|
from models import (
|
||||||
|
EmailVerificationRequest,
|
||||||
LoginRequest,
|
LoginRequest,
|
||||||
CreateCandidateRequest,
|
CreateCandidateRequest,
|
||||||
Candidate,
|
Candidate,
|
||||||
@ -1061,3 +1062,114 @@ async def confirm_password_reset(request: PasswordResetConfirm, database: RedisD
|
|||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
status_code=500, content=create_error_response("RESET_ERROR", "An error occurred resetting the password")
|
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")
|
||||||
|
)
|
Loading…
x
Reference in New Issue
Block a user