Skip to content

2way-vtrc-api — Core Pipeline

บทนี้อธิบาย lifecycle ของ HTTP request ใน 2way-vtrc-api (branch: prod) จาก edge จนถึง DB / LINE / cu-central-api — พร้อม trace ที่พิสูจน์จากซอร์ส

ดูแผนที่ route ที่ 01-repository-map และ persistence ที่ 03-persistence-integrations


Request lifecycle ภาพรวม

ต่างจาก 2way-api (NestJS global JwtAuthGuard) — repo นี้ ไม่มี global auth. แต่ละไฟล์ route เลือก middleware เอง

Client (LIFF / backoffice / LINE platform)


Express app (app.ts)
  │  Sentry requestHandler + tracingHandler
  │  static /2way/file
  │  bodyParser json 50mb
  │  CORS Origin=* + Credentials=true

Router mount (app.use('/2way/...', routeFile))


Per-route middleware (optional)
  │  checkBypassTokenMiddleWare   — admin / backoffice style
  │  checkBypassTokenLineMiddleWare — LIFF / LINE access token
  │  checkApiKeyMiddleWare        — x-api-key (import บ่อย แต่ไม่ได้ใส่ใน chain หลายไฟล์)
  │  express-validator            — บาง endpoint
  │  หรือ ไม่มี middleware เลย

Handler async (req, res)
  │  Sequelize queries / axios outbound / LINE push

res.status(200).send({ status: { code, message }, ... })
  │  หมายเหตุ: หลาย handler ส่ง HTTP 200 แม้ business error

Sentry errorHandler (registered early — ดูด้านล่าง)

หลักฐาน CORS: 2way-vtrc-api/app.ts:87-99
หลักฐาน body limit: app.ts:83-84

ลำดับ Sentry handlers ที่ผิดปกติ

ใน app.ts มีการลงทะเบียน:

typescript
app.use(Sentry.Handlers.requestHandler());
app.use(Sentry.Handlers.tracingHandler());
app.use(Sentry.Handlers.errorHandler());  // ก่อน route mounts

แหล่ง: app.ts:74-77

ตามเอกสาร Sentry สำหรับ Express — errorHandler ควรอยู่ หลัง controllers. ที่นี่ถูกใส่ก่อน app.use ของ routes ทั้งหมด (app.ts:101+) จึงอาจไม่จับ error จาก route handlers ตามที่ตั้งใจ — route หลายไฟล์เรียก Sentry.captureException(error) ตรงใน catch แทน (เช่น createIncident.ts:32)


Auth middleware ลึก — สามชั้นที่ต้องแยกให้ชัด

1) checkBypassTokenMiddleWare (backoffice / admin token)

ไฟล์: middleware/checkbypasstoken.ts

typescript
if (data && data.startsWith("Bearer ")) {
  const token = authHeader.substring(7, authHeader.length);
  const dataRQ = await requestCheckInVtrcApi(token);
  if (dataRQ?.confirmBypass || token == "testnaja") {
    req.confirmBypass = true;
    next();
  } else {
    res.status(400).send(tailResponse(400, "no Token or Permission [Bearer]"));
  }
}

แหล่ง: checkbypasstoken.ts:19-31

เส้นทาง verify ปกติ (requestCheckInVtrcApi):

  1. decodeJwt(token, process.env.SECRET_JWT_TWOWAY) (checkbypasstoken.ts:65)
  2. ถ้า decode ได้ → Token.findOne({ where: { tokenTwoWay: { [Op.like]: token }, profileKey } }) (checkbypasstoken.ts:101-108)
  3. ถ้าพบแถว → { confirmBypass: true, loginType }

ฟังก์ชันชื่อ requestCheckInVtrcApi แต่ ไม่ได้ยิง HTTP ไป vtrc-api ในโค้ดปัจจุบัน — มี GraphQL query string fetchEmployeeProfileForOtherService อยู่ใน object mapTypeRequest (checkbypasstoken.ts:45-60) แต่ไม่ถูกเรียกใช้ใน flow ที่อ่านได้

Master key: token == "testnaja" ผ่านทันทีโดยไม่ต้องมีแถวใน token table — ดู 04-scorecard SEC-2WAYVTRC-01

checkApiKeyMiddleWare เทียบ req.header("x-api-key") กับ process.env.API_KEY (checkbypasstoken.ts:115-125) — หลายไฟล์ import แต่ไม่ได้ใส่ใน route.post chain

2) checkBypassTokenLineMiddleWare (LIFF / LINE user)

ไฟล์: middleware/checkByPassTokenLine.ts

