Skip to content

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

typescript
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

typescript
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

text
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):

text
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 ใน JWTRole ที่ได้Hospital (จาก constants)
dbHRMI_RC_C_repSOMDEJโรงพยาบาลสมเด็จฯ ศรีราชา
dbHRMI_RC_B_repSABC(SABC — ไม่ map เป็น hospital code ตรง ๆ)
อื่น ๆ (เช่น dbHRMI_CU_H_rep)CHULAจุฬาลงกรณ์ (resolveHospitalCode)

หลักฐาน — ipd-user.util.ts:20-28, ipd.constants.ts:19-44

Flow สร้าง → อัปโหลด → ส่งตรวจ

text
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:

typescript
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_BYTES10 MiB:202
REQUIRED_SHEET_NAME'รวม':203
EXCEL_COL.*index คอลัมน์ (เช่น nationalId: 20, claimAmount: 19):46-67
CLAIMLCT_TO_AFFILIATIONmap รหัส 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)

text
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:

typescript
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 — sync
  • GET /pms-ai/reports/course-groups — รายงาน backoffice

ไม่มี Nest cron — job เริ่มจาก HTTP ของผู้ใช้เท่านั้น (ไม่พบ @nestjs/schedule ใน package.json)


Pipeline C — Health program configuration

text
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 mutationsoptional (fallback 'maker')ไม่ชื่อใน audit log
Health readsไม่บังคับ
Embedding jobsไม่บังคับใน controller
DB healthไม่บังคับ