Phase 2 — Candidate Dashboard
Goal: Candidates can log in via Firebase, see all their pipeline history across organizations, and view their own submitted answers for past sessions. Privacy boundary is enforced — no internal scores or recruiter notes leak.
2.1 Backend — Candidate Auth Middleware
src/middleware/candidateAuthMiddleware.ts
typescript
// 1. Verify Firebase idToken → get uid
// 2. Participant.findOne({ authId: uid }) → attach req.participant
// 3. If not found → onCandidateFirstLogin(uid, email from token)onCandidateFirstLogin(uid, email):
typescript
const existing = await Participant.findOne({ email: email.toLowerCase() });
if (existing && !existing.authId) {
await Participant.findByIdAndUpdate(existing._id, { authId: uid });
return existing;
}
if (!existing) {
return await Participant.create({
email,
authId: uid,
preferences: { emailNotifications: true },
stats: { totalPipelines: 0, totalInterviews: 0, noShowCount: 0 }
});
}
return existing;2.2 Backend — candidateProjection.ts
src/utils/candidateProjection.ts
Whitelist projection applied to every candidate API response. See Data Ownership docs for the complete field list.
2.3 Backend — Candidate Endpoints
New router: src/routes/candidateRoutes.ts — all routes use candidateAuthMiddleware
| Endpoint | Logic |
|---|---|
GET /v1/candidate/dashboard | CandidatePipeline.find({ participantId }).sort({ lastActivityAt: -1 }) → candidateProjection() |
GET /v1/candidate/pipeline/:id | Verify pipeline.participantId === req.participant._id → return projected pipeline |
GET /v1/candidate/interviews/:id | Verify interview.participantId === req.participant._id → toCandidateProjection() |
PATCH /v1/candidate/interviews/:id/rsvp | Accept or decline live invite → set participantRsvp |
PATCH /v1/candidate/preferences | Update Participant.preferences |
2.4 Frontend — Candidate Dashboard
New pages:
src/pages/candidate/Dashboard.tsx— all pipelines withjobSnapshot,candidateFacingStatus, stage dotssrc/pages/candidate/PipelineDetail.tsx—/candidate/pipeline/:id— stage timeline, per-stagecandidateStatussrc/pages/candidate/PastSession.tsx—/candidate/interviews/:id— own answers, Q&A transcript (no scores)
New service: src/services/candidateDashboard.service.ts
2.5 Frontend — RSVP for Live Interviews
From PipelineDetail.tsx, when candidateStatus === 'scheduled' and stage is live_1on1:
- Show "Accept" / "Decline" buttons
- Call
PATCH /v1/candidate/interviews/:id/rsvp
Phase 2 — Acceptance Criteria
- [ ] Candidate logs in with Firebase (same email as recruiter's invite) →
Participant.authIdis set - [ ]
GET /v1/candidate/dashboardreturns pipelines withcandidateFacingStatusandstageProgression[].candidateStatusonly - [ ] No
feedbacks,aiReport,aiScore,result,notesfields in any candidate API response - [ ] Candidate can view their own screening answers from a past session
- [ ] Candidate can RSVP accept/decline for a live 1-on-1 invite
- [ ] Dashboard shows correct
candidateFacingStatusafter recruiter shortlists/rejects