typescript
const dataRQ = await verifyLineTokenAndGetProfile(token);
if (dataRQ?.status || token == "testnaja") {
  req.userId = dataRQ.data?.userId;
  next();
}

แหล่ง: checkByPassTokenLine.ts:28-33

verifyLineTokenAndGetProfile:

  1. GET https://api.line.me/oauth2/v2.1/verify?access_token=... (checkByPassTokenLine.ts:50)
  2. GET https://api.line.me/v2/profile ด้วย Bearer (checkByPassTokenLine.ts:55-56)
  3. UserLine.findOne({ where: { lineID: profile.userId } }) (checkByPassTokenLine.ts:60-65)
  4. ถ้ามี user ใน DB → status: true; ไม่มี → status: false ข้อความ "Token Expire" (ชื่อ message ไม่ตรงสาเหตุจริง)

Master key ซ้ำ: "testnaja" ผ่านที่นี่ด้วย (checkByPassTokenLine.ts:30)

Retry helper fetchGetAlways ลองสูงสุด 3 ครั้ง (checkByPassTokenLine.ts:198-230)

3) requireJWTAuth (passport-jwt) — เกือบเป็น no-op

typescript
const jwtAuth = new JwtStrategy(jwtOptions, async (payload, done) => {
    done(null, true, { datafromauth: "user" });
});

แหล่ง: middleware/jwtpassport.ts:9-12

Strategy ไม่เคย reject จาก payload — แค่ signature ถูกด้วย SECRET จาก util/jwt.config.ts ก็ผ่าน. ค่า SECRET เป็น string literal ไม่ผ่าน env (jwt.config.ts:2). ค่า jwtExpiration: 60 / jwtRefreshExpiration: 120 เป็นค่า test ที่ยัง active (jwt.config.ts:7-8)


Response envelope มาตรฐาน

Handler ส่วนใหญ่ใช้ helper รูปแบบเดียวกัน:

typescript
const tailResponse = (code: number, message: string) => {
  return {
    status: {
      code: code,
      message: message
    }
  }
}

ตัวอย่าง: createIncident.ts:272-278, getProfile.ts:165-171

Convention ในโค้ด: code: 1000 = success เชิงธุรกิจ; 404 / 400 / 500 = ผิดพลาด — แต่ HTTP status มักเป็น 200 แม้ business fail (เช่น createIncident.ts:30-34 ส่ง res.status(200).send(err) ใน catch)


Trace A — POST /2way/addNewIncident (พนักงาน/แอดมินแจ้งปัญหา)

1) Route mount

typescript
const createIncident = require('./routes/TwoWayAPI/incident/createIncident');
app.use('/2way/addNewIncident', createIncident);

แหล่ง: app.ts:121-122

2) Middleware chain

typescript
route.post("/", checkBypassTokenMiddleWare, validatorCreateIncident, async (req, res) => {

แหล่ง: createIncident.ts:22

ต้องผ่าน bypass token (หรือ "testnaja") แล้วผ่าน express-validator

3) Business logic — queryAddNewIndicent

ลำดับ insert (ไม่ครอบ transaction):

ขั้นการกระทำบรรทัด
1ตรวจ IncidentType ตาม incidentTypeIDcreateIncident.ts:41-44
2ตรวจ ProgressStatus ตาม progressStatusIDcreateIncident.ts:46-49
3ถ้ามี remarkRemark.bulkCreatecreateIncident.ts:54-62
4สร้างเลขรันนิ่ง INC.xxxxx ผ่าน setRunningNo()createIncident.ts:65-72, 243-269
5IncidentRunning.bulkCreatecreateIncident.ts:71
6Incident.bulkCreatecreateIncident.ts:91
7อัปเดต Remark.incidentID ย้อนกลับcreateIncident.ts:93-98
8ถ้ามี attachment → Uploadimage.uploader + Attachment.bulkCreatecreateIncident.ts:101-127
9ถ้ามี lineID → LINE pushcreateIncident.ts:130-143

setRunningNo นับ Incident.count + 1 แล้ว pad เป็น INC.00001 รูปแบบ แล้ววนเช็คซ้ำใน IncidentRunning (createIncident.ts:243-269) — race condition ได้ถ้าสอง request พร้อมกัน

4) LINE side-effect

typescript
let endpoint = 'https://api.line.me/v2/bot/message/push'
let resData = await fetchAlways(endpoint, text, 1)

แหล่ง: createIncident.ts:142-143
ข้อความแจ้งผู้ใช้ว่าทีม Support ได้รับเรื่องแล้ว ภายใน 3 วันทำการ (createIncident.ts:136)

