-
Notifications
You must be signed in to change notification settings - Fork 0
Architecture
src/
├── server.ts # Entry: Express setup, MongoDB connect, Redis init
├── alias.ts # module-alias: @ → src/ (dev) or dist/ (prod)
├── config/
│ ├── process.config.ts # Env var validation + export
│ ├── cors.config.ts # CORS: CORS_ORIGIN allow-list, credentials: true
│ ├── session.config.ts # Express session config
│ ├── joi.config.ts # Shared Joi schemas (email, password, phone, description, etc.)
│ ├── regex.config.ts # Password + phone regex
│ └── swagger.config.ts # OpenAPI spec (swagger-jsdoc), served at /api-docs
├── database/
│ └── mongo.db.ts # Singleton MongoDB connection manager
├── locales/ # i18n message dictionaries (vi.ts default, en.ts) — 100+ keys,
│ # covers auth, candidate, CV sections, uploads/images
├── middlewares/
│ ├── verifyToken.middleware.ts # JWT extraction (Bearer/cookie/body/query) + blacklist
│ │ # + session-revocation + CSRF check; attaches req.user._id,
│ │ # forces req.body.candidateId
│ ├── csrf.middleware.ts # Standalone CSRF check for the 2 auth routes that
│ │ # bypass verifyToken (/auth/refresh, /auth/logout)
│ ├── language.middleware.ts # Resolves Accept-Language → req.lang / req.t(key)
│ ├── rateLimit.middleware.ts # Redis-backed rate limit; mem fallback
│ ├── errors.middleware.ts # Global error handler; AppError-aware
│ ├── requestLogger.middleware.ts # Logs method, URL, status, duration via Winston
│ ├── uploadCV.middleware.ts # Multer: candidate's own PDF résumé (max 5MB)
│ ├── uploadImages.middleware.ts # Multer: CV-section image attachments
│ └── uploadLinkedInExport.middleware.ts # Multer: LinkedIn export ZIP (max 20MB)
├── models/
│ ├── candidate.model.ts, generalInformation.model.ts, experience.model.ts,
│ │ education.model.ts, project.model.ts, certificate.model.ts,
│ │ award.model.ts, reference.modal.ts, application.model.ts, profile.model.ts,
│ │ visit.model.ts
│ └── part/index.ts # Reusable sub-schemas (skills, languages, socialMedia, localizedTextSchema)
├── routers/
│ ├── api/v1/ # All active routes
│ └── api/v2/ # Auth v2 — thin re-export of the same v1 auth controller (see below)
├── auth/ # Auth controller/service — the one real implementation, used by both v1 and v2
├── candidate/ # Self-profile controller/service, CV upload/download, LinkedIn export parsing
├── candidate_profile/ # One controller+service+validate per CV section (+ application, profile)
│ └── BaseController.ts / BaseService.ts # Shared CRUD factory: getAll/create/update/delete/
│ # restore/upload-images, used by every section
├── candidate_me/ # Public profile aggregation (slug/email, i18n, ?profile= filter),
│ # visit recording, PDF/JSON/DOCX export
├── services/
│ ├── index.ts # Core DB ops: baseFindDocument, baseCreateDocument,
│ │ # baseUpdateDocument, basePatchDocument, baseDeleteDocument
│ │ # (soft-delete), baseRestoreDocument
│ ├── redis.ts # Redis client singleton
│ ├── createPDF.ts # Puppeteer PDF generation
│ └── createDocx.ts # docx-based Word export (same aggregated data as PDF)
├── scripts/
│ └── migrate-localize-text-fields.ts # One-off data migration (see Data Models)
├── utils/ # jwt, bcrypt, tokenBlacklist, sessionRevocation, authCookies, csrf,
│ # slug, querySafe, i18n, timeout, helper, emailVerification,
│ # passwordReset, ...
├── errors/ # AppError hierarchy
└── types/ # base.type.ts, candidate.type.ts, express.d.ts
- Request logger (Winston)
- Cookie parser (httpOnly JWT cookies →
req.cookies) - Language resolution (
Accept-Language→req.lang) - Session middleware
- CORS (
CORS_ORIGINallow-list,credentials: true) - Body parser (JSON + URL-encoded)
-
GET /health— exempt from rate limit - Swagger UI (
/api-docs,/api-docs.json) — exempt from rate limit - Rate limiter (Redis or in-memory)
- Static files (
public/) - API router
- Global error handler
Dev runs on port 3001, prod on port 3008 (both respect LOCAL_PORT when it's actually set).
-
POST /auth/register→ validate Joi → check duplicate email → bcrypt(password, 12) → createCandidatedoc -
POST /auth/login→ find by email → bcrypt compare → signaccessToken(TOKEN_SECRET, expiryTOKEN_EXP_IN) +refreshToken(TOKEN_REFRESH, expiryTOKEN_REFRESH_EXP_IN, default 7d) with payload{ _id: candidateId }→ set as httpOnly cookies and returned in the response body (dual, transitional) - Every protected request →
verifyTokenmiddleware → extract the token from Bearer header, cookie, body, or query → verify signature → check blacklist → check per-candidate session-revocation timestamp (logout-all) → if the request authenticated purely off the cookie (no Bearer header), require a valid double-submit CSRF token → attachreq.user._idand forcereq.body.candidateIdto that same id, overwriting anything the client sent (the fix for the IDOR incident — see Security) -
POST /auth/refresh→ CSRF-checked → verify refresh token → blacklist old refresh token → issue new pair -
POST /auth/logout→ CSRF-checked → blacklist current token (Redis TTL = token remaining exp; in-memory fallback) -
POST /auth/logout-all→ bumps the candidate's session-invalidated-at timestamp, so every token issued before now is rejected on next use even if not individually blacklisted or expired
Earlier versions of this wiki (through v1.1.0) flagged routers/api/v2/auth.route.ts as pointing at a separate, untested, buggy implementation. That's no longer the case: api/v2/auth.route.ts now imports authRegister/authLogin directly from @/auth/auth.controller — the exact same controller /api/v1/auth uses. src/api/ is now empty (just a .gitkeep). /api/v2 is still thinner than v1 (only register/login, none of v1's logout/refresh/logout-all/forgot-password/reset-password/verify-email), but it's no longer a second, drifting code path.
The 7 CV sections (education, experience, award, certificate, project, reference, generalInformation) plus the newer application and profile collections all go through candidate_profile/BaseController.ts + BaseService.ts + services/index.ts's base DB ops. Ownership is enforced via req.user._id (see Security for the incident that made this actually true). Delete is soft (deletedAt timestamp, POST .../restore/:id to undo) rather than a real deleteOne, except account-level self-delete (DELETE /api/v1/candidate), which still hard-deletes and cascades across every section.
profile.model.ts stores a named subset of a candidate's own Education/Experience/Project/Certificate/Award/Reference ids. On first GET /api/v1/profile, ensureDefaultProfile() lazily synthesizes a "Tổng hợp" (All) profile containing every existing item, so no candidate ever starts with zero profiles. candidate_me/index.ts's public-profile handler accepts ?profile=<id> and, when it resolves to a profile owned by the requested candidate, filters each of the 6 selectable sections down to that profile's id lists — an invalid/foreign profile id falls back to the old unfiltered behavior (fail-closed, not fail-open), so every pre-existing share link keeps working unchanged.