7.1.3 · Persistence & integrations (V1 depth)
branch: prod-meeting-vtrc-api
บทนี้ลงลึก Sequelize models ทุกตาราง, ความสัมพันธ์, และ outbound integrations (vtrc-api, employee directory, SMTP, PDF, Tinify)
Persistence จริง — Sequelize + MSSQL
util/db.config.ts:1-25 สร้าง instance เดียว:
const SequelizeDB = require('sequelize');
const {env} = require('./env.ts');
const sequelizeDB = new SequelizeDB(env.database, env.username, env.password, {
host: env.host,
dialect: env.dialect,
port: env.port,
dialectOptions: {
options: {
useUTC: false,
encrypt: false,
}
},
timezone: env.timezone,
operatorsAliases: 0,
pool: {
max: env.pool.max,
min: env.pool.min,
acquire: env.pool.acquire,
idle: env.pool.idle
},
define: {
freezeTableName: true
}
});| Config | ค่า / ที่มา | หลักฐาน |
|---|---|---|
| dialect | process.env.DIALECT → mssql ใน .envprod | util/env.ts:11, .envprod |
| port | 1433 (prod placeholder) / 1436 (uat) | .envprod, .envuat |
| encrypt | false | db.config.ts:10 — ไม่ใช้ TLS ไปยัง MSSQL ตาม default tedious |
| timezone | hardcode Asia/Bangkok | env.ts:10 |
| pool | max 5, acquire 30000, idle 10000 | env.ts:12-17 |
| boot check | authenticate() เท่านั้น | db.config.ts:27-33 |
| sync/migrate | ไม่พบ .sync() หรือ migration folder | — |
Env โหลดจาก ./.env (env.ts:1-3) — ต้องมีขั้นตอน copy จาก .envdev/.envuat/.envprod (Docker: Dockerfile:24)
Credential ที่ comment ค้างใน env.ts:19-64 (host IP, password ตัวอย่าง) เป็นประวัติ environment เก่า — อย่าใช้เป็นค่าปัจจุบันโดยไม่ verify
Mongoose — ประกาศไว้แต่ตายแล้ว
util/mongo.config.ts:1-14 (ทั้งไฟล์ comment):
// const mongoose = require('mongoose')
// const portmongo = "27017";
// const dbconnect = '206.189.94.232';
// const username = 'dbuser'
// const password = 'databasebanzai'
// ...
// mongoose.connect('mongodb://'+username+':'+password+'@'+dbconnect+':'+portmongo+'/'+database+"?authSource=admin", ...)app.ts:8 ยัง require ไฟล์นี้ — side-effect ว่าง; credential ในคอมเมนต์เป็นของ MongoDB เก่า (อย่า rotate เป็นของ MSSQL โดยสับสน)
วิธีโหลด model
util/db.config.ts:39-43:
db.Approver = require('../model/approver/approver.ts')(sequelizeDB, SequelizeDB);
db.Group = require('../model/usergroup/usergroup.ts')(sequelizeDB, SequelizeDB);
db.Meeting = require('../model/meeting/meeting.ts')(sequelizeDB, SequelizeDB);
db.login = require('../model/login.ts')(sequelizeDB, SequelizeDB);db.* key | ไฟล์ | ตารางที่ export |
|---|---|---|
db.Approver | model/approver/approver.ts | Approver |
db.Group | model/usergroup/usergroup.ts | GroupMeeting, UserGroup |
db.Meeting | model/meeting/meeting.ts | 11 ตาราง (ด้านล่าง) |
db.login | model/login.ts | user, user_group |
Route ส่วนใหญ่ destructure จาก db.Meeting เช่น const { Meeting, Guest } = db.Meeting
ตาราง domain การประชุม — รายละเอียดฟิลด์
แหล่งเดียว: model/meeting/meeting.ts (565 บรรทัด) · associations ที่ :526-550
ทุก association ตั้ง constraints: false — Sequelize ไม่สร้าง FK จริงบน MSSQL; referential integrity พึ่ง application
Meeting (meeting.ts:236-314) · table name Meeting
| Field | Type | หมายเหตุ |
|---|---|---|
| id | INTEGER PK AI | |
| subject, location, detail | STRING | |
| approveID | INTEGER | ผู้อนุมัติ (emp-related id) |
| organizerID | STRING | |
| requireMeetingSummary | BOOLEAN | required |
| requireApprove | BOOLEAN | required |
| meetingURL | STRING | |
| startDate, endDate | DATE | default CURRENT_TIMESTAMP |
| documentDate | DATE | nullable — ใช้เป็น executeTime ของ job |
| isActive | INTEGER | 1=active, 2=draft, 0=deleted (จาก soft-delete) |
| isApproved | INTEGER | 0 pending / 1 approve / 2 reject / 3 after edit |
| remarkID | INTEGER | ชี้ Remark |
| created, updateDate | DATE | |
| summaryDueDate | DATE | nullable |
Guest (:3-64) · table Guest
| Field | Type | หมายเหตุ |
|---|---|---|
| id | INTEGER PK | |
| meetingID | INTEGER | ref Meeting (ORM only) |
| userID | STRING | empCode |
| isActive | BOOLEAN | default 1 |
| isStatusResponse | INTEGER | ref StatusResponse |
| isDelegate | BOOLEAN | default 0 |
| delegateID | INTEGER | ref Delegate |
| created, updateDate | DATE | |
| note | STRING |
ความหมาย isStatusResponse จาก flow ตอบรับ: 1=pending, 2=join, 3=reject, 4=delegate (acceptancemeeting.ts)
OtherGuest (:66-128) · table OtherGuest
แขกภายนอก: name, email, asRole + สถานะตอบรับเหมือน Guest
หมายเหตุ association bug: OtherGuest.belongsTo(OtherGuest, …) ที่ :531 — น่าจะตั้งใจ belongsTo(Meeting) — ส่งผลต่อ include ที่พึ่ง relation นี้อาจผิดพลาด
Attached (:130-183) · table Attached
| Field | หมายเหตุ |
|---|---|
| meetingID / summaryID | ผูกกับประชุมหรือสรุป |
| fileName, fileNameGen, fileType | |
| fileData | TEXT — เก็บ base64/path ซ้ำกับไฟล์บน disk |
Summary (:185-234) · table Summary
สรุปการประชุม: ไฟล์ + meetingID + userID + timestamps
StatusResponse (:316-332) · table StatusResponse
Lookup: id, statusName — seed อยู่นอกโค้ด (UNVERIFIED)
RemarkMeeting define name "Remark" (:334-362) · table Remark
detail, meetingID, createDate — ใช้ตอนอนุมัติมีหมายเหตุ
CommentSummaryMeeting define "Comment" (:364-403) · table Comment
changeDetail, status, meetingID, userID, timestamps — ความเห็นต่อสรุป
TokenMeeting define "Token" (:405-429) · table Token
| Field | ใช้ทำ |
|---|---|
| tokenMeeting | JWT ที่ออกให้ฝั่ง meeting |
| profileKey | ผูกกับ profile จาก vtrc |
| createDate |
ถูก stamp ใน checkbypasswhenredirect.ts:109-127 และอ่านใน checkbypass.ts:81-89
Delegate (:431-478) · table Delegate
delegateType (internal/external), meetingID, userID, name, email, asRole, timestamps
ScheduleJob (:480-525) · table ScheduleJob
| Field | หมายเหตุ |
|---|---|
| meetingID | |
| executeTime | เมื่อไหร่ควรรัน |
| jobType | เช่น "EMAIL" |
| jobStatus | ACTIVE / DONE / CANCLE |
| notiType | invite / required_approve / summary |
| created, updateDate |
Relation: Meeting.hasOne(ScheduleJob) (:549-550) — logic checkMeethingScheduleExist ถือว่าหนึ่ง meeting หนึ่ง job
ผัง relation (ORM metadata)
Meeting 1──N Guest
Meeting 1──N OtherGuest
Meeting 1──N Attached
Meeting 1──N Summary
Meeting 1──N Remark (RemarkMeeting)
Meeting 1──N Comment (CommentSummaryMeeting)
Meeting 1──1 ScheduleJob
Summary 1──N Attached
Delegate 1──1 Guest (via Guest.delegateID)
Delegate 1──1 OtherGuest
StatusResponse 1──1 Guest / OtherGuest (via isStatusResponse)หลักฐาน: model/meeting/meeting.ts:526-550
Approver + UserGroup
Approver (model/approver/approver.ts:1-41)
| Field | Type |
|---|---|
| id | INTEGER PK |
| empCode | STRING |
| sourceDB | STRING |
| orgCode | STRING |
| createDate, updateDate | DATE |
ใช้โดย route /meeting/secure/approver/* สำหรับ backoffice ตั้งรายชื่อผู้อนุมัติ
GroupMeeting + UserGroup (model/usergroup/usergroup.ts:1-76)
GroupMeeting 1──N UserGroup (groupID, constraints: false)| GroupMeeting | UserGroup |
|---|---|
| id, groupName, createDate, updateDate | id, userID, groupID, sourceDB, timestamps |
Route: /meeting/secure/group/* และ /meeting/secure/groupmeeting/* (duplicate legacy)
model/login.ts — นอก domain
ตาราง user / user_group มีฟิลด์เช่น user_gametype, user_register_channel, user_register_source, user_password (login.ts:56-71) — ลักษณะระบบเกม/community
โหลดตอน boot (db.config.ts:43) และถูก destructure ใน middleware/checkbypass.ts:3 (const { user, user_group } = db.login) แต่ ไม่พบการ query user ใน middleware นั้น — dead import ของตาราง
ไม่พบ route ที่ใช้ db.login เป็น persistence ของการประชุม — QUAL-MTVTRC-02
Integration ภายนอก
ตารางสรุป
| ปลายทาง | Client | Env / hardcode | ใช้ทำ | หลักฐาน |
|---|---|---|---|---|
| vtrc-api GraphQL | vtrcConnector | VTRC_BASE_CONNECT, header apiKey=API_KEY_OUTBOUND_VTRC | (1) profile ใน redirect middleware (2) push sendNotificationFrom3Party | vtrc.connector.ts:5-10, sendNoti.ts:81-98, checkbypasswhenredirect.ts:69-71 |
| Employee / "TwoWay" GraphQL | cdbConnector | CDB_BASE_CONNECT; header api-key hardcode; Bearer JWT hardcode ใน interceptor | fetchListEmployeeForTwoWay map ชื่อ-อีเมล | cdb.connector.ts:3-26 |
| SMTP Outlook | nodemailer | host/port hardcode; MAIL_PASS จาก env | อีเมลเชิญ + iCal | Emailsender.func.ts:54-65 |
| html-pdf-node / puppeteer | ใน container (CHROMIUM_PATH) | หนังสือเชิญ / ลายเซ็น | pdfgenbyhtml.func.ts, Dockerfile:4-6 | |
| Tinify | tinify SDK | key hardcode ในซอร์ส | ควรบีบอัดรูป — call ถูก comment | Uploadimage.class.ts:5,29-30 |
| Sentry | @sentry/node | DSN hardcode | error/tracing | app.ts:79-86 |
cdbConnector — hardcode ที่ต้องรู้
util/axiosinterceptors/cdb.connector.ts:3-26:
const cdbConnector = axios.create({
baseURL: process.env.CDB_BASE_CONNECT,
timeout: 10000,
headers: { 'api-key': 'eyJz-CI6Ikp-4pWY-lhdCI6' }
});
cdbConnector.interceptors.request.use((request) => {
request.headers.Authorization = 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9....';
return request;
});- api-key และ Bearer ฝังในซอร์ส (ไม่ใช่ env)
- ใน
.envuatCDB_BASE_CONNECTชี้http://uat-centralizeapi.redcross.or.th:25563/api-uat/graphql— สอดคล้องกับ centralize/employee API ไม่ใช่2way-meeting-backoffice
Query name fetchListEmployeeForTwoWay หมายถึง meeting-vtrc-api เป็นฝ่ายเรียกออก ไปหา employee directory ที่ทีมเรียกว่า TwoWay — ไม่ได้พิสูจน์ว่า 2way-meeting-backoffice เรียกเข้า service นี้
ตรวจใน 2way-meeting-backoffice ด้วย grep หา /meeting/secure / MEETING_API แล้วไม่พบ reference โดยตรงในรอบเอกสารก่อนหน้า — สรุป inbound จาก 2way-meeting-backoffice เป็น UNVERIFIED
vtrc push bridge (ย้ำจาก pipeline)
ดูรายละเอียด mutation ที่ 7.1.2 Push notification bridge — ที่นี่สรุปเฉพาะ transport:
- Timeout 10s (
vtrc.connector.ts:8) - ไม่มี response interceptor / retry
- ไม่มี Authorization Bearer บน connector (มีแต่ apiKey) — ต่างจาก commented sample ที่
:12-16
PDF generation
| ขั้น | ไฟล์ |
|---|---|
| Template EJS | util/pdftemplate/sign_join_meeting.ejs, layout.html.ejs |
| Orchestration | util/helper/createSignnature.helper.ts |
| Render | lib/pdfgenbyhtml.func.ts:6-26 → html-pdf-node.generatePdf |
| Alternate merge | pdfGeneraterAndReturnBufferDataNa (:28-56) — puppeteer.launch({ args: ["--no-sandbox", ...] }) + pdf-merger-js |
Docker ติดตั้ง Chromium และตั้ง PUPPETEER_EXECUTABLE_PATH (Dockerfile:4-6) — จำเป็นสำหรับ PDF ใน Alpine
--no-sandbox ลด isolation ของ Chromium — พื้นผิวเสี่ยงถ้ารender HTML จาก input ผู้ใช้โดยไม่ sanitize (UNVERIFIED ระดับ sanitize ของ EJS)
Fonts: fonts/ copy ไป /usr/share/fonts/kanit ใน image (Dockerfile:23)
File storage
| Path | บทบาท |
|---|---|
./storage/meeting/ | ไฟล์อัปโหลด + PDF |
HTTP /meeting/file | express.static (app.ts:147) — ไม่มี auth (guard ที่เคยมีถูก comment ที่ app.ts:90-103) |
| Compose volume | /home/hrappladmin/storage/meeting → /app/storage/meeting |
ไฟล์แนบเก็บทั้งบน disk และคอลัมน์ Attached.fileData — เพิ่มขนาด DB และความเสี่ยง data exfiltration ผ่าน API ที่คืนแถว Attached
Env inventory (ชื่อตัวแปรเท่านั้น — ห้ามคัดลอกค่า secret)
จาก .envprod / .envuat / .envdev (โครงสร้างคล้ายกัน):
| Var | บทบาท |
|---|---|
API_KEY | inbound x-api-key |
API_KEY_OUTBOUND_VTRC | header ไป vtrc-api (ชื่อในไฟล์อาจเป็น API_KEY_OUTBOUND — ตรวจให้ตรงตอน deploy) |
SECRET_JWT_VTRC / SECRET_JWT_MEETING | verify / sign ใน redirect middleware |
CDB_BASE_CONNECT | employee GraphQL |
VTRC_BASE_CONNECT | vtrc GraphQL |
MAIL_PASS | SMTP |
DATABASE / USERNAME / PASSWORD / HOST / PORT / DIALECT | Sequelize |
BASE_URL / FILE_PATH / PROJECT_URL | URL ประกอบลิงก์ไฟล์ |
.envuat มี password/host จริงใน plaintext ที่ commit แล้ว — ดู SEC-MTVTRC-01
สิ่งที่ไม่มีใน repo
- SQL migration / seed สำหรับ
StatusResponse - CI config (Bitbucket/GitHub Actions) ในโฟลเดอร์นี้ — UNVERIFIED pipeline ภายนอก
- Redis / queue อื่นนอก
ScheduleJobtable - WebSocket / GraphQL server ของตัวเอง (เป็น REST caller เท่านั้น)