Skip to content

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 | delegate

Flow 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

  1. checkFileType(reqData.Attached) — ถ้าไม่ผ่าน throw code 502 (meeting.ts:161-162)
  2. สร้างแถว Meeting:
    • isActive: Draft ? 2 : 1
    • isApproved: ApproverID ? 0 : 1
    • (meeting.ts:166-187)
  3. Guest[] จาก RedcrossGuest — organizer ที่อยู่ในรายชื่อได้ isStatusResponse: 2 (ตอบรับอัตโนมัติ) คนอื่น 1 (meeting.ts:189-201)
  4. OtherGuest[] จากแขกภายนอก (meeting.ts:203-216)
  5. Attached[]uploader() เขียนไฟล์ลง disk และเก็บ fileData (base64) ในตารางด้วย (meeting.ts:224-243)
  6. ถ้า 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 (ScheduleJobemailTask)

การส่งอีเมลเชิญไม่ synchronous ตอนสร้างประชุม — เขียนแถว ScheduleJob แล้วรอ executor

สร้างคิว — createScheduleJob

util/helper/corn.email.helper.ts:48-68

javascript
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.tsemail.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:

javascript
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 — ถ้า sendNoti fail งานถูก mark DONE แล้ว (push หาย)

sendMail chain (ชื่อไฟล์คล้ายกัน)

  1. util/helper/emailsender.ts — query Meeting + include Guest/OtherGuest/Attached, gen PDF, map user จาก cdb
  2. lib/Emailsender.func.ts:sendMailWhenAdd — สร้าง iCal (method REQUEST) ส่งผ่าน nodemailer

SMTP (lib/Emailsender.func.ts:54-65):

javascript
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หลักฐาน
approve1approvemeeting.ts:55-61
reject2:63-69
approveAfterEdit / approveafteredit3:71-77 (switch ใช้ _.toLower)

Side effects

  1. ถ้ามี Remark → insert Remark (RemarkMeeting) แล้วผูก Meeting.remarkID (:83-91, :141-147)
  2. ถ้า 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

statusisStatusResponseSide-effect
join2อัปเดต Guest/OtherGuest อย่างเดียว
reject3อัปเดตอย่างเดียว
delegate4insert Delegate + ถ้า internal insert Guest ใหม่ (isDelegate: 1)

Delegate internal vs external

  • Internal: มี Delegate.UserIDcheckDuplicateGuest (ซ้ำ → error 402) → insert Guest → insert Delegate (acceptancemeeting.ts:108-132)
  • External: ไม่มี UserID → delegateType: "external" เก็บ name/email/asRole ใน Delegate โดยไม่ verify กับระบบพนักงาน (:99-107)

อีเมลถึงตัวแทนถูกปิด

javascript
// let sendEmail = await sendMailToDelegate(data.MeetingID, email);

ที่ acceptancemeeting.ts:125 — ฟังก์ชัน sendMailToDelegate ยังมีอยู่ (:342-370 โดยประมาณ) แต่ไม่ถูกเรียก → ตัวแทนไม่ได้รับอีเมลเฉพาะตอนถูกมอบหมาย


Push notification bridge — sendNotivtrc-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
INVITEinviteทุก Guest.userID
REQUEST_APPROVErequired_approveMeeting.approveID คนเดียว
SUMMARYsummaryทุก Guest

Mutation ที่ยิงออก

sendNoti.ts:81-98:

javascript
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 ใส่ header apiKey: 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.notiTypecorn.email.helper.ts:113
(comment) ตอน add meetingINVITEmeeting.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

javascript
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/sendsendMail(4403)meeting.ts:73
/meeting/secure/health-check/emailส่งเมลทดสอบhealth-check.ts
/meeting/mockuser/oneคืน PIImockuser.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)