Skip to content

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

LIFF ID (hardcoded ใน _app.tsx:74-86):

PathappEnv == productionอื่น ๆ (UAT/dev)
/line (RC)2000453580-DG1n2baK1656935487-Vxm27Owv
/line-cu (CU)2005957277-P564X0G12005863745-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

tsx
{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):

  1. checkAuth = Component.layout == "L2" (:11)
  2. ถ้าไม่ใช่ L2 → render ต่อโดยไม่ verify
  3. ถ้าเป็น L2 และไม่มี localStorage.accessTokenrouter.replace("/") + clearAllStorage() (:20-23)
  4. ถ้ามี token → POST ${appConnectAPI}/api/v1/auth/verifyToken พร้อม Authorization: Bearer (:26-36)
  5. ถ้า response.data.statusCode !== 200 → redirect / + clear (:38-41)
  6. 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 / getTokenaccessToken, isRememberแม้ isRemember เป็น false ก็ยังเขียน localStorage ไม่ใช่ sessionStorage (บรรทัด session ถูก comment) (auth.tsx:6-12)
setProfile / getProfileprofileTwowayJWT profile ของพนักงาน/แอดมิน
setProfileNonEmp / getProfileNonEmpprofileNonEmpnon-employee profile
getUserInfoอ่าน token + profileคืน { profile, isSuperAdmin: level === 1 } (:58-74)
clearAllStoragelocalStorage.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 fetchAPI2way-api

tsx
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)

tsx
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 siteMethodPath
Login FormPOST/api/v1/auth
withAuthVerify / withAuthPOST/api/v1/auth/verifyToken
bypassLoginPOST/api/v1/auth/redirectbypass

Announces (services/announces.tsx)

ฟังก์ชันMethodPath
getAnnouncesTypesGETapi/v1/announces/types
getAnnouncesGETapi/v1/announces
createAnnouncesPOSTapi/v1/announces
getAnnouncesContentIdGETapi/v1/announces/{contentId}
updateAnnouncesContentIdPUTapi/v1/announces/{id}
deleteAnnouncesContentIdDELETE/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)

ฟังก์ชันMethodPath
getSurveysTypeGETapi/v1/surveys/types
getSurveysStatusGETapi/v1/surveys/status
getSurveyListGETapi/v1/surveys (+ empCode จาก profile)
getSurveysContentIdGETapi/v1/surveys/{id}/{empCode}
createSurveysPOSTapi/v1/surveys
updateSurveyPUTapi/v1/surveys/{id}
getSummarySurveyGETapi/v1/surveys/summary
approveSurveyPOSTapi/v1/surveys/adminApprove
rejectSurveyPOSTapi/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:

ActionMethodPath
list / filtersGETapi/v1/complaints, /subjects, /responsible, /status, /departments, /types
summaryPOSTapi/v1/complaints/summary
detailGETapi/v1/complaints/{id}/{empCode}
rejectPOSTapi/v1/complaints/adminReject
assignPOSTapi/v1/complaints/adminAssign
assignee managePOSTapi/v1/complaints/assigneeManage
secretary approve/declinePOST.../secretaryApprove, .../secretaryDecline
superAdmin approval/rejectPOST.../superAdminApproval, .../superAdminReject
update isKnowPOSTapi/v1/complaints/updateIsKnow
change divisionPOSTapi/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)
  • จัดการแท็ก: getTagListtags/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.tsxgetSatisfactionSurvey

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 เริ่มด้วย:

tsx
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://.../2way2way-vtrc-api

9.2 Registration (RC)

features/2way/Register/index.tsx:

StepEndpoint
verifyPOST .../verifyUserLine (:165,206)
OTPPOST .../verifyOTPUserLine (:267)
createPOST .../createUserLine (:336)

CU ใช้ path ต่อท้าย /cu ใน RegisterCu/index.tsx:162,208,305,372

9.3 Problem / FAQ

ActionEndpointไฟล์
list FAQ/getListFAQsProblem/List/index.tsx:50
create question/createQuestion (มีช่องว่างท้าย path ในโค้ด)Problem/Detail/index.tsx:147
send LINE message/sendMessageLineFAQsStepYesProblem/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 อาจว่างโดยไม่มี toast

Approve จากหน้า View/Edit:

approveSurveyPOST api/v1/surveys/adminApprove (surveys.tsx:121-127)


11. Recoil / hooks ที่เกี่ยวกับ pipeline

Atom / Hookใช้ทำอะไร
stores/atoms/loginTypeAtomอ้างใน withAuth.tsx (legacy guard)
stores/atoms/tokenAtomtoken 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_*)