2 · Core pipeline — workflow-api
อ้างอิง branch: uat
ไม่มี authentication guard
main.ts ไม่ เรียก app.useGlobalGuards(...) (workflow-api/src/main.ts (branch: uat):9-82) และไม่พบ @UseGuards(...) ใน controller ของ embedding / health-checkup / ipd
สิ่งที่มี:
| ชั้น | ทำอะไร | ไม่ทำอะไร |
|---|---|---|
Swagger addBearerAuth | ปุ่ม Authorize ใน UI | ไม่ enforce runtime |
resolveIpdUser(req) | decode JWT → อ่าน profile → อนุมาน role | ไม่ verify signature |
resolveHealthProgramActor(req) | ดึงชื่อสำหรับ audit | ไม่ verify signature |
requireRole(user, ...) | เช็ค role ที่อนุมานแล้ว | ไม่เพิ่ม cryptographic trust |
Swagger ระบุเอง:
signature is not verified in this service
หลักฐาน — main.ts:42
Comment ใน util:
Does not verify the signature — rely on API gateway / auth layer for validation
หลักฐาน — jwt-actor.util.ts:1-4
มี API gateway ชั้นนอกที่ verify จริงหรือไม่ = UNVERIFIED จาก repo นี้ (ดู SEC-WORKFLOW-01)
วิธี decode
export function decodeJwtPayloadUnsafe(token: string): Record<string, unknown> | null {
const parts = token.split('.');
if (parts.length !== 3 || !parts[1]) return null;
try {
const json = Buffer.from(parts[1], 'base64url').toString('utf8');
// ... JSON.parse ...
} catch {
return null;
}
}หลักฐาน — workflow-api/src/common/auth/jwt-actor.util.ts (branch: uat):33-47
ใครก็สร้าง JWT-shaped string (header.payload.sig) ได้ — ส่วน signature ถูกละเลย
การอนุมาน role IPD จาก sourceDB
function inferHospitalUserRole(sourceDB?: string): IpdRole {
if (sourceDB === 'dbHRMI_RC_C_rep') return 'SOMDEJ';
if (sourceDB === 'dbHRMI_RC_B_rep') return 'SABC';
return 'CHULA';
}หลักฐาน — workflow-api/src/modules/ipd/ipd-user.util.ts (branch: uat):20-28
resolveIpdUser บังคับมี Bearer token + empCode + sourceDB ไม่งั้น ForbiddenException (ipd-user.util.ts:30-64) — แต่ค่าใน payload ไม่ถูกพิสูจน์ทาง crypto
Layer ร่วมของ HTTP
HTTP request
│
▼
CORS (enableCors — default permissive)
│
▼
RequestContextMiddleware forRoutes('*')
│
▼
validateRequest() pipe custom — ไม่ใช่ ValidationPipe ตรง ๆ
│
▼
HttpErrorFilter
│
▼
Controller
│ IPD: resolveIpdUser(req)
│ Health mutations: resolveHealthProgramActor(req)
│ Embedding: มักไม่มี actor check ใน controller layer
▼
Service → DataSource (named connection)หลักฐาน bootstrap — main.ts:12-23, middleware — app.module.ts:26-28
Pipeline A — IPD billing batch (end-to-end)
โดเมนที่กระทบเงินและสิทธิ์หลายบทบาทมากที่สุด
Status machine
จาก BATCH_STATUS (ipd.constants.ts:3-15):
DRAFT
│ submit (CHULA maker)
▼
PENDING_REVIEW ←→ RETURN_MAKER / RETURN_REVIEWER
│
▼
PENDING_APPROVAL
│ approval-decision
▼
APPROVED → POSTED / POST_FAILED
↘ REJECTED
DELETED (soft)ค่าอื่นใน enum: REVIEWING (ประกาศไว้)
Role × hospital mapping
sourceDB ใน JWT | Role ที่ได้ | Hospital (จาก constants) |
|---|---|---|
dbHRMI_RC_C_rep | SOMDEJ | โรงพยาบาลสมเด็จฯ ศรีราชา |
dbHRMI_RC_B_rep | SABC | (SABC — ไม่ map เป็น hospital code ตรง ๆ) |
อื่น ๆ (เช่น dbHRMI_CU_H_rep) | CHULA | จุฬาลงกรณ์ (resolveHospitalCode) |
หลักฐาน — ipd-user.util.ts:20-28, ipd.constants.ts:19-44
Flow สร้าง → อัปโหลด → ส่งตรวจ
Client (โรงพยาบาล / SABC backoffice)
│ Authorization: Bearer <jwt ไม่ verify>
▼
POST /ipd/batches
│ IpdController.createBatch → resolveIpdUser
▼
IpdBatchService.createBatch
├─ ถ้า role !== 'CHULA' → Forbidden
├─ validate batchName, startDate, closeDate, ...
├─ INSERT ipdBillingBatch status=DRAFT (connection ที่ชี้ตาราง IPD — ผ่าน this.db)
└─ INSERT ipdLogs
▼
POST /ipd/batches/:batchId/files (multipart Excel)
│ sheet บังคับชื่อ 'รวม', ขนาด ≤ 10MB
│ parse ตาม EXCEL_COL column indexes
│ validationStatus PASSED/FAILED ต่อไฟล์
▼
POST /ipd/batches/:batchId/submit
│ IpdBatchService.submitBatch
├─ role ต้องเป็น CHULA
├─ createdBy === user.empCode
├─ status เป็น DRAFT หรือ RETURN_MAKER
├─ มีไฟล์ validationStatus='PASSED'
└─ transaction:
UPDATE status='PENDING_REVIEW'
INSERT ipdLogs / ipdLogDetailsหลักฐาน create — ipd-batch.service.ts:299-302 (role check), controller ipd.controller.ts:75-80
หลักฐาน submit — ipd-batch.service.ts:1894-1953:
async submitBatch(user: IpdUser, batchId: string) {
if (user.role !== 'CHULA') { throw new ForbiddenException(...); }
// ...
if (batch.createdBy !== user.empCode) { throw new ForbiddenException(...); }
if (!isMakerEditableStatus(batch.status)) { throw new UnprocessableEntityException(...); }
// ...
await this.db.transaction(async (em) => {
await em.query(
`UPDATE ipdBillingBatch
SET status='${BATCH_STATUS.PENDING_REVIEW}',
updatedBy=@0,updatedAt=GETDATE()
WHERE batchId=@1`,
[user.fullName, batchId],
);
// INSERT ipdLogs ...
});
}BATCH_STATUS.PENDING_REVIEW ถูก interpolate เป็นค่าคงที่ใน SQL string ปนกับ @0/@1 — ปัจจุบันไม่ exploitable จาก input แต่เป็นรูปแบบเสี่ยง (CORR-WORKFLOW-02)
Excel parsing constraints
จาก ipd.constants.ts:
| ค่าคงที่ | ค่า | หลักฐาน |
|---|---|---|
MAX_FILE_SIZE_BYTES | 10 MiB | :202 |
REQUIRED_SHEET_NAME | 'รวม' | :203 |
EXCEL_COL.* | index คอลัมน์ (เช่น nationalId: 20, claimAmount: 19) | :46-67 |
CLAIMLCT_TO_AFFILIATION | map รหัส claim → หน่วยงาน | :71-103 |
หลัง parse จะมีขั้นตอน HRMI match (IpdHrmiMatchService) เพื่อผูกรายการกับพนักงานจริงใน connection hrmi / hrmi_nbc
Approval / accounting
POST .../submit-approval— ส่งเข้าคิวอนุมัติ (ipd.controller.ts:307)POST .../approval-decision— อนุมัติ / คืน / ปฏิเสธ (ipd.controller.ts:323)GET /approval-center/pending,GET /accounting-center/pending— คิวงานแยกศูนย์- Export เอกสารผ่าน
IpdDocumentService(cover memo, สรุปตาม org/person/COA)
รายละเอียดทุก transition อยู่ใน ipd-batch.service.ts (~2753 บรรทัด) — เอกสารนี้ไม่ duplicate ทั้งไฟล์ แต่ entry points อยู่ที่ controller ด้านบน
ความเสี่ยงหลักของ pipeline นี้
เพราะ role มาจาก sourceDB ใน payload ที่ไม่ verify — ผู้ประกอบ JWT ปลอมด้วย profile.sourceDB='dbHRMI_RC_B_rep' จะได้ role SABC และผ่าน requireRole ของ flow ที่อนุญาต SABC โดยไม่ต้องมี session จริงจาก IdP (ถ้าไม่มี gateway ชั้นนอก)
Pipeline B — Course embedding (PMS AI)
POST /pms-ai/embedding/jobs/initial
▼
EmbeddingService.startInitialJob() (~2527 บรรทัดใน service)
├─ ดึงหลักสูตรที่ยังไม่ embed จาก PostgreSQL connection 'postgres' (schema pms_ai)
├─ LlmService.callEmbedding()
│ POST {AI_ROUTER_BASE_URL}/embeddings
│ model จาก AI_ROUTER_EMBEDDING_MODEL (default Qwen/Qwen3-Embedding-8B)
│ dimensions จาก EMBEDDING_DIMENSIONS (4096 ใน .env.example)
├─ เก็บเวกเตอร์ (รวมถึงไฟล์ .npy ผ่าน utils/npy.util.ts)
├─ จัดกลุ่มด้วย EMBEDDING_CLUSTER_SIMILARITY_THRESHOLD / GROUPING_THRESHOLD
└─ อัปเดต EmbeddingJob → COMPLETED / PARTIAL / FAILED
▼
GET /pms-ai/embedding/jobs/:id
└─ สถานะ + progressหลักฐาน controller — embedding.controller.ts:26-52
หลักฐาน LLM client — llm.service.ts:82-113:
async callEmbedding(request: EmbeddingRequest): Promise<any> {
const url = `${this.baseUrl}/embeddings`;
// Authorization: Bearer ${this.apiKey}
// body: model, input, encoding_format, dimensions?
}Env ที่เกี่ยวข้อง (ชื่อเท่านั้น — ไม่คัดลอก secret): AI_ROUTER_BASE_URL, AI_ROUTER_API_KEY, AI_ROUTER_EMBEDDING_MODEL, EMBEDDING_* (.env.example:10-22)
Assignments / reports
หลังมี course groups:
POST /pms-ai/assignments— มอบหมายหลักสูตรให้พนักงานตามกลุ่มPOST /pms-ai/assignments/sync— syncGET /pms-ai/reports/course-groups— รายงาน backoffice
ไม่มี Nest cron — job เริ่มจาก HTTP ของผู้ใช้เท่านั้น (ไม่พบ @nestjs/schedule ใน package.json)
Pipeline C — Health program configuration
GET /health-checkups/health-programs/configurations
└─ อ่าน config — ไม่มี guard (เปิดอ่าน)
▼
POST /health-checkups/health-programs/configurations
├─ resolveHealthProgramActor(req, dto, 'create')
│ JWT display name → body.createdBy → x-user-id → 'maker'
└─ HealthProgramConfigService.create()
└─ AuditLogService → INSERT healthCheckLogs (benefit_write)
▼
PATCH /:id , PATCH /:id/toggle-status , DELETE /:id
└─ actor resolution แบบเดียวกัน + audit
▼
POST .../eligibility-simulator
└─ จำลองสิทธิ์ตาม criteria ของโปรแกรมหลักฐาน actor resolution — jwt-actor.util.ts:146-176
หลักฐาน endpoints — health-program-config.controller.ts:110,396,502,563,595,663
Audit table ชื่อ healthCheckLogs บน connection benefit_write (audit-log.service.ts ค่าคงที่ AUDIT_LOG_TABLE) — ใช้ร่วมข้าม action types จำนวนมาก (CREATE, APPROVE, SIMULATE, …)
Pipeline D — DB connection health
GET /pms-ai/db-connections/health — ตรวจหลาย connection พร้อมกัน (db-connection-check.service.ts ตามที่ระบุใน docs เดิม ~บรรทัด 34-50) เพื่อ operational check ว่า postgres/vtrc/hrmi/benefit ยังต่อได้
สรุป auth ต่อ pipeline
| Pipeline | มี Bearer? | Verify signature? | ใช้ claim เพื่อ |
|---|---|---|---|
| IPD mutations | บังคับ ( Forbidden ถ้าไม่มี ) | ไม่ | role + empCode + ownership |
| Health mutations | optional (fallback 'maker') | ไม่ | ชื่อใน audit log |
| Health reads | ไม่บังคับ | — | — |
| Embedding jobs | ไม่บังคับใน controller | — | — |
| DB health | ไม่บังคับ | — | — |