Skip to content

6.7 · External integrations + cron + SFTP

บทนี้เจาะลึกการเชื่อมต่อกับระบบภายนอกของ benefit-api — ประกอบด้วย SFTP (ส่งไฟล์ FMIS), cron jobs (pull/push HRMI), Puppeteer (PDF), และ ExcelJS (Excel export)


Integration topology

                        ┌──────────────────┐
                        │   benefit-api    │
                        └────────┬─────────┘

        ┌────────────────────────┼─────────────────────────┐
        │                        │                         │
        ▼                        ▼                         ▼
   ┌─────────┐             ┌──────────┐              ┌──────────┐
   │   SFTP  │             │   HRMI   │              │  Puppeteer│
   │ (FMIS)  │             │  MSSQL   │              │ (PDF gen) │
   └─────────┘             └──────────┘              └──────────┘
        │                        │
        │ cron 80min apart       │
        ▼                        ▼
   FMIS gateway             dbHRMI_Center

SFTP — exportpaymentreport.service.ts

Credentials ใน source code — exportpaymentreport.service.ts:135-145

135:145:benefit-api/src/modules/exportPaymentReport/exportpaymentreport.service.ts
  private readonly sftpConfig = {
    host: '10.188.8.16',
    port: 22,
    username: 'ftpfmis',
    password: '~x-:#"nTb-2k+_TaE',  // hardcoded in source
    readyTimeout: 30000,
    algorithms: {
      cipher: [
        '3des-cbc',
        'aes128-ctr',
        'aes192-ctr',
        'aes256-ctr',
        'aes128-gcm@openssh.com',
      ],
    },
  };

ปัญหา:

  1. password: '~x-:#"nTb-2k+_TaE' hardcoded ใน source — ถูก commit เข้า git
  2. username: 'ftpfmis' แสดงว่าเป็น account เฉพาะ FMIS — ใครได้ source คือได้สิทธิ์ write/read files บน SFTP
  3. host: '10.188.8.16' IP internal — leak topology

ดู 6.8 S3 สำหรับ remediation

SFTP library — ssh2-sftp-client

package.json:31:

31:31:benefit-api/package.json
    "ssh2-sftp-client": "^9.1.0",

ใช้ ssh2-sftp-client v9 — wrapper ของ ssh2 สำหรับ SFTP protocol

Flow — exportpaymentreport.service.ts:6050-6160

1. Service builds CSV/JSON file from `vtrc.WDHospitals` rows
2. connect(sftpConfig)
3. put(Buffer, `/upload/${filename}`)        ← write to FMIS gateway
4. INSERT benefit.WDExportPaymentTransaction (status='PENDING')
5. cron 80 min ต่อมา:
6.   list('/download/') → fetch JSON status files
7.   parse JSON → UPDATE benefit.WDExportPaymentTransaction
8.   delete remote file (cleanup)

Cron jobs — src/crontab/cronTab.service.ts

cronTab.service.ts:30-65 — definition

30:65:benefit-api/src/crontab/cronTab.service.ts
  async pullData() {
    // ดึง HRMI data จาก dbHRMI_Center schema
    // update benefit.* tables
  }

  async pushData() {
    // push benefit data กลับไป HRMI
    // (inverse flow)
  }

Schedule — cronTab.controller.ts:14-30

ts
@Cron(CronExpression.EVERY_DAY_AT_1AM)
async handlePull() { ... }

@Cron(CronExpression.EVERY_DAY_AT_2_20AM)
async handlePush() { ... }
CronScheduleDirectionTarget
handlePull01:00 dailyHRMI → benefit-apireads dbHRMI_Center.*
handlePush02:20 dailybenefit-api → HRMIwrites dbHRMI_Center.*

ห่างกัน 80 นาที — พอดึง HRMI data ก่อน แล้ว push benefit result กลับ

Cron ถูก expose ผ่าน public endpoint — manualBatch.controller.ts

ts
@Public()
@Get('manualBatch/pull')
async manualPull() { return this.cronTab.pullData(); }

@Public()
@Get('manualBatch/push')
async manualPush() { return this.cronTab.pushData(); }

