Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
183 changes: 132 additions & 51 deletions CLAUDE.md

Large diffs are not rendered by default.

151 changes: 93 additions & 58 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
# 📄 Resume API - Backend (Updated)
# 📄 Resume API - Backend

Một ứng dụng API backend **hoàn chỉnh** để **quản lý hồ sơ ứng viên (CV/Resume)** với **Redis rate limiting**, **token blacklist**, **PDF export**, **Winston logging**, và **Jest testing**.
Một ứng dụng API backend **hoàn chỉnh** để **quản lý hồ sơ ứng viên (CV/Resume)** với **JWT (cookie + Bearer)**, **CSRF protection**, **Redis rate limiting**, **token blacklist**, **CV profiles (multi-version)**, **job application tracker**, **LinkedIn export import**, **PDF/DOCX export**, **i18n (vi/en)**, **Winston logging**, và **Jest testing**.

**Version**: 1.0.0 | **Author**: DatVT | **License**: ISC
**Version**: 1.7.0 | **Author**: DatVT | **License**: ISC

---

## 🎯 Features

- 🔐 **Authentication**: JWT (access/refresh), Bcrypt, Token Blacklist (Redis)
- 👤 **Profile**: Candidate info + General (skills, languages, career)
- 🔐 **Authentication**: JWT (access/refresh, httpOnly cookie or Bearer), Bcrypt, CSRF protection, Token Blacklist (Redis), logout-all, forgot/reset password + email verification (stubs)
- 👤 **Profile**: Candidate info + General (skills, languages, career) + shareable vanity slug
- 📚 **Education** / 💼 **Experience** / 🏆 **Awards** / 📜 **Certificates** / 🚀 **Projects** / 👥 **References**
- 📄 **PDF Export** (Pug + PDFKit/Puppeteer)
- 🗂️ **CV Profiles** (multi-version): named subsets of CV sections for tailoring what a public link shows
- 📋 **Job Application Tracker**: applied/interview/offer/rejected pipeline per candidate
- 📎 **LinkedIn Import**: parse a LinkedIn "Data export" ZIP into Education/Experience entries for review
- 📤 **CV File Upload/Download**: store and retrieve a candidate's own PDF résumé
- 📊 **Public Profile Visits**: per-visit analytics (IP + geo) on public profile views
- 🗑️ **Soft Delete + Restore**: recoverable deletes across all CV sections
- 🌐 **i18n**: Vietnamese/English via `Accept-Language`, localized free-text fields (career, descriptions, introduction)
- 📄 **PDF/JSON/DOCX Export** (Pug + PDFKit/Puppeteer/docx)
- 🛡️ **Rate Limiting** (Redis/mem fallback)
- 📊 **Logging** (Winston daily)
- 🧪 **Tests** (Jest: auth/middlewares/utils/DB)
Expand All @@ -37,20 +44,25 @@ Một ứng dụng API backend **hoàn chỉnh** để **quản lý hồ sơ ứ

### Auth & Security

| Tech | Version | Purpose |
| ------------------ | ------- | ---------- |
| JWT | 9.0.2 | Tokens |
| Bcrypt | 5.1.1 | Passwords |
| express-rate-limit | 8.3.0 | Protection |
| Tech | Version | Purpose |
| ------------------ | ------- | ---------------------- |
| JWT | 9.0.2 | Tokens (access/refresh, cookie or Bearer) |
| Bcrypt | 5.1.1 | Passwords |
| express-rate-limit | 8.3.0 | Protection |
| cookie-parser | 1.4.7 | httpOnly JWT cookies |
| geoip-lite | 1.4.10 | Visit geo-location |

### Utils

| Tech | Version | Purpose |
| -------------------- | -------------- | ---------- |
| Joi | 17.13.1 | Validation |
| PDFKit/Puppeteer/Pug | 0.15/22.13/3.0 | PDF |
| Winston | 3.19.0 | Logging |
| swagger-jsdoc/swagger-ui-express | 6.3.0/5.0.1 | OpenAPI docs |
| Tech | Version | Purpose |
| --------------------------------- | --------------- | ------------------------------- |
| Joi | 17.13.1 | Validation |
| PDFKit / Puppeteer / Pug | 0.15/22.13/3.0 | PDF |
| docx | 9.7.1 | DOCX export |
| multer | 2.3.0 | File uploads (CV, images) |
| adm-zip / csv-parse | 0.6.1 / 7.0.2 | LinkedIn export ZIP/CSV parsing |
| Winston | 3.19.0 | Logging |
| swagger-jsdoc / swagger-ui-express| 6.3.0/5.0.1 | OpenAPI docs |

