4 · Auth model + external integrations
บทนี้รวม auth ที่พิสูจน์จากซอร์ส (no role / no @Public) และแผนที่ outbound ทั้งหมดของ ai-recruitment-api บน branch prod
Pipeline ที่เรียก integration: 02 Core pipeline
ตาราง DB: 03 Persistence
Auth model — global JWT only
ไม่มี NestJS / ไม่มี @Public / ไม่มี RolesGuard
Repo นี้เป็น Express — ไม่มี decorator @Public(), ไม่มี Passport strategy, ไม่มี RolesGuard
เทียบกับ theme "no-role-auth" ใน brief ของ volume นี้: ยืนยันแล้วว่ามีแค่การ verify signature + expiry
Mount จุดเดียว
app.use(verifyJWT);
// ...
app.use('/ai-recruitment-api/api', router);แหล่ง: server.js:37, server.js:60
ไม่มี per-route middleware ที่ข้าม JWT ใน routes/index.js
verifyJWT implementation
const SECRET_KEY = process.env.JWT_SECRET || '';
const verifyJWT = async (req, res, next) => {
const token = req.header('Authorization')?.split(' ')[1];
if (!token) {
return res.status(401).json({ message: 'Access Denied. No token provided.' });
}
try {
const decoded = jwt.verify(token, SECRET_KEY);
req.user = decoded;
const { payload } = await getPayLoad(token)
// ... GMT+7 console.log with payload?.uid
next();
} catch (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).send('TOKEN_EXPIRED')
} else {
return res.status(401).send('TOKEN_INVALID')
}
}
};แหล่ง: middleware/verifyJWT.js:4-39
| ตรวจอะไร | ผล |
|---|---|
| มี Bearer token | จำเป็น — ไม่มี → 401 JSON |
jwt.verify(token, JWT_SECRET) | signature + exp |
iss / aud / client / role / scope | ไม่ตรวจ |
| Quota ต่อ user | ไม่มี |
getPayLoad ใช้ decode จาก jsonwebtoken โดยส่ง SECRET_KEY เป็น argument ที่สอง (:45) — ใน API ของ jsonwebtoken, decode ไม่ได้ verify (ต่างจาก verify); ใช้เพื่อ log uid เท่านั้น หลัง verify ผ่านแล้ว
req.user ถูกอ่านใน logger ของ server (server.js:49 → req.user?.uid) และใน uploadController error log (uploadController.js:22)
Shared JWT secret ข้าม 3 services
| Service | Env key ที่อ่าน | Hash ของค่า (SHA-256 16 hex แรก) |
|---|---|---|
ai-recruitment-api | JWT_SECRET (.env:11) | 68e3ccd444f33362 |
benefit-api | SECRET_KEY | 68e3ccd444f33362 (ตรงกัน) |
recruitment-api | SECRET_KEY | 68e3ccd444f33362 (ตรงกัน) |
ความยาวค่า 372 ตัวอักษรทั้งสาม — ยืนยันว่าเป็น secret ชุดเดียวกันบนเครื่องเอกสารนี้
ผลเชิงสถาปัตยกรรม:
- Token ที่ออกด้วย secret นี้จาก service ใดในกลุ่ม สามารถผ่าน
verifyJWTได้ ถ้ายังไม่หมดอายุ - ไม่มีชั้นแยกว่า token มาจาก back-office หรือ candidate portal
- ค่าใช้จ่าย OpenAI/remove.bg ผูกกับ API key เดียวของ process — ไม่มี per-user metering ในโค้ด
CORS vs Auth
CORS จำกัด origin เฉพาะ back-office + localhost (server.js:24-28) — ลดการเรียกจาก browser โดเมนอื่น
แต่ ไม่ได้แทนที่ auth: client ที่ไม่ใช่ browser (curl, server-side) ที่ถือ JWT ที่ถูกต้องยังเรียกได้โดยไม่สนใจ CORS
Route upload ตั้ง Access-Control-Allow-Origin: req.headers.origin || '*' (routes/index.js:91-106) กว้างกว่า global CORS — เป็นจุดไม่สอดคล้องที่ควรรู้ตอน audit
Headers ที่ CORS อนุญาต
Content-Type, authorization, apiKey (server.js:30)
โค้ดฝั่งนี้ ไม่พบการอ่าน header apiKey เพื่อ authorize — ต่างจาก legacy vtrc-api ที่ใช้ device apiKey
ชื่อ header น่าจะเหลือเพื่อให้ browser preflight ผ่านถ้า client ส่งมาด้วย
External integrations map
ai-recruitment-api :9430
│
┌─────────────────────┼─────────────────────┐
▼ ▼ ▼
OpenAI API remove.bg API api.qrserver.com
chat/completions /v1.0/removebg create-qr-code
images.generate
│ │ │
└──────────┬──────────┴──────────┐ │
▼ ▼ ▼
MSSQL recruitment_ai filesystem Puppeteer
(read-only) storage* local Chromiumไม่พบ axios/fetch ไปยัง vtrc-api, centralize-api, cu-central-api, LINE, หรือ vtrc-common ในโค้ด .js ของ repo นี้
Integration 1 — OpenAI Chat Completions
| รายการ | ค่า |
|---|---|
| Env | OPENAI_API_KEY (.env:5) |
| URL | https://api.openai.com/v1/chat/completions |
| Auth ไปปลายทาง | Authorization: Bearer ${OPENAI_API_KEY} |
Wrapper กลาง
config/openai.js:7-28 — model gpt-4o, messages แบบ user เดี่ยว, fail → ''
ผู้เรียก wrapper:
generateJobDescriptionController.js×4module2_2_controller.js×1 (career)
Inline axios (ไม่ผ่าน wrapper)
| ไฟล์ | Model |
|---|---|
summarizeJobDescriptionController.js:44-60 | gpt-3.5-turbo |
summarizeJobQualificationController.js | gpt-3.5-turbo |
generateInterviewQuestionsController.js:106-124 (callChatGPT) | gpt-3.5-turbo |
ไม่มี timeout ที่ตั้งใน axios ของ wrappers เหล่านี้ (ต่างจาก remove.bg ที่มี 30–45s)
Integration 2 — OpenAI Images API
| รายการ | ค่า |
|---|---|
| Client | official openai npm package |
| Method | openai.images.generate |
| Env | OPENAI_API_KEY ชุดเดียวกับ chat |
| Default model | gpt-image-1 |
| Caller | module2_2_controller.js:177 ผ่าน callDalle |
แหล่ง wrapper: config/dalle.js:41-52
Env DALLE_ENDPOINT ใน .env:8 ไม่ถูกอ่าน ใน dalle.js — endpoint ถูกกำหนดโดย SDK
Integration 3 — remove.bg
| รายการ | ค่า |
|---|---|
| Env | REMOVE_BG_API_KEY (.env:7) |
| URL | https://api.remove.bg/v1.0/removebg |
| Auth header | X-API-Key / X-Api-Key |
Call site A — service (character pipeline)
services/removeBgService.js:46-85 — รองรับ buffer / b64 / URL
timeout 30s (:75)
เรียกจาก module2_2_controller.js:194
Call site B — upload controller
controllers/removeBgUploadController.js:31-86 — multipart หรือ urlencoded
timeout 45s (:45, :83)
เรียกจาก route routes/index.js:117
ถ้าขาด API key: throw / 500 (removeBgService.js:47,131, removeBgUploadController.js:90-95)
Integration 4 — QR Server
| รายการ | ค่า |
|---|---|
| URL | https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=... |
| Env | ไม่มี — hardcode |
| Auth | ไม่มี |
| Caller | combineJobPostingController.js:65-75 (และ orphan module_2_3_combine_image.js:65) |
ส่ง job_link จาก client ตรงเข้า query string หลัง encodeURIComponent — dependency ภายนอกสำหรับทุกภาพประกาศที่ combine
Integration 5 — Puppeteer / Chromium
ไม่ใช่ HTTP ภายนอก แต่เป็น local subprocess:
| รายการ | ค่า | แหล่ง |
|---|---|---|
| Package | puppeteer ^22.6.5 | package.json:23 |
| Executable | /usr/bin/chromium-browser | combineJobPostingController.js:378, Dockerfile:21 |
| Skip download | PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true | Dockerfile:20 |
ใช้เฉพาะ combine pipeline (mounted + orphan twin)
Integration 6 — MSSQL recruitment_ai
ดู 03 Persistence — read-only จากมุม service นี้
Integration 7 — HCTI (ประกาศใน env แต่ไม่พบ caller)
| Key | .env |
|---|---|
HCTI_USER_ID | บรรทัด 9 |
HCTI_API_KEY | บรรทัด 10 |
ค้นทั้ง repo ไม่พบ string HCTI / hcti ในไฟล์ .js — UNVERIFIED ว่าเคยใช้ในอดีต; ปัจจุบันเป็น dead credential ใน env
Comment ใน combineJobPostingController บอกว่าเคยย้ายจาก online renderer มาเป็น Puppeteer local (combineJobPostingController.js:1-9) — สอดคล้องกับ HCTI ที่เหลือใน env
Secrets บน disk / ใน git
| ประเภท | ตำแหน่ง | Tracked? |
|---|---|---|
| MSSQL password | .env:2 | ใช่ (git ls-files .env คืนไฟล์) แม้ .gitignore:51 มี .env |
| OpenAI API key | .env:5 | ใช่ |
| remove.bg API key | .env:7 | ใช่ |
| HCTI credentials | .env:9-10 | ใช่ |
| JWT secret (shared) | .env:11 | ใช่ |
เอกสารนี้ ไม่คัดลอกค่า secret — อ้างเฉพาะหมายเลขบรรทัดและประเภท
Dockerfile COPY . . (Dockerfile:31) จะ bake .env เข้า image ถ้าไฟล์อยู่ใน build context — ความเสี่ยง operational เพิ่มเติมเมื่อ build จาก working tree ที่มี .env
สิ่งที่ service นี้ไม่เชื่อม
ยืนยันจากการค้นโค้ด:
- ไม่เรียก GraphQL ของ
vtrc-api - ไม่เรียก NestJS
centralize-api/cu-central-api - ไม่ใช้ shared library
vtrc-common - ไม่มี LINE / SSO / SFTP client
จุดเชื่อมแพลตฟอร์มที่เหลือมีแค่: (1) shared JWT secret (2) shared DB recruitment_ai (3) CORS ไป back-office
Checklist ตอน debug auth / integration
- 401
No token provided→ ขาด headerAuthorization: Bearer ... TOKEN_INVALID→ secret ไม่ตรง หรือ token เสียรูป (ตรวจว่าฝั่งออก token ใช้SECRET_KEYชุดเดียวกับJWT_SECRET)TOKEN_EXPIRED→ ต่ออายุที่ issuer เดิม- CORS error ใน browser → origin ไม่ใช่ 3 ค่าใน whitelist (หรือ path upload ที่ตั้ง
*แต่ยังต้องผ่าน JWT) - OpenAI ว่างเงียบ → ดูว่าเป็น fail-soft ของ
callOpenAIหรือไม่; ตรวจ billing/key - remove.bg 401/402 → key หรือโควต้า
- combine 500 เรื่อง Chromium → path
/usr/bin/chromium-browserบน host
ไป บท 5 Cron jobs →