8.4 · Routing + auth gate + dynamic menu
บทนี้อธิบายว่า vtrc-rc-backoffice จัดการ route อย่างไร ตรวจ auth อย่างไร และเมนูถูกสร้างอย่างไรจาก backend
ภาพรวม
URL (browser)
↓
BrowserRouter
↓
Switch (เลือก match path)
↓
├── /login → Login (public)
├── /TheamCss → TheamCss (public, typo — ห้ามแก้)
├── /pathLogin/:token/:refreshToken → PathLogin (SSO bypass, public)
├── / → ถ้า isLoggedIn() === true → SiderLayout
│ ↓
│ axiosWorkforce.get("fetchgroupmenu")
│ axiosWorkforce.get("fetchsidemenu")
│ ↓
│ <Route> จาก routes.js ภายใน shell
├── !isLoggedIn() → <Redirect to="/login" />
└── (default) → NotFoundไม่มี component ชื่อ PrivateRoute — gate เป็น conditional JSX ใน App
routes.js — route table ขนาดใหญ่
src/config/routes.js เป็น array ที่เก็บ route entry ส่วนใหญ่ของแอป (ประมาณ 2,550 บรรทัด / ~276 paths)
// shape โดยย่อ — ไม่ใช่ไฟล์ทั้งก้อน
export const routes = [
{
path: "/home",
component: Home,
exact: true,
key: "home",
// label / moduleKey ฯลฯ ตาม entry
},
// ...
];ข้อระวังเกี่ยวกับ routes.js
- ไฟล์ใหญ่มาก — ควร grep หา path แทน scroll
- มี snapshot หลายตัว (
routes_bak.js,routes_bk-prd.js, …) — ห้ามใช้เป็นต้นแบบ (ดู บท 7.1) - การกรองสิทธิ์ฝั่ง frontend อ่อน — สิทธิ์จริงอยู่ที่ API
การ mount route ใน SiderLayout
src/components/SiderLayout/index.js (~1,168 บรรทัด) เป็น shell ที่ render sider + content และผูก <Route> จาก routes ภายใน Layout
concept: หลัง login สำเร็จผู้ใช้เข้า SiderLayout แล้วจึงได้เมนู + หน้าตาม path
Auth gate — isLoggedIn() + Redirect
function App () {
console.log = console.warn = console.error = () => { };
return (
<BrowserRouter>
<Switch>
<Route path="/login" exact component={Login} />
<Route path="/TheamCss" exact component={TheamCss} />
<Route path="/pathLogin/:token/:refreshToken" exact component={PathLogin}/>
{isLoggedIn() && <Route path="/" component={SiderLayout} />}
{!isLoggedIn() && <Redirect to="/login" />}
<Route component={NotFound} />
</Switch>
</BrowserRouter>
)
}isLoggedIn() — boolean ไม่ใช่ token string
export const isLoggedIn = () => {
if (localStorage.getItem('isRemember')) {
return !!localStorage.getItem('token') && !!localStorage.getItem('refreshToken')
} else {
return !!sessionStorage.getItem('token') && !!sessionStorage.getItem('refreshToken')
}
}จุดสำคัญ:
- คืน
true/falseเท่านั้น — ต้องมีครบทั้ง access token และ refresh token - ไม่ตรวจ expiry ของ JWT — token หมดอายุยังเข้า shell ได้ จนกว่า API จะตอบ
TOKEN_EXPIRED/TOKEN_INVALID(ดู บท 7.3) - JWT ที่แนบ request มาจาก
getToken()ไม่ใช่จากisLoggedIn()
SSO bypass — /pathLogin/:token/:refreshToken
PathLogin รับ token จาก URL แล้วเก็บลง storage — ทางเข้าจากระบบอื่น
ข้อระวัง: token ใน URL อาจรั่วผ่าน history / logs / referer (ดู บท 7.9)
เมนู dynamic — axiosWorkforce
เมนูไม่ใช่ static array จาก routes.js อย่างเดียว — SiderLayout ดึงจาก workforce REST ผ่าน axiosWorkforce (src/config/axios-workforce.js)
ขั้นตอนใน componentDidMount
const dataMenu = await axiosWorkforce.get("fetchgroupmenu");
console.log("dataMenu", dataMenu)
if (dataMenu.data) {
let MainMenus = MainMenu;
for (var data of dataMenu.data.data) {
MainMenus = [...MainMenus, data];
}
this.setState({
fetchMainMenu: MainMenus,
});
}
const dataSideMenu = await axiosWorkforce.get("fetchsidemenu");| ขั้น | การเรียก | ผล |
|---|---|---|
| 1 | axiosWorkforce.get("fetchgroupmenu") | กลุ่มเมนูหลัก รวมเข้ากับ MainMenu คงที่ในไฟล์ |
| 2 | axiosWorkforce.get("fetchsidemenu") | รายการเมนูย่อย (filter menuLevel === 2 + children) |
| 3 | setState | render AntD Menu / ลิงก์ไป path |
base URL ของ axiosWorkforce คือ .../workforce-api/api/restful (prod/UAT ตาม origin) — header Authorization + apiKey ถูกแนบโดย interceptor ของ AxiosBO (getToken() + apiKeyNews())
ไม่ใช่ axios.post("/api/fetchgroupmenu") และ ไม่มี getApikey() ใน flow นี้
ข้อดี / ข้อเสีย
- เพิ่มเมนูได้จาก backend โดยไม่ต้อง deploy frontend เสมอ
- โหลดช้าขึ้น (รอ 2 GET) และถ้า workforce REST ล่ม เมนูไม่ขึ้น
- debug ต้องดู Network ของ
fetchgroupmenu/fetchsidemenuไม่ใช่แค่routes.js
checkShowMenu — กรองเมนูฝั่ง frontend อ่อน
checkShowMenu = (key) => {
const { fetchMenuByRole } = this.state;
let checkMenu = true;
if (key == "set_rule") {
} else if (key == "ShowTitleConfig" || key == "ShowTitleCreate") {
checkMenu = true;
} else if (fetchMenuByRole.length !== 0) {
{
fetchMenuByRole
.map((routesNew) => (checkMenu = true));
}
}
return checkMenu;
};ในทางปฏิบัติมักคืน true — สิทธิ์จริงอยู่ที่ backend (GraphQL @auth / REST ของแต่ละ service)
โครงสร้างของ SiderLayout
ไฟล์ใหญ่ (~1,168 บรรทัด) รวม shell layout, ดึง profile, เมนู, breadcrumb, ลิงก์เปิดแท็บใหม่
คำแนะนำเมื่อแก้
- อย่า refactor ทั้งไฟล์ในครั้งเดียว
- ถ้าแก้เมนู ให้ grep
fetchgroupmenu/fetchsidemenu/axiosWorkforce - มี snapshot
_bkในโฟลเดอร์เดียวกัน — ห้ามใช้เป็นต้นแบบ
React Router v5
| API | ใช้ทำอะไร |
|---|---|
BrowserRouter | root router |
Switch | เลือก match path แรกที่ตรง |
Route | define path + component |
Redirect | ส่งไป /login เมื่อยังไม่ login |
Link / withRouter | นำทางภายในแอป |
ไม่ใช่ React Router v6 — ไม่มี <Routes> / useNavigate
เปรียบเทียบกับ vtrc-web (ดู Volume 8 บท 6.4)
| ด้าน | vtrc-rc-backoffice | vtrc-web |
|---|---|---|
| React Router | v5 | v5 |
| Route table | routes.js ขนาดใหญ่ | routes.js |
| เมนู | dynamic (axiosWorkforce GET) | static เป็นหลัก |
| Auth gate | isLoggedIn() boolean + Redirect | pattern คล้ายกัน |
| SSO bypass | /pathLogin/:token/:refreshToken | (ดู Volume 6) |
PrivateRoute | ไม่มี | (ดู Volume 6) |
เคล็ดลับตอนทำงานจริง
ตอนเพิ่มหน้าใหม่
- สร้างหน้าใน
src/pages/... - เพิ่ม entry ใน
src/config/routes.js - ถ้าต้องโชว์ในเมนู — ต้องมีข้อมูลเมนูฝั่ง workforce/backend ที่
fetchgroupmenu/fetchsidemenuส่งกลับ - ทดสอบเข้า path ตรง ๆ ก่อน
ตอนเมนูไม่แสดง
- Network:
fetchgroupmenu/fetchsidemenuสำเร็จหรือไม่ - ตรวจว่า token + apiKey ถูกแนบโดย
AxiosBO - เทียบ
pathUrl/menuNameEnกับ entry ในroutes.js