Skip to content

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 — the IUser document (Mongoose lean object)
  • req.organizationId — the ObjectId of the user's organization
  • req.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 — the IParticipant document

No Auth Routes

These routes use no middleware and are publicly accessible:

RouteAccess control
GET /v1/screening/:tokenInterview.screeningToken validated in handler
POST /v1/screening/:token/startSame
POST /v1/screening/:token/submitSame
POST /v1/interviews/decline/:declineTokenInterview.declineToken validated in handler
GET /v1/dsa/:token (Phase 3)Interview.dsaToken validated in handler
GET /v1/stage-typesPublic — no token required
GET /v1/plansPublic — no token required
/lobby/:roomId, /room/:roomIdMeeting 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
  }
}