fetchAlways import จาก routes/WebHookApi/triggerMenu (createIncident.ts:16)

5) ความเสี่ยง correctness

  • ไม่มี sequelize.transaction() — orphan Remark / IncidentRunning ได้ถ้า fail กลางทาง — CORR-2WAYVTRC-01
  • attachment.map(async ...) ไม่ await Promise.all — insert ไฟล์อาจยังไม่เสร็จก่อน response Success (createIncident.ts:104-127)
  • Error ส่งกลับ HTTP 200 (createIncident.ts:34)

Trace B — POST /2way/getProfile (ดึงโปรไฟล์จาก lineID) — ไม่มี auth

1) Route mount

typescript
const getProfile = require('./routes/TwoWayAPI/userLine/getProfile');
app.use('/2way/getProfile', getProfile);

แหล่ง: app.ts:236-237

2) Auth ที่ถูกถอดออก

โค้ดที่เคยป้องกัน (ยังเห็นใน comment):

typescript
// route.post("/", checkBypassTokenLineMiddleWare, async function(req: any, res: any) {

แหล่ง: getProfile.ts:34-48

ตัวที่ live:

typescript
route.post("/", async function (req: any, res: any) {
  const reqData = req.body;
  const userId = reqData.lineID;

แหล่ง: getProfile.ts:49-52

3) Query + enrich จาก cu-central-api

  1. UserLine.findOne({ where: { lineID: userId } }) (getProfile.ts:70-75)
  2. คืน id, userID, identityCard, birthDate, lineID, isActive (getProfile.ts:80-87)
  3. axios POST GraphQL ไป https://centralizeapi.redcross.or.th:25563/api/graphql query fetchListEmployeeForTwoWay (getProfile.ts:96-120)
  4. เติม fullName + tel จากผลลัพธ์ (getProfile.ts:127-133)

4) ผลกระทบ

ใครก็ตามที่ส่ง { "lineID": "U...." } ได้ PII ของพนักงานที่ผูก LINE นั้นโดยไม่ต้องมี Bearer — SEC-2WAYVTRC-02

5) ตัวแปร CU — พฤติกรรมเดียวกัน

POST /2way/getProfile/cu (app.ts:183-184) ใน userLineCu/getProfile.ts:50-63 ก็ถอด checkBypassTokenLineMiddleWare ออกเช่นกัน — debt เดียวกันสอง path


Trace C — POST /2way/updateIncident — ไม่มี auth middleware

typescript
route.post("/", validatorUpdateIncident, async (req, res) => {

แหล่ง: updateIncident.ts:23

มีแค่ validator — ไม่มี checkBypassTokenMiddleWare. ใครก็ตามที่เข้าถึง network ได้สามารถอัปเดตสถานะ incident ได้ถ้าส่ง body ผ่าน validation — SEC-2WAYVTRC-07

Logic สำคัญ: ถ้า progressStatusID == 4 แล้ว (closed) จะไม่ให้อัปเดตต่อ (updateIncident.ts:50)


Trace D — POST /2way/loginSSO (SSO non-employee / backoffice)

1) Mount

app.ts:223-224routes/TwoWayAPI/sso/loginSSO.ts

2) ไม่มี auth บน endpoint (ตั้งใจ — เป็น login)