---

Expand All @@ -60,22 +72,24 @@ Một ứng dụng API backend **hoàn chỉnh** để **quản lý hồ sơ ứ
backend/
├── src/
│ ├── server.ts (health/Redis)
│ ├── config/ (env/Joi/CORS)
│ ├── config/ (env/Joi/CORS/session/swagger)
│ ├── database/ (Mongo)
│ ├── middlewares/ (rateLimit/logger)
│ ├── models/ (schemas)
│ ├── routers/api/v1/ (CRUD routes)
│ ├── candidate_profile/ (controllers/services per section)
│ ├── services/ (PDF/Redis)
│ ├── utils/ (JWT/bcrypt/blacklist)
│ ├── middlewares/ (rateLimit/logger/verifyToken/csrf/language/uploads)
│ ├── models/ (schemas incl. application/profile/visit)
│ ├── routers/api/v1/ (CRUD routes) + api/v2/ (auth WIP)
│ ├── candidate/ (profile + upload-cv + LinkedIn import)
│ ├── candidate_profile/ (controllers/services per section, incl. application/profile)
│ ├── candidate_me/ (public profile, visits, PDF/JSON/DOCX export)
│ ├── services/ (PDF/Redis/base DB ops)
│ ├── utils/ (JWT/bcrypt/blacklist/i18n/csrf)
│ ├── views/ (Pug)
│ ├── public/ (assets/pdf)
│ ├── __tests__/ (Jest)
│ └── types/
├── scripts/ (GitHub automation)
├── TODO.md (progress)
├── package.json
└── README.md ← Updated
└── README.md
```

---
Expand All @@ -97,6 +111,7 @@ TOKEN_SECRET=... (32+ chars)
TOKEN_REFRESH=...
SESSION_SECRET=...
REDIS_URL=redis://localhost:6379 # Optional
CORS_ORIGIN=https://your-frontend-domain.example # required for the httpOnly cookie flow
```

3. **Redis** (rec.): `brew install redis && redis-server`
Expand All @@ -119,7 +134,7 @@ docker compose up --build

**Prod** (compiled image, real secrets required):
```
cp .env.example .env # fill in real TOKEN_SECRET/TOKEN_REFRESH/SESSION_SECRET
cp .env.example .env # fill in real TOKEN_SECRET/TOKEN_REFRESH/SESSION_SECRET/CORS_ORIGIN
docker compose -f docker-compose.prod.yml up -d --build
```
→ http://localhost:3008/health
Expand All @@ -135,49 +150,68 @@ in-memory store when `REDIS_URL` is unset.

### Auth `/api/v1/auth`

| Method | Path | Desc |
| ------ | ----------- | --------------- |
| POST | `/register` | Create user |
| POST | `/login` | Get tokens |
| POST | `/logout` | Blacklist token |
| POST | `/refresh` | Renew access |
| Method | Path | Desc |
| ------ | ----------------- | ---------------------------------------------- |
| POST | `/register` | Create user |
| POST | `/login` | Get tokens (GET also supported, deprecated) |
| POST | `/logout` | Blacklist current token (CSRF-checked) |
| POST | `/logout-all` | Revoke every token issued to this candidate |
| POST | `/refresh` | Renew access token (CSRF-checked) |
| POST | `/forgot-password`| Request password reset (stub, no email sent) |
| POST | `/reset-password` | Reset password using a reset token |
| GET | `/verify-email` | Verify email using a token issued on register |

### Candidate `/api/v1/candidate`

| Method | Path | Desc |
| --------- | --------- | ----------- |
| GET | `/:email` | Get profile |
| PUT/PATCH | `/` | Update |
| Method | Path | Desc |
| ------ | ------------------------- | ------------------------------------------- |
| GET | `/:email` | Get profile by email |
| PUT | `/update` | Full update |
| PATCH | `/update` | Partial update |
| DELETE | `/` | Delete own account + all CV section data |
| POST | `/upload-cv` | Upload a PDF résumé (max 5MB) |
| GET | `/cv-file` | Download the uploaded résumé |
| POST | `/parse-linkedin-export` | Parse a LinkedIn export ZIP (stateless, not persisted) |
| GET | `/visits` | Own public-profile visit count + list |

