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_CenterSFTP — exportpaymentreport.service.ts
Credentials ใน source code — exportpaymentreport.service.ts:135-145
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',
],
},
};ปัญหา:
password: '~x-:#"nTb-2k+_TaE'hardcoded ใน source — ถูก commit เข้า gitusername: 'ftpfmis'แสดงว่าเป็น account เฉพาะ FMIS — ใครได้ source คือได้สิทธิ์ write/read files บน SFTPhost: '10.188.8.16'IP internal — leak topology
ดู 6.8 S3 สำหรับ remediation
SFTP library — ssh2-sftp-client
package.json:31:
"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
async pullData() {
// ดึง HRMI data จาก dbHRMI_Center schema
// update benefit.* tables
}
async pushData() {
// push benefit data กลับไป HRMI
// (inverse flow)
}Schedule — cronTab.controller.ts:14-30
@Cron(CronExpression.EVERY_DAY_AT_1AM)
async handlePull() { ... }
@Cron(CronExpression.EVERY_DAY_AT_2_20AM)
async handlePush() { ... }| Cron | Schedule | Direction | Target |
|---|---|---|---|
handlePull | 01:00 daily | HRMI → benefit-api | reads dbHRMI_Center.* |
handlePush | 02:20 daily | benefit-api → HRMI | writes dbHRMI_Center.* |
ห่างกัน 80 นาที — พอดึง HRMI data ก่อน แล้ว push benefit result กลับ
Cron ถูก expose ผ่าน public endpoint — manualBatch.controller.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 ประเภท:
| Task | Service | Source table (HRMI) | Target table (benefit) |
|---|---|---|---|
| WDHospital | hospitals.task.service.ts | dbHRMI_Center.Hospitals | vtrc.WDHospitals (cross-schema write!) |
| Disaster | disaster.task.service.ts | dbHRMI_Center.Disaster | benefit.Disaster* |
| StudyLeave | studyLeave.task.service.ts | dbHRMI_Center.StudyLeave | benefit.StudyLeave |
| Inheritance | inheritance.task.service.ts | dbHRMI_Center.Inheritance | benefit.Inheritance |
| Funeral | funeral.task.service.ts | dbHRMI_Center.Funeral | benefit.Funeral |
ทุก task ใช้ pattern:
- query HRMI ด้วย
FORMAT(hd.ModifiedDate, 'yyyy-MM-dd') = '${bizDate}'(SQL injection pattern ดู 6.4) - update target table
Puppeteer — PDF generation
package.json:25
"puppeteer": "^21.3.6",Puppeteer v21 สำหรับ render HTML → PDF
Usage — exportpaymentreport.service.ts:6700+
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:
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
"exceljs": "^4.3.10",ใช้สร้าง .xlsx จาก query result
Pattern — exportpaymentreport.service.ts:7417+
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
@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:
"vtrc-common": "file:../vtrc-common",benefit-api ใช้ vtrc-common (shared library ที่อยู่นอก repo) สำหรับ utility — WelfareUtil (date format), type definitions
import { WelfareUtil } from 'vtrc-common';
const bizDate = WelfareUtil.getCurrentDate();vtrc-common ไม่ได้ version-pinned (ใช้ file: reference) — rebuild ในแต่ละ environment อาจได้ code ต่างกัน
Integration risk summary
| Integration | Pattern | Risk |
|---|---|---|
| SFTP (FMIS) | Hardcoded password | Critical — credential leak |
| HRMI pull/push | Cron + raw SQL | High — injection ใน bizDate filter |
| Puppeteer PDF | --no-sandbox | Medium — sandbox disabled |
| ExcelJS export | OK | Low |
| File upload | @Public(), no whitelist | High — arbitrary file storage |
vtrc-common | file: reference | Medium — version drift |