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 มีการลงทะเบียน:
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
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):
decodeJwt(token, process.env.SECRET_JWT_TWOWAY)(checkbypasstoken.ts:65)- ถ้า decode ได้ →
Token.findOne({ where: { tokenTwoWay: { [Op.like]: token }, profileKey } })(checkbypasstoken.ts:101-108) - ถ้าพบแถว →
{ 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
const dataRQ = await verifyLineTokenAndGetProfile(token);
if (dataRQ?.status || token == "testnaja") {
req.userId = dataRQ.data?.userId;
next();
}แหล่ง: checkByPassTokenLine.ts:28-33
verifyLineTokenAndGetProfile:
- GET
https://api.line.me/oauth2/v2.1/verify?access_token=...(checkByPassTokenLine.ts:50) - GET
https://api.line.me/v2/profileด้วย Bearer (checkByPassTokenLine.ts:55-56) UserLine.findOne({ where: { lineID: profile.userId } })(checkByPassTokenLine.ts:60-65)- ถ้ามี 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
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 รูปแบบเดียวกัน:
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
const createIncident = require('./routes/TwoWayAPI/incident/createIncident');
app.use('/2way/addNewIncident', createIncident);แหล่ง: app.ts:121-122
2) Middleware chain
route.post("/", checkBypassTokenMiddleWare, validatorCreateIncident, async (req, res) => {แหล่ง: createIncident.ts:22
ต้องผ่าน bypass token (หรือ "testnaja") แล้วผ่าน express-validator
3) Business logic — queryAddNewIndicent
ลำดับ insert (ไม่ครอบ transaction):
| ขั้น | การกระทำ | บรรทัด |
|---|---|---|
| 1 | ตรวจ IncidentType ตาม incidentTypeID | createIncident.ts:41-44 |
| 2 | ตรวจ ProgressStatus ตาม progressStatusID | createIncident.ts:46-49 |
| 3 | ถ้ามี remark → Remark.bulkCreate | createIncident.ts:54-62 |
| 4 | สร้างเลขรันนิ่ง INC.xxxxx ผ่าน setRunningNo() | createIncident.ts:65-72, 243-269 |
| 5 | IncidentRunning.bulkCreate | createIncident.ts:71 |
| 6 | Incident.bulkCreate | createIncident.ts:91 |
| 7 | อัปเดต Remark.incidentID ย้อนกลับ | createIncident.ts:93-98 |
| 8 | ถ้ามี attachment → Uploadimage.uploader + Attachment.bulkCreate | createIncident.ts:101-127 |
| 9 | ถ้ามี lineID → LINE push | createIncident.ts:130-143 |
setRunningNo นับ Incident.count + 1 แล้ว pad เป็น INC.00001 รูปแบบ แล้ววนเช็คซ้ำใน IncidentRunning (createIncident.ts:243-269) — race condition ได้ถ้าสอง request พร้อมกัน
4) LINE side-effect
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()— orphanRemark/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
const getProfile = require('./routes/TwoWayAPI/userLine/getProfile');
app.use('/2way/getProfile', getProfile);แหล่ง: app.ts:236-237
2) Auth ที่ถูกถอดออก
โค้ดที่เคยป้องกัน (ยังเห็นใน comment):
// route.post("/", checkBypassTokenLineMiddleWare, async function(req: any, res: any) {แหล่ง: getProfile.ts:34-48
ตัวที่ live:
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
UserLine.findOne({ where: { lineID: userId } })(getProfile.ts:70-75)- คืน
id,userID,identityCard,birthDate,lineID,isActive(getProfile.ts:80-87) - axios POST GraphQL ไป
https://centralizeapi.redcross.or.th:25563/api/graphqlqueryfetchListEmployeeForTwoWay(getProfile.ts:96-120) - เติม
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
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-224 → routes/TwoWayAPI/sso/loginSSO.ts
2) ไม่มี auth บน endpoint (ตั้งใจ — เป็น login)
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 ที่หัวไฟล์:
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-27 — SEC-2WAYVTRC-08
Trace E — LINE webhook POST /2way/WebHookApi/Line
Mount: app.ts:114-115 → routes/WebHookApi/index.ts
route.post('/Line', async function (req: RequestLineWebHook, res) {แหล่ง: WebHookApi/index.ts:251
Flow โดยสรุป (ไฟล์ยาว ~566 บรรทัด):
- รับ payload ตามโครงสร้าง LINE webhook (
RequestLineWebHookinterface ที่index.ts:23-93) - แยกประเภท event / message
- เรียก helpers จาก
triggerMenu.ts(triggerText,triggeImg) และchangeRich.ts(nextPageLine,previousPageLine) สำหรับ rich menu navigation - Authorization header สำหรับ LINE API สร้างจาก
process.env.LINE_AUTH(WebHookApi/index.ts:18-21)
มี endpoint เสริม POST /Line/checkToken (index.ts:343) สำหรับตรวจ token
คู่ขนานฝั่ง CU: /2way/cu/webHookApi → WebHookCuApi/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 ปิด)
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:
- Event
sendMessage(Socket.func.ts:32) - ถ้า image/video/pdf → อัปโหลดแล้วเปลี่ยน
body.msgเป็น URLhttps://vtrcdev.redcross.or.th/2way/file/...(Socket.func.ts:67-75) — hardcode dev hostname Chat.findOne({ channel: body.lineID })แล้วchat.save(Socket.func.ts:85-100)- 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
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 middleware | backdoor user i3admin ใน auth.service (คนละรูปแบบ) |
| Token store | ตาราง Sequelize token | ตรวจ JWT signature เป็นหลัก |
| Unauth PII endpoint | getProfile / getProfile/cu พิสูจน์แล้ว | ดู scorecard ของ 2way-api แยก |
| Runtime | ts-node ตรง | node dist/... หลัง compile |
ห้ามสรุปว่าสอง repo มี auth model เดียวกัน — consumer ต้องดูว่าเรียก host/path ไหน
Error handling patterns ที่พบบ่อย
| Pattern | ตัวอย่าง | ผล |
|---|---|---|
res.status(200).send(err) ใน catch | createIncident.ts:34 | client ต้องอ่าน status.code ไม่ใช่ HTTP status |
throw 'INTERNAL_SERVER_ERROR' string | helper insert หลายไฟล์ | ไม่ใช่ Error object — stack หาย |
Sentry.captureException ใน catch | createIncident.ts:32 | ชดเชย errorHandler ที่ลงทะเบียนผิดจังหวะ |
console.log token/headers | checkbypasstoken.ts:16-17 | เสี่ยง leak credential ใน log |
สิ่งที่อยู่นอกขอบเขตบทนี้
- รายการ entity fields เต็ม / connection strings → 03-persistence-integrations
- ตาราง debt + remediation → 04-scorecard