Skip to content

Architecture

_david edited this page Aug 21, 2026 · 1 revision

Architecture

Project structure

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: origin '*' (known gap — see Security)
│   ├── 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)
├── middlewares/
│   ├── verifyToken.middleware.ts   # JWT extraction + blacklist check; attaches req.user._id;
│   │                                 also forces req.body.candidateId to the authenticated user
│   ├── 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
├── 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
│   └── part/index.ts          # Reusable sub-schemas (skills, languages, socialMedia, localizedTextSchema)
├── routers/
│   ├── api/v1/                # All active routes
│   └── api/v2/                # Auth v2 — thin, incomplete duplicate of v1 auth; see below
├── auth/                      # v1 auth controller/service — the real, tested implementation
├── api/v1/auth/               # Legacy/duplicate auth impl used by v2 routes — untested, has had bugs
├── candidate/                 # Self-profile controller/service
├── candidate_profile/         # One controller+service+validate per CV section
│   └── BaseController.ts / BaseService.ts  # Shared CRUD factory used by all 7 sections
├── candidate_me/               # Public profile aggregation + PDF export (candidateId-based, no auth)
├── services/
│   ├── index.ts                # Core DB ops: baseFindDocument, baseCreateDocument, baseUpdateDocument, baseDeleteDocument
│   ├── redis.ts                 # Redis client singleton
│   └── createPDF.ts             # Puppeteer PDF generation
├── scripts/
│   └── migrate-localize-text-fields.ts  # One-off data migration (see Data Models)
├── utils/                       # jwt, bcrypt, tokenBlacklist, querySafe, i18n, timeout, helper, ...
├── errors/                      # AppError hierarchy
└── types/                       # base.type.ts, candidate.type.ts, express.d.ts

Request flow (server.ts middleware order)

  1. Request logger (Winston)
  2. Language resolution (Accept-Languagereq.lang)
  3. Session middleware
  4. CORS (origin: '*')
  5. Body parser (JSON + URL-encoded)
  6. GET /health — exempt from rate limit
  7. Rate limiter (Redis or in-memory)
  8. Static files (public/)
  9. API router
  10. Global error handler

Dev runs on port 3001, prod on port 3008.

Auth flow

  1. POST /auth/register → validate Joi → check duplicate email → bcrypt(password, 12) → create Candidate doc
  2. POST /auth/login → find by email → bcrypt compare → sign accessToken (TOKEN_SECRET, expiry TOKEN_EXP_IN) + refreshToken (TOKEN_REFRESH, expiry TOKEN_REFRESH_EXP_IN, default 7d) with payload { _id: candidateId }
  3. Every protected request → verifyToken middleware → extract Bearer token → verify signature → check blacklist → attach req.user._id and force req.body.candidateId to that same id, overwriting anything the client sent (this is the fix for the IDOR incident — see Security)
  4. POST /auth/refresh → verify refresh token → blacklist old refresh token → issue new pair
  5. POST /auth/logout → blacklist current token (Redis TTL = token remaining exp; in-memory fallback)

The v1 vs v2 auth duplication

routers/api/v2/auth.route.ts (/api/v2/auth/register, /login) points at a separate, untested implementation in src/api/v1/auth/ (confusing path — it's the legacy code that /api/v2/ happens to route to), not the real src/auth/ used by /api/v1/auth/*. This duplication caused a real bug: a missing await on password hashing made POST /api/v2/auth/register fail 100% of the time until fixed 2026-08-21. Recommendation, not yet done: delete src/api/v1/auth/ and point v2 at the same src/auth/ implementation v1 uses — tracked as issue #77.

Shared CRUD pattern for CV sections

All 7 CV sections (education, experience, award, certificate, project, reference, generalInformation) go through candidate_profile/BaseController.ts + BaseService.ts + services/index.ts's base DB ops (baseFindDocument, baseCreateDocument, baseUpdateDocument, baseDeleteDocument). Ownership is enforced via req.user._id (see Security for the incident that made this actually true, as of 2026-08-21).

Clone this wiki locally