4 · Auth model + external integrations
pms-api ใช้โมเดล auth แบบ dual-header เหมือน legacy frontends ของแพลตฟอร์ม — Authorization: Bearer <jwt> + apiKey: <device-key> — ผ่าน global JwtAuthGuard พร้อม @Public() override และมี ThrottlerGuard เป็น global APP_GUARD เพิ่มจาก repo อื่นในกลุ่ม
ทุก claim อ้างจาก canonical branch uat
Auth stack overview
| ส่วน | หลักฐาน | บทบาท |
|---|---|---|
| Global JWT guard | pms-api/src/main.ts (branch: uat):42 — app.useGlobalGuards(new JwtAuthGuard(...)) | ทุก route ต้องผ่าน auth ยกเว้น @Public() |
| Passport JWT strategy | pms-api/libs/jwt/jwt.strategy.ts (branch: uat) | ตรวจลายเซ็น JWT ด้วย SECRET_KEY |
| Guard logic + apiKey | pms-api/libs/jwt/jwt-auth.guard.ts (branch: uat) | ตรวจ apiKey จาก API_DEVICE_KEY, เรียก workforce checkRoleRc |
| Token issue/store | pms-api/src/modules/auth/auth.service.ts (branch: uat) | jsonwebtoken sign + เก็บลง authData |
| Rate limit | pms-api/src/app.module.ts (branch: uat):51-58,116 | 100 req / 60s ต่อ IP |
| Device keys | pms-api/config/configuration.ts (branch: uat):69-211 | map key → APP_TYPE สำหรับ prod และ uat |
JwtAuthGuard — flow ทีละขั้น
pms-api/libs/jwt/jwt-auth.guard.ts (branch: uat):33-83
canActivate(context: ExecutionContext) {
const isPublic = this.reflector.getAllAndOverride<boolean>(
isPublicKey,
[context.getHandler(), context.getClass()],
);
if (isPublic) {
return true;
}
this.apiKey = request.headers?.apikey;
const apiDeviceKey = this.configService.get('API_DEVICE_KEY') || [];
const mapKey = apiDeviceKey[process.env.NODE_ENV]?.find(
(a) => a.key === this.apiKey,
);
// TRIGGER bypass ...
if (!mapKey && process.env.NODE_ENV !== 'development') {
throw new UnauthorizedException({ statusCode: 401, error: 'API KEY Invalid' });
}
return super.canActivate(context);
}ลำดับการตัดสินใจ:
@Public()→ ผ่านทันที ไม่ตรวจ JWT ไม่ตรวจ apiKey- อ่าน
apikeyheader (ตัวพิมพ์เล็ก — Nest normalize headers เป็น lowercase) - หา device key จาก
API_DEVICE_KEY[NODE_ENV]— ใน UAT ใช้ชุดuat:ในconfiguration.ts:132-211 - TRIGGER bypass — ถ้า
mapKey.APP_TYPE === 'TRIGGER'และ JWT payload มีappType === 'trigger'→ ผ่านโดยไม่เรียก Passport (jwt-auth.guard.ts:64-68) ใช้สำหรับ usage-permission trigger จากระบบภายนอก - apiKey บังคับนอก development — ถ้าไม่มี mapKey และ
NODE_ENV !== 'development'→401 API KEY Invalid - Passport JWT —
super.canActivate()ตรวจ Bearer token handleRequest+checkRole— ถ้ามีทั้ง token และ apiKey จะเรียก workforce HTTP เพื่อเช็ค role ตามชื่อ controller
checkRole → workforce-api
pms-api/libs/jwt/jwt-auth.guard.ts (branch: uat):126-144
async checkRole() {
const params = { menu: this.controllerName };
const headers = {
Authorization: this.token,
apiKey: this.apiKey,
};
const { data } = await this.axios.get(
`${this.workforceEndPoint}/checkRoleRc`,
{ headers, params },
);
return data;
}endpoint ปลายทางสร้างจาก WORKFORCE_SERVER + /workforce-api/api/restful/checkRoleRc (บรรทัด 31 ของ guard)
ถ้า checkRoleResult.status === false → 403 Forbidden (handleRequest บรรทัด 110-112)
ความเสี่ยง — ถ้า workforce-api ล่ม ทุก authenticated request ที่ส่งทั้ง token+apiKey จะได้ 403 (ไม่ใช่ 503) เพราะ catch block แปลง error เป็น Forbidden (jwt-auth.guard.ts:115-121)
JwtStrategy validate
pms-api/libs/jwt/jwt.strategy.ts (branch: uat):24-37
async validate(payload: any) {
if (payload.email && payload?.client === 'rc-web') {
const result = await this.authDateService.getByEmailCheckExists(
{ email: payload.email },
);
if (!result) {
throw new ForbiddenException({ statusCode: 403 });
}
}
return payload;
}เงื่อนไขพิเศษเฉพาะ client rc-web — ต้องมีแถวใน authData ตาม email มิฉะนั้น 403 client อื่นผ่านด้วย payload อย่างเดียวหลังลายเซ็นถูกต้อง
AuthDataService — ออกและเก็บ token
pms-api/src/modules/auth/auth.service.ts (branch: uat):21-32
private readonly privateKey = process.env.SECRET_KEY;
private readonly refreshKey = process.env.REFRESH_TOKEN_SECRET_KEY;
generateToken(payload: any): { token: string; refreshToken: string } {
const token = jwt.sign(payload, this.privateKey, {
expiresIn: process.env.JWT_EXPIRE_IN,
});
const refreshToken = jwt.sign(payload, this.refreshKey, {
expiresIn: process.env.REFRESH_EXPIRE_IN,
});
return { token, refreshToken };
}| หัวข้อ | สถานะบน branch uat |
|---|---|
SECRET_KEY | มีใน .env:41 |
REFRESH_TOKEN_SECRET_KEY | ไม่พบใน .env ที่ commit — refreshKey จะเป็น undefined ถ้าไม่มี env runtime อื่น |
JWT_EXPIRE_IN / REFRESH_EXPIRE_IN | ไม่พบใน .env ที่ commit — expire ขึ้นกับค่าที่ inject ตอน deploy |
token ถูกอัปเดตลงตาราง authData ผ่าน updateToken() (auth.service.ts:34-54) — ทำให้ revoke ได้ด้วยการลบแถว (logout / delete)
AuthController ที่ auth.controller.ts ว่างเปล่า (ไม่มี route) — การ login จริงน่าจะเกิดที่ service อื่นหรือถูกเรียกจากภายใน UNVERIFIED ว่า frontend เรียก auth ของ pms-api โดยตรงหรือได้ JWT จาก vtrc-api/workforce
Token validation helpers
| Method | บรรทัด | ทำอะไร |
|---|---|---|
validateEmailParamEqualTokenEmail | auth.service.ts:179-203 | verify JWT แล้วเทียบ email กับ param (ยกเว้น iss === redcross.or.th:vtrc) |
validateToken | :205-238 | ตรวจ existence ใน authData + email match |
validateTokenOnlyVTRC | :240-256 | บังคับ iss === redcross.or.th:vtrc |
validateOTP | :275-308 | OTP TTL 5 นาที (เทียบจาก createdAt) — ต่างจาก cu-central ที่ TTL 3 นาที |
ThrottlerGuard (จุดเด่นของ pms-api)
pms-api/src/app.module.ts (branch: uat):51-58,116
ThrottlerModule.forRoot({
throttlers: [{ ttl: 60000, limit: 100 }],
}),
// ...
providers: [..., { provide: APP_GUARD, useClass: ThrottlerGuard }],- 100 requests ต่อ 60 วินาทีต่อ IP (default ของ
@nestjs/throttler) - เป็น repo เดียวในกลุ่ม Domain Microservices ที่ตั้ง global throttle
@Public()endpoints ก็ถูก throttle ด้วย (ยกเว้นถ้ามี@SkipThrottle()— ไม่พบในโค้ดปัจจุบัน)
Device keys (API_DEVICE_KEY)
อยู่ใน configuration.ts ไม่ใช่ .env — มีชุด prod และ uat
ตัวอย่าง APP_TYPE ที่พบในชุด uat:
| APP_TYPE | APP_NAME (ตัวอย่าง) |
|---|---|
WEB | VTRC-WEB-UAT |
MOBILE | VTRC-MOBILE-UAT |
WEB_ADMIN | VTRC-BACKOFFICE-UAT |
INTEGRATE_APP | INFOMA-UAT, CONICLE-UAT |
TWOWAY | VTRC-TWOWAY-UAT |
MEETING | VTRC-MEETING-UAT |
RECRUITMENT / RECRUITMENT-WEB | VTRC-RECRUITMENT-UAT |
TRIGGER | USAGE-PERMISSION-TRIGGER |
ค่า key เป็น UUID ที่ hardcode ใน source — ใครอ่าน repo ได้ก็ปลอม apiKey ได้ (ต้องคู่กับ JWT ที่ถูกต้อง ยกเว้น @Public())
มี entry LOAD_TEST_* ในชุด uat (configuration.ts:158-174) — ใช้สำหรับ load test
@Public() endpoints (9 จุด active)
grep @Public() ทั่ว pms-api/src (branch: uat) — นับเฉพาะที่ไม่ได้ comment
| # | Controller | Method / Path | บรรทัด | ชั้นป้องกันที่ 2 | ระดับความเสี่ยง |
|---|---|---|---|---|---|
| 1 | BudgetController | GET /budget/list | budget.controller.ts:17-20 | ไม่มี | กลาง — รายการ budget |
| 2 | BudgetController | GET /budget/getOrgUnit | :38-41 | ไม่มี | ต่ำ-กลาง — org unit dropdown |
| 3 | BudgetController | GET /budget/getEmployeeSalary | :49-53 | ไม่มี | สูงมาก — คืนเงินเดือนตาม org unit |
| 4 | ConfigNotificationController | POST /configNotification/check-notifications | configNotification.controller.ts:44-55 | NODE_ENV === 'development' | ต่ำ (นอก dev คืน { message: 'Nope !!!' }) |
| 5 | MigrateDataController | POST /migrateData/migrate-data | migrateData.controller.ts:32-67 | NODE_ENV === 'development' | ต่ำ (นอก development ปฏิเสธ) |
| 6 | UsagePermissionController | POST /usagePermission/send-email-promote-new | usagePermission.controller.ts:89-118 | NODE_ENV === 'development' | ต่ำ |
| 7 | UploadController | GET /upload/:id | upload.controller.ts:13-16 | ไม่มี | กลาง — รายการไฟล์ตาม id |
| 8 | AppController | GET /health | app.controller.ts:8-11 | ไม่มี | ต่ำ — health check สมเหตุสมผล |
commented @Public() ใน upload controller (บรรทัด 19, 34, 40) ไม่นับ
รายละเอียด getEmployeeSalary (จุดวิกฤต)
pms-api/src/modules/budget/budget.controller.ts (branch: uat):49-53
@Public()
@Get('/getEmployeeSalary')
getEmployeeSalary(@Query() query: GetSalaryByOrgUnitDto) {
return this.service.getEmployeeSalary(query);
}service เรียก stored procedure sp_get_employeeSalary_orgUnit (budget.repositories.ts:256-273) หรือ query builder ทางเลือก — คืน budgetPerDepartment, salaryStaff, salaryEmployee, budgetStaff, budgetEmployee
ไม่ต้องมี JWT ไม่ต้องมี apiKey — ถูก throttle แค่ 100 req/นาทีเท่านั้น ดู exploit ใน 06 Scorecard SEC-PMS-01
Pattern NODE_ENV double-guard
สาม endpoint (notification / migrate / promote email) ใช้ pattern เดียวกัน:
if (process.env.NODE_ENV === 'development') {
// ทำงานจริง
}
return { message: '...' }; // ปฏิเสธดีกว่า benefit-api ที่ batch endpoints เป็น @Public() โดยไม่มีชั้นสอง — แต่บน UAT (NODE_ENV=uat) ชั้นนี้จะปฏิเสธอยู่แล้ว จึงไม่เปิดช่องบน UAT/prod สำหรับสามอันนี้
ข้อยกเว้น — budget @Public() ทั้งสามไม่มีชั้นสองใด ๆ
External integrations
| ปลายทาง | env var | ค่า UAT (จาก .env) | เรียกจาก | ใช้ทำอะไร |
|---|---|---|---|---|
| workforce DB (ไม่ใช่ HTTP) | WORKFORCE_DB_* | host 10.188.128.58, db dbWorkforce | 10 module (ดู 03 Persistence) | อ่านองค์กร/พนักงาน/trigger queue |
| workforce-api HTTP | WORKFORCE_SERVER | https://uat-vtrcapi.redcross.or.th | jwt-auth.guard.ts:31,136-141 | GET .../checkRoleRc |
| pms-ai-api | PMS_AI_API_URL | https://pms-ai-api.vtrcsupport.com | createForm.service.ts:408, createForm.repositories.ts:1133 | vector embedding สร้าง/ดึง master course |
| VTRC ESS / Backoffice (URL ใน email เท่านั้น) | VTRC_ESS_URL, VTRC_BACKOFFICE_URL | https://uat-vtrc.redcross.or.th, https://uat-vtrcbackoffice.redcross.or.th | configNotificationSendEmail.service.ts:172 | ลิงก์ในอีเมลแจ้งเตือน — ไม่ใช่ API call |
| SMTP Outlook | SMTP_HOST/USER/PASS | smtp-mail.outlook.com | libs/services/emailService.ts | ส่งอีเมล (retry 3 ครั้ง) |
| vtrc-api (env ประกาศ) | VTRC_SERVER | https://uat-vtrcapi.redcross.or.th/ | configuration.ts:6 | UNVERIFIED — ไม่พบจุดเรียกใช้ชัดเจนในรอบนี้ |
| vtrc DB (TypeORM) | VTRC_DB_* | database vtrc | connection ประกาศใน AppModule | UNVERIFIED — ไม่พบ inject |
pms-ai-api (โดเมนภายนอก)
pms-api/src/modules/createForm/createForm.service.ts (branch: uat):400-417
async setCourseLearning(evaluateFormLearning, req) {
const dataForCreate = evaluateFormLearning
?.filter((i) => !i.courseId && i.courseName)
?.map((i) => ({ courseName: i.courseName, createdBy: req?.user?.profile?.empCode })) || [];
if (dataForCreate?.length > 0) {
const { data } = await this.axios.post(
`${this.pmsApiURL}/vector-embeddings/createMasterCourse`,
dataForCreate,
);
// map courseId กลับเข้า DTO
}
}และ createForm.repositories.ts:1133 — GET .../vector-embeddings/getMasterCourse
โดเมน vtrcsupport.com ไม่ใช่ redcross.or.th — ต่างจาก integration อื่นในกลุ่ม ส่วนใหญ่ที่อยู่ใต้โดเมนสภากาชาด ข้อมูลที่ส่งออกคือชื่อคอร์ส + empCode ของผู้สร้าง — ควรยืนยัน data processing agreement (ดู QUAL-PMS-04 ใน scorecard)
พบเอกสารอ้างอิงใน workflow-api/docs/pms-ai-api-spec.md — UNVERIFIED ความสัมพันธ์ runtime ระหว่าง workflow-api กับ service นี้
SMTP
libs/services/emailService.ts — nodemailer port 587, retry สูงสุด 3 ครั้ง, render EJS จาก libs/pdfTemplate/email-template/
Secrets ที่อยู่ใน source control
| ประเภท | ไฟล์ | บรรทัด / ขอบเขต | หมายเหตุ |
|---|---|---|---|
| MSSQL password ×4 ชุด (main / vtrc / workforce / workforceTemp) | pms-api/.env (branch: uat) | 12, 19, 26, 33 | ค่า UAT (NODE_ENV=uat) ไม่ใช่ prod |
JWT SECRET_KEY | .env:41 | ค่าเดียวกับ benefit/recruitment/ai-recruitment | รั่วแล้วกระทบ forge JWT ข้าม service |
| SMTP password | .env:45 | noreply-vtrc@redcross.or.th | ส่งอีเมลในนามองค์กรได้ถ้าถูกขโมย |
| GCP SA key | pms-api/vtrc-gcp-sa-key.json (branch: uat) | ทั้งไฟล์ | ห้าม log / exfiltrate |
| Device API keys | config/configuration.ts:69-211 | hardcode ใน source | ปลอม apiKey ได้ถ้ามี JWT |
ห้าม paste ค่า secret จริงลงเอกสารหรือ chat — อ้างเฉพาะ path:line
แม้ .env เป็น UAT แต่ SECRET_KEY แชร์กับ prod ของอย่างน้อย 3 service อื่นในแพลตฟอร์ม — severity ระดับแพลตฟอร์ม ไม่ใช่แค่ UAT (ดู SEC-PMS-02)
CORS และ ValidationPipe (เกี่ยวข้องกับ auth surface)
pms-api/src/main.ts (branch: uat):43-66
ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true })— reject field นอก DTO (เข้มกว่า repo อื่น)- CORS: นอก prod อนุญาตทุก origin; ใน prod block origin ที่มีคำว่า
localhostแต่ยังallowedHeaders: '*'และอนุญาต origin อื่นเกือบทั้งหมด
ผลต่อ @Public() — จาก browser ใดก็เรียก getEmployeeSalary ได้ถ้า network ถึง UAT/prod (ขึ้นกับ network ACL)
สรุปความแตกต่างจาก repo เพื่อนบ้าน
| หัวข้อ | pms-api | benefit-api / recruitment-api |
|---|---|---|
| Global throttle | มี (100/60s) | ไม่มี |
@Public() + NODE_ENV double-guard | มีบน migrate/noti/email | มักไม่มี |
| Public salary endpoint | มี — ความเสี่ยงสูงสุดของ repo | — |
Shared SECRET_KEY | ยืนยันครั้งที่ 4 ในกลุ่ม | เหมือนกัน |
| Role check ภายนอก | เรียก workforce checkRoleRc | แตกต่างตาม repo |
ไป บท 5 Cron jobs →