7.1.2 · Core pipeline (V1 depth)
branch: prod-meeting-vtrc-api
เอกสารนี้ไล่ flow หลักของระบบประชุมตั้งแต่ request เข้ามาจนถึง side-effect (DB, email, push) พร้อมรายละเอียด push notification bridge และ mock endpoint leak
Auth ระดับ global สำหรับ /meeting/secure/* ถูก comment ที่ app.ts:104-120 — flow ด้านล่างจึงรันได้โดยไม่ต้องมี Bearer เว้นแต่ caller ใส่เอง (และแม้ใส่ middleware ส่วนใหญ่ก็ไม่ได้ apply บน route)
ภาพรวม 1 รอบชีวิตการประชุม
POST /meeting/secure/meeting/addNewMeeting
│
├─► Meeting + Guest[] + OtherGuest[] + Attached[] (ไม่มี transaction)
│
└─► ถ้าไม่ draft และไม่มี ApproverID
└─► createScheduleJob(EMAIL, notiType=invite)
POST /meeting/secure/approvemeeting (ถ้ามี ApproverID)
└─► isApproved + optional Remark
└─► createScheduleJob(EMAIL, notiType=required_approve)
emailTask (cron หรือ GET /corn-email)
├─► sendMail(meetingID) → PDF + SMTP + iCal
├─► updateScheduleJob(DONE)
└─► sendNoti(meetingID, type) → vtrc-api GraphQL mutation
POST /meeting/secure/acceptancemeeting
└─► join | reject | delegateFlow 1 — สร้างการประชุม (addNewMeeting)
Endpoint: POST /meeting/secure/meeting/addNewMeeting
Mount: app.ts:150-151 · Handler: routes/secure/meeting/meeting.ts:29-49
Validation
express-validatorเช็คเฉพาะSubjectไม่ว่าง (meeting.ts:31)- ฟิลด์อื่น (
OrganizerID,RedcrossGuest,Attached, …) ไม่มี schema validation ก่อนเข้า business logic
ลำดับ insert
checkFileType(reqData.Attached)— ถ้าไม่ผ่าน throw code 502 (meeting.ts:161-162)- สร้างแถว
Meeting:isActive: Draft ? 2 : 1isApproved: ApproverID ? 0 : 1- (
meeting.ts:166-187)
Guest[]จากRedcrossGuest— organizer ที่อยู่ในรายชื่อได้isStatusResponse: 2(ตอบรับอัตโนมัติ) คนอื่น1(meeting.ts:189-201)OtherGuest[]จากแขกภายนอก (meeting.ts:203-216)Attached[]—uploader()เขียนไฟล์ลง disk และเก็บfileData(base64) ในตารางด้วย (meeting.ts:224-243)- ถ้า
Draft == falseและApproverIDว่าง →createScheduleJobด้วยNOTITYPE.INVITE(meeting.ts:244-255)
การเรียก sendMail / sendNoti ตรง ๆ ถูก comment ไว้ที่ meeting.ts:253-254 — ส่งผ่าน queue เท่านั้น
ไม่มี DB transaction
Meeting, Guest, OtherGuest, Attached เป็น insert แยกกัน ไม่มี sequelize.transaction() — ถ้า fail กลางทางจะเหลือ orphan meeting (CORR-MTVTRC-02)
Soft delete
DELETE /meeting/secure/meeting/deletemeeting ตั้ง isActive: 0 (meeting.ts:84-96) — ไม่ลบแถวจริง
Test hook ที่อันตราย
GET /meeting/secure/meeting/send เรียก sendMail(4403) แบบ hardcode meeting ID (meeting.ts:73-81) — ไม่มี auth; ใช้แล้วอาจส่งอีเมลจริงของ meeting นั้น
Flow 2 — คิวอีเมล + push (ScheduleJob → emailTask)
การส่งอีเมลเชิญไม่ synchronous ตอนสร้างประชุม — เขียนแถว ScheduleJob แล้วรอ executor
สร้างคิว — createScheduleJob
util/helper/corn.email.helper.ts:48-68
let params = {
meetingID: meetingData?.meetingID,
executeTime: reqData?.executeDate,
jobType: reqData?.jobType,
jobStatus: TASK_STATUS.ACTIVE, // "ACTIVE"
notiType: reqData?.notiType,
};
return await ScheduleJob.create(params);ถ้ามี ScheduleJob ของ meetingID นี้อยู่แล้ว (checkMeethingScheduleExist) จะ skip (:52-56, :135-149) — meeting เดิมแก้แล้วอยากส่งใหม่ต้องจัดการแถวเก่าเอง
สถานะ job enum (corn.email.helper.ts:7-16):
| ค่าในโค้ด | ความหมาย |
|---|---|
ACTIVE | รอประมวลผล |
DONE | เสร็จ |
CANCLE | ยกเลิก (สะกดผิดจาก CANCEL) |
ใครเรียก emailTask?
| ทาง | เรียกหรือไม่ | หลักฐาน |
|---|---|---|
scheduleEmail() ใน corn-job/email.corn.ts | นิยามไว้ ทุก 15 นาที (JOB_SCHEDULE) แต่ ไม่ถูกเรียกจาก app.ts | email.corn.ts:6-12, app.ts:4 |
GET /meeting/secure/corn-email/ | เรียก executeTask("email") โดยตรง | corn-email.ts:10-14 |
checkScheduleJob — ขอบเขตเวลา
corn.email.helper.ts:19-47 ดึงงานที่:
jobStatus LIKE 'ACTIVE'executeTime > TODAY_STARTและexecuteTime < NOW
งานที่ executeTime เป็นของวันก่อนหน้าจะไม่ถูกดึง (CORR-MTVTRC-04)
Loop ประมวลผล
corn.email.helper.ts:103-121:
for (const element of result) {
await sendMail(element.meetingID);
await updateScheduleJob({ meetingID: element.meetingID, jobStatus: DONE });
await sendNoti(element.meetingID, element.notiType);
}- ไม่มี try/catch รายตัว — งานหนึ่ง fail ทำให้งานถัดไปในรอบเดียวกันหยุด
- ลำดับ: เมลก่อน → mark DONE → ค่อย push — ถ้า
sendNotifail งานถูก mark DONE แล้ว (push หาย)
sendMail chain (ชื่อไฟล์คล้ายกัน)
util/helper/emailsender.ts— queryMeeting+ include Guest/OtherGuest/Attached, gen PDF, map user จาก cdblib/Emailsender.func.ts:sendMailWhenAdd— สร้าง iCal (method REQUEST) ส่งผ่าน nodemailer
SMTP (lib/Emailsender.func.ts:54-65):
const transporter = nodemailer.createTransport({
host: "smtp-mail.outlook.com",
port: 587,
auth: {
user: "noreply-vtrc@redcross.or.th",
pass: process.env.MAIL_PASS,
},
});Employee profile ผ่าน GraphQL fetchListEmployeeForTwoWay บน cdbConnector (ดู 7.1.3)
Flow 3 — อนุมัติการประชุม
Endpoint: POST /meeting/secure/approvemeeting/routes/secure/meeting/approvemeeting.ts:24-46 · mount app.ts:176-177
Status → isApproved
Status (body) | isApproved | หลักฐาน |
|---|---|---|
approve | 1 | approvemeeting.ts:55-61 |
reject | 2 | :63-69 |
approveAfterEdit / approveafteredit | 3 | :71-77 (switch ใช้ _.toLower) |
Side effects
- ถ้ามี
Remark→ insertRemark(RemarkMeeting) แล้วผูกMeeting.remarkID(:83-91,:141-147) - ถ้า approve / approveafteredit → คำนวณ
executeDate(วันนี้ หรือdocumentDateถ้ายังไม่ถึง) แล้วcreateScheduleJobด้วยNOTITYPE.REQUEST_APPROVE(:92-111)
Authorization gap
Handler ไม่ตรวจว่า caller คือ approveID ของ meeting — ไม่มี middleware auth บน route นี้; สิทธิ์พึ่งชั้นหน้าบ้านเท่านั้น (UNVERIFIED ว่ามี proxy ชั้นนอกบังคับ auth)
Flow 4 — ตอบรับ / ปฏิเสธ / มอบหมายตัวแทน
Endpoint: POST /meeting/secure/acceptancemeeting/routes/secure/meeting/acceptancemeeting.ts:31-60 · mount app.ts:178-179
status | isStatusResponse | Side-effect |
|---|---|---|
join | 2 | อัปเดต Guest/OtherGuest อย่างเดียว |
reject | 3 | อัปเดตอย่างเดียว |
delegate | 4 | insert Delegate + ถ้า internal insert Guest ใหม่ (isDelegate: 1) |
Delegate internal vs external
- Internal: มี
Delegate.UserID→checkDuplicateGuest(ซ้ำ → error 402) → insert Guest → insert Delegate (acceptancemeeting.ts:108-132) - External: ไม่มี UserID →
delegateType: "external"เก็บ name/email/asRole ในDelegateโดยไม่ verify กับระบบพนักงาน (:99-107)
อีเมลถึงตัวแทนถูกปิด
// let sendEmail = await sendMailToDelegate(data.MeetingID, email);ที่ acceptancemeeting.ts:125 — ฟังก์ชัน sendMailToDelegate ยังมีอยู่ (:342-370 โดยประมาณ) แต่ไม่ถูกเรียก → ตัวแทนไม่ได้รับอีเมลเฉพาะตอนถูกมอบหมาย
Push notification bridge — sendNoti → vtrc-api
ไฟล์กลาง: util/helper/sendNoti.ts
Client: util/axiosinterceptors/vtrc.connector.ts
สิ่งที่ service นี้ทำ / ไม่ทำ
| ทำ | ไม่ทำ |
|---|---|
POST GraphQL mutation sendNotificationFrom3Party | ยิง FCM / APNs / LINE ตรง |
ส่ง users เป็นรายการ { empCode } | เก็บ receipt / retry ของ push เอง |
ตั้ง notificationGroup: "NEWS" | ตั้ง redirect_path (ว่างเสมอ) |
NOTITYPE enum
sendNoti.ts:16-25:
| Constant | ค่า string | ผู้รับใน payload |
|---|---|---|
INVITE | invite | ทุก Guest.userID |
REQUEST_APPROVE | required_approve | Meeting.approveID คนเดียว |
SUMMARY | summary | ทุก Guest |
Mutation ที่ยิงออก
sendNoti.ts:81-98:
const mock = {
users: sendData["empCode"],
title: sendData["title"],
body: sendData["body"],
notificationGroup: "NEWS",
redirect_path: "",
};
const grahpQlSetBody = {
mutaionByUser: {
query: `mutation sendNotificationFrom3Party($notifications : [NotificationLists!]!){
sendNotificationFrom3Party(notifications : $notifications){
status
}
}`,
variables: {
notifications: mock,
},
},
};
await vtrcConnector.post("/", grahpQlSetBody.mutaionByUser);หมายเหตุจากซอร์ส:
- ตัวแปรชื่อ
mock/grahpQlSetBody/mutaionByUser— typo คงไว้ตามโค้ด vtrcConnectorใส่ headerapiKey: process.env.API_KEY_OUTBOUND_VTRC(vtrc.connector.ts:3-9)baseURL=VTRC_BASE_CONNECT(ใน.envuatชี้https://uat-vtrcapi.redcross.or.th/api-uat/graphql)
นี่คือหลักฐานว่า push ของระบบประชุมเป็น downstream ของ legacy vtrc-api ไม่ใช่ผู้ deliver เอง
จุดเรียก sendNoti
| จุด | notiType | หลักฐาน |
|---|---|---|
emailTask หลังส่งเมล | จากคอลัมน์ ScheduleJob.notiType | corn.email.helper.ts:113 |
| (comment) ตอน add meeting | INVITE | meeting.ts:254 |
Summary notification type มีใน enum แต่ต้องมี caller อื่นสร้าง job ด้วย notiType: summary — ตรวจใน flow summary แยกเมื่อ maintain
Auth bridge — redirect จาก backoffice
Endpoint: GET /meeting/redirectbypass/routes/auth/redirectbypass.ts:22 · mount app.ts:212-213
ลำดับ (middleware/checkbypasswhenredirect.ts):
Bearer token (จาก vtrc-api)
│
▼ decodeJwt(token, SECRET_JWT_VTRC)
│
▼ vtrcConnector POST fetchEmployeeProfileForOtherService
│
▼ jwt.sign(profile, SECRET_JWT_MEETING, { expiresIn: "48h" })
│
▼ TokenMeeting.create/update (tokenMeeting + profileKey)
│
▼ response { token, profile, pathRedirectForWeb: "" }ต้องมี x-api-key ตรง API_KEY ด้วย — ไม่เช่นนั้น 400
จุดนี้คือสะพานที่ meeting-backoffice ใช้แลก session สำหรับ UI จัดการประชุม (cross-ref Vol 7.2)
Mock endpoint leak — GET /meeting/mockuser/one
Mount: app.ts:209-210 · Handler: routes/mockdelatlast/mockuser.ts:29-60
route.get("/one", async (req, res) => {
const mockdata = {
user: [
{
userID: 5596233,
name: "วินัย ตั้งสกุล",
email: "viani@redcross.or.th",
tel: "0976564322",
},
// ... อีก 2 รายการ PII จริงในซอร์ส
],
};
res.status(200).send(mockdata);
});ข้อเท็จจริง:
- ไม่มี auth middleware
- ไม่อยู่ใต้
/secure/แต่เข้าถึงได้จาก internet ถ้าระบบ expose port/path นี้ - ข้อมูลเป็นชื่อ-อีเมล-เบอร์ที่ดูเหมือนพนักงานจริง — ไม่ใช่ faker
- โฟลเดอร์ชื่อ
mockdelatlast(typo จาก delta/at last)
จัดเป็น SEC-MTVTRC-02 Critical ใน 7.1.4
Health-check ที่ส่งอีเมลจริง
GET /meeting/secure/health-check/email (health-check.ts:9-17):
- ไม่มี auth
- hardcode
namfon.k@i3gateway.com - เรียก
healthCheckEmailจากlib/Emailsender.func.ts→ ส่ง SMTP จริงทุกครั้งที่ถูกยิง
จัดเป็น SEC-MTVTRC-03
Manual ops endpoints (ไม่มี auth)
| Path | ผล | File |
|---|---|---|
/meeting/secure/corn-email/ | รัน emailTask ทั้งคิว ACTIVE ของวันนี้ | corn-email.ts |
/meeting/secure/meeting/send | sendMail(4403) | meeting.ts:73 |
/meeting/secure/health-check/email | ส่งเมลทดสอบ | health-check.ts |
/meeting/mockuser/one | คืน PII | mockuser.ts |
ในสภาพที่ global auth ถูกปิด endpoints เหล่านี้เป็นพื้นผิวโจมตี / abuse ได้โดยตรง
สรุป dependency ระหว่าง flow
addNewMeeting ──(no Approver)──► ScheduleJob(invite)
│
└──(has Approver)──► approvemeeting ──► ScheduleJob(required_approve)
│
▼
corn-email / (intended cron) ──► sendMail + sendNoti
│
└──► vtrc-api push
acceptancemeeting ──► Guest/Delegate only (email to delegate disabled)