### CRUD Pattern (CV sections + Application + Profile)

**Paths**: `/api/v1/{education,experience,award,certificate,project,reference,generalInformation,application,profile}`

### CRUD Pattern (all sections)
| Method | Path | Desc |
| ------ | ------------- | -------------------------------- |
| GET | `/` | List (optional `page`/`limit`/`sort`) |
| POST | `/create` | Create |
| PUT | `/update` | Update |
| DELETE | `/delete/:id` | Soft-delete by ID (ownership checked) |
| POST | `/restore/:id`| Restore a soft-deleted entry |

**Paths**: `/api/v1/{education,experience,award,certificate,project,reference,generalInformation}`
| Method | Path | Desc |
|--------|------|------|
| GET | `/` | List |
| POST | `/create` | Create |
| PUT | `/update` | Update |
| DELETE | `/delete/:id` | Delete |
`generalInformation` also has `PATCH /update`. `profile` also synthesizes
a default "Tổng hợp" (All) profile on first `GET /` if the candidate has
none yet.

**Header**: `Authorization: Bearer <token>`
**Header**: `Authorization: Bearer <token>` (or httpOnly JWT cookie)

### Other

| Method | Path | Auth | Desc |
| ------ | ---------------- | ---- | ------------------------------ |
| GET | `/health` | None | Health check |
| GET | `/api-docs` | None | Swagger UI (OpenAPI docs) |
| GET | `/api-docs.json` | None | Raw OpenAPI spec (JSON) |
| Method | Path | Auth | Desc |
| ------ | -------------------------- | ---- | ------------------------------------------------------------ |
| GET | `/health` | None | Health check |
| GET | `/api/me/:email` | None | Public profile by vanity slug or email, optional `?profile=` filter |
| POST | `/api/me/:email/visit` | None | Record a visit (count/timestamp/IP/geo) |
| GET | `/api/v1/download-pdf` | Token via query | Export own CV as `pdf` (default), `json`, or `docx` |
| GET | `/api-docs` | None | Swagger UI (OpenAPI docs) |
| GET | `/api-docs.json` | None | Raw OpenAPI spec (JSON) |

---

## 🔐 Auth Flow

1. **Login** → `{token, tokenRefresh}`
2. **API Calls**: `Authorization: Bearer ${token}`
3. **Refresh**: POST `/auth/refresh`
4. **Logout**: Blacklist (Redis/utils/tokenBlacklist.ts)
5. **Invalid**: Checked via Redis/mem store
1. **Login** → `{token, tokenRefresh}` (issued as httpOnly cookies and in the response body)
2. **API Calls**: `Authorization: Bearer ${token}` or the httpOnly cookie
3. **Refresh**: POST `/auth/refresh` (CSRF-checked)
4. **Logout**: Blacklist current token (Redis/utils/tokenBlacklist.ts) — `/auth/logout-all` revokes every token
5. **Invalid**: Checked via Redis/mem blacklist store

---

Expand Down Expand Up @@ -210,15 +244,15 @@ thiết kế của từng phần.

---

---

## 🛡️ Production Notes

- **Rate Limit**: Redis (fallback mem), exempt `/health`
- **Blacklist**: Redis/utils/tokenBlacklist.ts
- **CSRF**: required on non-Bearer auth flows (`/auth/refresh`, `/auth/logout`) — see `utils/csrf.ts`
- **CORS**: `CORS_ORIGIN` (comma-separated allow-list) required for the httpOnly cookie flow — `credentials: true` cannot combine with a wildcard origin
- **Logs**: Winston daily rotation
- **Static**: public/ (CSS/JS/fonts/img/PDFs)
- **PDF**: services/createPDF.ts + views/
- **PDF/DOCX**: services/createPDF.ts + views/

---

Expand All @@ -229,6 +263,7 @@ thiết kế của từng phần.
| Mongo fail | MONGO*URI or MONGOBD*\* vars |
| Redis fail | `brew install redis` or mem fallback |
| JWT invalid | Token expired/blacklisted |
| CSRF error | Ensure the CSRF token accompanies non-Bearer refresh/logout calls |
| Rate limited | Wait / check Redis |
| Build fail | `npm run copy` |
| Logs | Check `logs/` (Winston) |
Expand Down
Loading
Loading