typescript
route.post("/", async (req, res) => {

แหล่ง: loginSSO.ts:29

3) Flow

username/password จาก body


checkEmployeeCD(username)
  │  GraphQL fetchListEmployeeForTwoWay ที่ centralizeapi:25563
  │  ถ้าพบว่าเป็นพนักงาน CD → ปฏิเสธ (return 500 "Username or Password is incorrect.")
  │  loginSSO.ts:58-80

ssoLogin(username, password)
  │  ยิงไป SSO ที่ IP ภายใน + AES encrypt
  │  credentials hardcode ในไฟล์ (ดู 03)

signNewJwt(sso) → stampDBToken(newJwt) ลงตาราง token


คืน { isSuccess, token }

Hardcoded constants ที่หัวไฟล์:

typescript
const SSO_END_POINT='http://10.188.127.131/SSOService/api'
const SSO_APP_ID='VTRCBO'
const SSO_PASSWORD='P@ssw0rd'
const SSO_USERNAME='user'
const SSO_REQUEST_ID='SSO220607000000031'
const AES_SECRET_KEY='EoABvcD7RsFBeKRZgSOHS4LLD08To6Pc'
const AES_INIT_VECTOR='AaNZRKzytx9yCAiA'

แหล่ง: loginSSO.ts:21-27SEC-2WAYVTRC-08


Trace E — LINE webhook POST /2way/WebHookApi/Line

Mount: app.ts:114-115routes/WebHookApi/index.ts

typescript
route.post('/Line', async function (req: RequestLineWebHook, res) {

แหล่ง: WebHookApi/index.ts:251

Flow โดยสรุป (ไฟล์ยาว ~566 บรรทัด):

  1. รับ payload ตามโครงสร้าง LINE webhook (RequestLineWebHook interface ที่ index.ts:23-93)
  2. แยกประเภท event / message
  3. เรียก helpers จาก triggerMenu.ts (triggerText, triggeImg) และ changeRich.ts (nextPageLine, previousPageLine) สำหรับ rich menu navigation
  4. Authorization header สำหรับ LINE API สร้างจาก process.env.LINE_AUTH (WebHookApi/index.ts:18-21)

มี endpoint เสริม POST /Line/checkToken (index.ts:343) สำหรับตรวจ token

คู่ขนานฝั่ง CU: /2way/cu/webHookApiWebHookCuApi/index.ts (app.ts:116-117) ใช้ env LINE_CU_* จาก .env.prod

รายละเอียด rich menu flow เชิงบริการแยกดูที่เอกสาร 2way-line-service — webhook ใน repo นี้อยู่คู่กับ logic ใน 2way-line-service แต่เป็นคนละ process


Trace F — Socket.io chat (โครงมี — Mongo ปิด)

typescript
export const io = require('socket.io')(server, { cors: { origin: '*' } });
const chatHandler = require("./lib/Socket.func");
io.on("connection", onConnection);

แหล่ง: app.ts:270-282

ใน lib/Socket.func.ts:

  1. Event sendMessage (Socket.func.ts:32)
  2. ถ้า image/video/pdf → อัปโหลดแล้วเปลี่ยน body.msg เป็น URL https://vtrcdev.redcross.or.th/2way/file/... (Socket.func.ts:67-75) — hardcode dev hostname
  3. Chat.findOne({ channel: body.lineID }) แล้ว chat.save (Socket.func.ts:85-100)
  4. emit returnAdminMessage{lineID} กลับ client

เพราะ mongo.config.ts ไม่ถูก require — Mongoose ไม่มี connection จาก entrypoint นี้ → write จะ fail หรือ hang — CORR-2WAYVTRC-02

HTTP chat routes ถูก comment ปิดแล้ว (app.ts:245-250) แต่ socket path ยังเปิด


Trace G — Mock path POST /2way/TwoWayAPI/RegisterLineUser

typescript
const data = req.body
const result = isEmpty(data);
if (result) {
  res.status(200).send({ status: { code: "1000", message: "Success" } });
} else {
  throw "Data is not defined and not null";
}

แหล่ง: routes/TwoWayAPI/index.ts:239-251

isEmpty ผ่านเมื่อ body ไม่ว่าง — ไม่ validate ฟิลด์ ไม่เขียน DB — ตอบ Success เสมอถ้ามี object ใด ๆ — QUAL-2WAYVTRC-01


เปรียบเทียบ auth กับ 2way-api (NestJS)

หัวข้อ2way-vtrc-api (repo นี้)2way-api
Global guardไม่มีJwtAuthGuard + @Public()
Master bypass string"testnaja" ใน 2 middlewarebackdoor user i3admin ใน auth.service (คนละรูปแบบ)
Token storeตาราง Sequelize tokenตรวจ JWT signature เป็นหลัก
Unauth PII endpointgetProfile / getProfile/cu พิสูจน์แล้วดู scorecard ของ 2way-api แยก
Runtimets-node ตรงnode dist/... หลัง compile

ห้ามสรุปว่าสอง repo มี auth model เดียวกัน — consumer ต้องดูว่าเรียก host/path ไหน


Error handling patterns ที่พบบ่อย

Patternตัวอย่างผล
res.status(200).send(err) ใน catchcreateIncident.ts:34client ต้องอ่าน status.code ไม่ใช่ HTTP status
throw 'INTERNAL_SERVER_ERROR' stringhelper insert หลายไฟล์ไม่ใช่ Error object — stack หาย
Sentry.captureException ใน catchcreateIncident.ts:32ชดเชย errorHandler ที่ลงทะเบียนผิดจังหวะ
console.log token/headerscheckbypasstoken.ts:16-17เสี่ยง leak credential ใน log

สิ่งที่อยู่นอกขอบเขตบทนี้