Auth Middleware
Recruiter Auth
File: src/middleware/authMiddleware.ts
Applied at router level via router.use(authMiddleware). All routes in the protected router get req.user and req.organizationId.
typescript
async function authMiddleware(req, res, next) {
const token = req.headers.authorization?.split('Bearer ')[1];
if (!token) return res.status(401).json({ message: 'No token' });
const decoded = await admin.auth().verifyIdToken(token);
const user = await User.findOne({ authId: decoded.uid });
if (!user) return res.status(401).json({ message: 'User not found' });
// Fast path: use organizationId already on the user doc
let orgId = user.organizationId;
// Fallback: look up by org members array (handles legacy records)
if (!orgId) {
const org = await Organization.findOne({ 'members.userId': user._id });
if (!org) return res.status(401).json({ message: 'Organization not found. Please contact support.' });
orgId = org._id;
}
req.user = user;
req.organizationId = orgId;
next();
}req.user— theIUserdocument (Mongoose lean object)req.organizationId— theObjectIdof the user's organizationreq.user._id— recruiter's MongoDB ID
Candidate Auth
File: src/middleware/candidateAuthMiddleware.ts
Used only on /v1/candidate/* routes.
typescript
async function candidateAuthMiddleware(req, res, next) {
const token = req.headers.authorization?.split('Bearer ')[1];
if (!token) return res.status(401).json({ message: 'No token' });
const decoded = await admin.auth().verifyIdToken(token);
let participant = await Participant.findOne({ authId: decoded.uid });
if (!participant) {
participant = await onCandidateFirstLogin(decoded.uid, decoded.email);
}
req.participant = participant;
next();
}req.participant— theIParticipantdocument
No Auth Routes
These routes use no middleware and are publicly accessible:
| Route | Access control |
|---|---|
GET /v1/screening/:token | Interview.screeningToken validated in handler |
POST /v1/screening/:token/start | Same |
POST /v1/screening/:token/submit | Same |
POST /v1/interviews/decline/:declineToken | Interview.declineToken validated in handler |
GET /v1/dsa/:token (Phase 3) | Interview.dsaToken validated in handler |
GET /v1/stage-types | Public — no token required |
GET /v1/plans | Public — no token required |
/lobby/:roomId, /room/:roomId | Meeting link acts as access control |
Zod Validation Pattern
Controllers define Zod schemas at module scope and call them before business logic:
typescript
const createJobSchema = z.object({
title: z.string().min(1).max(200),
description: z.string().min(1),
stages: z.array(stageConfigSchema).min(1),
});
class JobOpeningController {
async create(req: Request, res: Response, next: NextFunction) {
const parsed = createJobSchema.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json({ errors: parsed.error.errors });
}
// business logic with parsed.data
}
}