endpoint manualBatch/* เป็น @Public() → ใครก็ยิง trigger cron ได้ แม้ไม่ login

ดู 6.8 S7

pullData flow

pullData ครอบคลุม task 5 ประเภท:

TaskServiceSource table (HRMI)Target table (benefit)
WDHospitalhospitals.task.service.tsdbHRMI_Center.Hospitalsvtrc.WDHospitals (cross-schema write!)
Disasterdisaster.task.service.tsdbHRMI_Center.Disasterbenefit.Disaster*
StudyLeavestudyLeave.task.service.tsdbHRMI_Center.StudyLeavebenefit.StudyLeave
Inheritanceinheritance.task.service.tsdbHRMI_Center.Inheritancebenefit.Inheritance
Funeralfuneral.task.service.tsdbHRMI_Center.Funeralbenefit.Funeral

ทุก task ใช้ pattern:

  1. query HRMI ด้วย FORMAT(hd.ModifiedDate, 'yyyy-MM-dd') = '${bizDate}' (SQL injection pattern ดู 6.4)
  2. update target table

Puppeteer — PDF generation

package.json:25

25:25:benefit-api/package.json
    "puppeteer": "^21.3.6",

Puppeteer v21 สำหรับ render HTML → PDF

Usage — exportpaymentreport.service.ts:6700+

ts
const browser = await puppeteer.launch({
  args: ['--no-sandbox', '--disable-setuid-sandbox'],
});
const page = await browser.newPage();
await page.setContent(htmlString);
const pdf = await page.pdf({ format: 'A4', printBackground: true });
await browser.close();

ปัญหา: --no-sandbox เปิดที่ production — เสี่ยงถ้า HTML content มี script injection (แต่ HTML ถูกสร้างภายใน service ไม่ใช่ user-supplied — lower risk)

Chromium ใน Docker image

Dockerfile:18-25:

18:25:benefit-api/Dockerfile
RUN apk add --no-cache \
      nmap \
      chromium \
      nss \
      freetype \
      harfbuzz \
      ttf-freefont \
      ...
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser

ติดตั้ง Chromium สองรอบ (ครั้งแรกผ่าน apk, ครั้งที่สองผ่าน Puppeteer installer) — image ขยาย


ExcelJS — Excel export

package.json:14

14:14:benefit-api/package.json
    "exceljs": "^4.3.10",

ใช้สร้าง .xlsx จาก query result

Pattern — exportpaymentreport.service.ts:7417+

ts
const workbook = new ExcelJS.Workbook();
const sheet = workbook.addWorksheet('Report');
sheet.columns = [
  { header: 'Doc No', key: 'docNo', width: 20 },
  { header: 'Amount', key: 'amount', width: 15 },
  // ...
];
rows.forEach(r => sheet.addRow(r));
const buffer = await workbook.xlsx.writeBuffer();
return buffer;

File uploads — upload.controller.ts

ts
@Public()
@Post('upload')
@UseInterceptors(FileInterceptor('file', { storage: multerDiskStorage }))
async upload(@UploadedFile() file) {
  return { filename: file.filename, path: `/uploads/${file.filename}` };
}
  • endpoint @Public() → ใครก็ upload ได้
  • ใช้ multer.diskStorage — เขียน file ลง /uploads/ ใน container
  • ไม่มี MIME/extension whitelist ใน code ที่อ่าน
  • ไม่จำกัดขนาด file (limit ที่ตั้งคือ 50MB global body limit)

ดู 6.8 S8


vtrc-common library dependency

package.json:34:

34:34:benefit-api/package.json
    "vtrc-common": "file:../vtrc-common",

benefit-api ใช้ vtrc-common (shared library ที่อยู่นอก repo) สำหรับ utility — WelfareUtil (date format), type definitions

ts
import { WelfareUtil } from 'vtrc-common';
const bizDate = WelfareUtil.getCurrentDate();

vtrc-common ไม่ได้ version-pinned (ใช้ file: reference) — rebuild ในแต่ละ environment อาจได้ code ต่างกัน


Integration risk summary

IntegrationPatternRisk
SFTP (FMIS)Hardcoded passwordCritical — credential leak
HRMI pull/pushCron + raw SQLHigh — injection ใน bizDate filter
Puppeteer PDF--no-sandboxMedium — sandbox disabled
ExcelJS exportOKLow
File upload@Public(), no whitelistHigh — arbitrary file storage
vtrc-commonfile: referenceMedium — version drift

ไปต่อ