2way-meeting-backoffice — Core Pipeline
บทนี้อธิบายลำดับการทำงานจริงของ auth, route guard, LIFF bootstrap, และการเรียก backend — ทุก claim อ้าง file:line จาก branch prod
1. App bootstrap (_app.tsx)
ลำดับเมื่อโหลดทุกหน้า:
1. export default appWithTranslation(withAuthVerify(MyApp))
│ _app.tsx:152
▼
2. MyApp: ถ้า appEnv == "production" → mute console.log/error/warn
│ _app.tsx:55-58
▼
3. useEffect #1: ถ้า path segment เป็น "line" หรือ "line-cu"
│ → dynamic import @line/liff → liff.init({ liffId })
│ → ถ้ายังไม่ login → liff.login({ redirectUri: current href })
│ _app.tsx:69-102
▼
4. useEffect #2: router.events → LoadingScreen ระหว่างเปลี่ยนหน้า
│ _app.tsx:105-114
▼
5. Render tree:
RecoilRoot → CacheProvider → StoreProvider
→ ThemeProvider → Layout3(level=Component.layout)
→ <Component {...pageProps} />
_app.tsx:116-145LIFF ID (hardcoded ใน _app.tsx:74-86):
| Path | appEnv == production | อื่น ๆ (UAT/dev) |
|---|---|---|
/line (RC) | 2000453580-DG1n2baK | 1656935487-Vxm27Owv |
/line-cu (CU) | 2005957277-P564X0G1 | 2005863745-QPmEEd0b |
ตรวจ path ด้วย window.location.pathname.split("/")[2] เป็น urlProject2 (_app.tsx:33-34) — ทำงานได้เพราะมี basePath ทำให้ segment ที่สองเป็น line / line-cu
CSP meta ถูกตั้งใน <Head> (_app.tsx:126-129) — connect-src * + wss://vtrc.redcross.or.th (ไม่ตรงกับ websocket host จริง vtrcapi.redcross.or.th ใน env — ดู scorecard)
2. Layout และ route guard
2.1 Layout3
{level === "L2" ? <Navbar /> : <Fragment />}
{level === "L2" ? <SideMenu /> : <Fragment />}
<PrimaryContainer level={level}>{children}</PrimaryContainer>(layouts/Layout3.tsx:13-17)
เฉพาะหน้าแอดมิน (layout = "L2") ได้ shell ครบ
2.2 withAuthVerify — guard ที่ active
ห่อทั้งแอปที่ _app.tsx:152
พฤติกรรม (HOC/withAuthVerify.tsx):
checkAuth = Component.layout == "L2"(:11)- ถ้าไม่ใช่ L2 → render ต่อโดยไม่ verify
- ถ้าเป็น L2 และไม่มี
localStorage.accessToken→router.replace("/")+clearAllStorage()(:20-23) - ถ้ามี token →
POST ${appConnectAPI}/api/v1/auth/verifyTokenพร้อมAuthorization: Bearer(:26-36) - ถ้า
response.data.statusCode !== 200→ redirect/+ clear (:38-41) - catch error → redirect
/+ clear (:44-47)
จุดสำคัญ:
- ทำงานใน
useEffectหลัง mount — render ครั้งแรกยังผ่าน (return <WrappedComponent />ที่:63) - เกณฑ์สำเร็จคือ
statusCode === 200— ต่างจากwithAuth.tsxที่เช็ค=== 201(withAuth.tsx:41) withAuth.tsxถูก comment ออกจาก_app(_app.tsx:148) แต่ยังอยู่ใน repo
2.3 withAuthSuperAdmin
ชื่อไฟล์บอก super-admin แต่ implementation แค่เช็คว่ามี accessToken หรือไม่ (HOC/withAuthSuperAdmin.tsx:8-14) — ไม่ได้อ่าน profile.level ระดับ super-admin จริงอยู่ที่ getUserInfo() ใน utils/auth.tsx:71 (isSuperAdmin: profile?.level === 1)
3. Token และ profile storage
utils/auth.tsx:
| ฟังก์ชัน | Storage key | หมายเหตุ |
|---|---|---|
setToken / getToken | accessToken, isRemember | แม้ isRemember เป็น false ก็ยังเขียน localStorage ไม่ใช่ sessionStorage (บรรทัด session ถูก comment) (auth.tsx:6-12) |
setProfile / getProfile | profileTwoway | JWT profile ของพนักงาน/แอดมิน |
setProfileNonEmp / getProfileNonEmp | profileNonEmp | non-employee profile |
getUserInfo | อ่าน token + profile | คืน { profile, isSuperAdmin: level === 1 } (:58-74) |
clearAllStorage | — | localStorage.clear() + sessionStorage.clear() (:26-28) |
ไม่มี httpOnly cookie, ไม่มี refresh-token rotation ที่พบในโค้ด
4. แอดมิน login flow (trace เต็ม)
หน้า: / → features/BackOffice/Login (pages/index.tsx:3-8)
Submit ใน Login/components/Form.tsx:
1. ssoLogin() → axios.post(`${appConnectAPI}/api/v1/auth`, { username, password })
Form.tsx:95-110
▼
2. ถ้า response.data.success:
setToken({ accessToken: response.data.token }, null) (:115)
▼
3. ถ้า statusCode == 2000:
setProfile(JSON.stringify({ ...dataProfile, level: level===1 ? 1 : 2 }))
(:116-122)
else:
สร้าง profile stub จาก username, level: 2 (:124-141)
▼
4. snackbar "เข้าสู่ระบบสำเร็จ" → router.replace(
"/2wayfrontend/2waybackoffice/ChatCenter"
) (:149-150)ปลายทาง backend: 2way-api AuthController @Post() บน @Controller('auth') (ดูเอกสาร 2way-api) — endpoint เดียวกับที่มี hardcode backdoor i3admin ตาม SEC-2WAYAPI-01
หลัง redirect ไป ChatCenter (layout L2) → withAuthVerify ยิง verifyToken อีกรอบ
5. Bypass login จาก VTRC backoffice
หน้า: /2wayfrontend/bypassLogin/[token] (pages/2wayfrontend/bypassLogin/[token].tsx)
1. อ่าน token จาก router.query.token (:13)
2. POST ${appConnectAPI}/api/v1/auth/redirectbypass
Authorization: Bearer ${tokenID} (:34-44)
3. สำเร็จ → setToken + setProfile(level) → ChatCenter (:48-57)
4. ล้มเหลว → รอ 1s แล้ว window.location.href =
"https://vtrcbackoffice.redcross.or.th/Dashboard" (:65-68)เป็นสะพาน SSO จากระบบ backoffice หลักของ VTRC เข้า 2way backoffice โดยไม่กรอก username/password
6. Axios wrappers — สองเส้นทางหลัก
6.1 fetchAPI → 2way-api
let pathUrl = `${process.env.appConnectAPI}/${url}`;
// + Authorization: Bearer ${localStorage.accessToken}(utils/fetchApiWithAxios.tsx:8-16)
รองรับ GET (params) และ POST/PUT (data) — ไม่มี DELETE branch ที่ทำงานจริงใน wrapper นี้ ทั้งที่ announces.tsx เรียก "DELETE" (services/announces.tsx:76-78) → คำสั่ง delete ผ่าน fetchAPI จะไม่ยิง request (bug — ดู scorecard)
Error handling: .catch แล้ว resolve(error) ไม่ reject (fetchApiWithAxios.tsx:35-40, 59-64) — caller ได้ error object ปนกับ success response
6.2 fetchRedCrossAPI → host + relative path (chat-center)
url: `${process.env.appConnectRedCross}/${url}`
// เช่น https://vtrcapi.redcross.or.th/chat-center-api/api/v1/chat/rooms(fetchApiWithAxios.tsx:82-91)
ใช้โดย utils/chatCenterApi.tsx ที่ประกอบ ${chatCenterPath}/... (chatCenterApi.tsx:23,32)
คืน { status, statusText, data } ทั้ง success และ axios error response (:93-100)
6.3 fetchAPICustom → absolute URL
ใช้เมื่อ caller ส่ง URL เต็มจาก appConnectChatCenterAPI / appConnectIncidentAPI (fetchApiWithAxiosCustom.tsx:13) — pattern เดียวกับ fetchAPI เรื่อง resolve error
7. แคตตาล็อก API ฝั่งแอดมิน (2way-api)
Auth
| Call site | Method | Path |
|---|---|---|
| Login Form | POST | /api/v1/auth |
| withAuthVerify / withAuth | POST | /api/v1/auth/verifyToken |
| bypassLogin | POST | /api/v1/auth/redirectbypass |
Announces (services/announces.tsx)
| ฟังก์ชัน | Method | Path |
|---|---|---|
getAnnouncesTypes | GET | api/v1/announces/types |
getAnnounces | GET | api/v1/announces |
createAnnounces | POST | api/v1/announces |
getAnnouncesContentId | GET | api/v1/announces/{contentId} |
updateAnnouncesContentId | PUT | api/v1/announces/{id} |
deleteAnnouncesContentId | DELETE | /announces/{contentId} (path ไม่มี api/v1 + method ไม่รองรับใน wrapper) |
Create/Update ส่ง contentAssigns (ผู้รับ), contentManager, typeAssign (announces.tsx:36-53) — UI จัดการผู้รับใน Announce/*/components/Form.tsx + Employee.tsx
Surveys (services/surveys.tsx)
| ฟังก์ชัน | Method | Path |
|---|---|---|
getSurveysType | GET | api/v1/surveys/types |
getSurveysStatus | GET | api/v1/surveys/status |
getSurveyList | GET | api/v1/surveys (+ empCode จาก profile) |
getSurveysContentId | GET | api/v1/surveys/{id}/{empCode} |
createSurveys | POST | api/v1/surveys |
updateSurvey | PUT | api/v1/surveys/{id} |
getSummarySurvey | GET | api/v1/surveys/summary |
approveSurvey | POST | api/v1/surveys/adminApprove |
rejectSurvey | POST | api/v1/surveys/adminReject |
Approver admin สำหรับแบบสำรวจคือคู่ adminApprove / adminReject (surveys.tsx:121-144)
Employee dropdowns (services/employee.tsx)
ทุกเส้นทางใต้ api/v1/employee/...:
Organize, Division, Department, position, posManage, manager, level, group, admin, '' (list), byEmpCode
ใช้ประกอบตัวเลือกผู้รับประกาศ/แบบสำรวจ และ admin picker
Complaints / Feedback (inline ใน component — services/feedback.tsx ว่าง)
จาก Feedback/Edit/components/Form.tsx และ Dashboard:
| Action | Method | Path |
|---|---|---|
| list / filters | GET | api/v1/complaints, /subjects, /responsible, /status, /departments, /types |
| summary | POST | api/v1/complaints/summary |
| detail | GET | api/v1/complaints/{id}/{empCode} |
| reject | POST | api/v1/complaints/adminReject |
| assign | POST | api/v1/complaints/adminAssign |
| assignee manage | POST | api/v1/complaints/assigneeManage |
| secretary approve/decline | POST | .../secretaryApprove, .../secretaryDecline |
| superAdmin approval/reject | POST | .../superAdminApproval, .../superAdminReject |
| update isKnow | POST | api/v1/complaints/updateIsKnow |
| change division | POST | api/v1/complaints/changeDivicode |
นี่คือ workflow อนุมัติหลายขั้น (admin → secretary → superAdmin) ของฟีเจอร์ Feedback/Complaint ฝั่งแอดมิน
8. ChatCenter pipeline (แอดมิน)
8.1 โหลดหน้า ChatCenter
features/BackOffice/ChatCenter/index.tsx:
mount
├── fetchChatRoomList() → getChatRoomList() → GET chat-center .../chat/rooms
│ chatCenterApi.tsx:85-92, index.tsx:63-80
├── getTeamAdmin() → GET ${appConnectChatCenterAPI}/teamAdmin/list
│ index.tsx:83-95
└── socket = io.connect(appConnectWebSocket, {
transports: ["websocket"],
path: "/chat-center-ws/socket.io"
}) index.tsx:172-176เมื่อเลือก chatRoom:
getListChatMessage → GET .../chat/chatRoomHistory?roomEmpCode=
getNotificationList → GET .../appointment?empCodeRoom=
getProblemList → GET ${appConnectIncidentAPI}/incidents/chat-center/list/{empCode}(index.tsx:104-161)
8.2 Team / group admin (ChatCenter/admin)
หน้า /2wayfrontend/2waybackoffice/ChatCenter/admin (pages/.../ChatCenter/admin.tsx)
- ตารางทีม: teamId, teamName, teamDetail, tags, memberLength, isShow (
admin/index.tsx:26-67) - โหลดข้อมูลผ่าน
getTeamAdminList()(chatCenterApi.tsx:25-64) - Modal CRUD ทีม:
POST/PUT ${chatCenterApiPath}/teamAdmin(teamAdminModal/index.tsx:82,100) - ค้นหาสมาชิก:
GET .../teamAdmin/listMember?fullName=(searchAdminName.tsx:46) - จัดการแท็ก:
getTagList→tags/list(chatCenterApi.tsx:66-71) - Care taker list:
teamAdmin/listCareTaker(chatCenterApi.tsx:223-232)
นี่คือ group/approver-style admin ของ Chat Center — จัดทีมผู้ดูแล + tag ที่รับผิดชอบ ไม่ใช่ meeting approver
8.3 Satisfaction report
features/Satisfaction/index.tsx → getSatisfactionSurvey →
GET ${chatCenterPath}/satisfactedurvey/reportServey (สะกดตามโค้ด — satisfactedurvey) (chatCenterApi.tsx:168-169)
สร้างลิงก์แบบสอบถาม: POST .../satisfactedurvey/genSurvey (:216-218) — UI copy-link ถูก comment ไว้ (Satisfaction/index.tsx:66-74)
9. LIFF employee pipeline (ตัวอย่าง)
9.1 Profile resolve
เกือบทุกหน้า LIFF เริ่มด้วย:
axios.post(`${process.env.appConnectChatAPI}/getProfile`, { ... })ตัวอย่าง: Announce/List/index.tsx:175, Complaint/index.tsx:167, Incident/List/index.tsx:63
appConnectChatAPI = APP_CONNECT_API_BASIC = https://.../2way → 2way-vtrc-api
9.2 Registration (RC)
features/2way/Register/index.tsx:
| Step | Endpoint |
|---|---|
| verify | POST .../verifyUserLine (:165,206) |
| OTP | POST .../verifyOTPUserLine (:267) |
| create | POST .../createUserLine (:336) |
CU ใช้ path ต่อท้าย /cu ใน RegisterCu/index.tsx:162,208,305,372
9.3 Problem / FAQ
| Action | Endpoint | ไฟล์ |
|---|---|---|
| list FAQ | /getListFAQs | Problem/List/index.tsx:50 |
| create question | /createQuestion (มีช่องว่างท้าย path ในโค้ด) | Problem/Detail/index.tsx:147 |
| send LINE message | /sendMessageLineFAQsStepYes | Problem/Detail/index.tsx:171 |
Backoffice Question ใช้ชุดเดียวกันบน appConnectChatAPI: getListFAQs, addNewFAQs, updateFAQs, updateStatusFAQs
9.4 Incident (external API)
services/line/incidents.tsx:
GET ${appConnectIncidentAPI}/master/list/003- list by empCode / detail by id
ChatCenter ยังดึง incidents/chat-center/list/{empCode} (ChatCenter/index.tsx:146)
10. Survey list trace (แอดมิน) — ตัวอย่าง end-to-end
1. Survey/Dashboard → getSurveyList(props)
services/surveys.tsx:94-113
2. อ่าน empCode จาก JSON.parse(getProfile().profile)
3. fetchAPI(params, "GET", "api/v1/surveys")
4. URL = ${appConnectAPI}/api/v1/surveys
= https://vtrcapi.redcross.or.th/2way-api/api/v1/surveys
5. Header Authorization: Bearer <accessToken>
6. 2way-api SurveysController → MSSQL (ดู 2way-api persistence doc)
7. ถ้า error → fetchAPI resolve(error); surveys.tsx catch console.log
→ UI อาจว่างโดยไม่มี toastApprove จากหน้า View/Edit:
approveSurvey → POST api/v1/surveys/adminApprove (surveys.tsx:121-127)
11. Recoil / hooks ที่เกี่ยวกับ pipeline
| Atom / Hook | ใช้ทำอะไร |
|---|---|
stores/atoms/loginTypeAtom | อ้างใน withAuth.tsx (legacy guard) |
stores/atoms/tokenAtom | token state (ตรวจ usage จุดต่อจุดก่อนพึ่ง) |
hooks/useChatCenterNoti | รายการแจ้งเตือน chat |
hooks/useChatCenterTagList | แท็กสำหรับ filter/assign |
hooks/useChatCenterCareTakerList | รายชื่อผู้ดูแล |
State หลักของ ChatCenter ยังใช้ useState ในหน้าเป็นส่วนใหญ่ ไม่ได้อยู่ใน Recoil ทั้งก้อน
12. สิ่งที่ pipeline นี้ไม่ทำ
- ไม่มี Next.js
middleware.ts/ SSR auth - ไม่มี server-side session store
- ไม่เรียก
meeting-vtrc-apiหรือ repo ใน Meeting Platform group - ไม่มี database connection ของตัวเอง (แม้ env จะมี
APP_CONNECT_DB_*)