4.1.3 · State/styling/locale + Security/Performance + Scorecard
State — local เท่านั้น ไม่มี global state library
state ทั้งหมดอยู่ 3 ที่ — this.state (component), localStorage (token/refreshToken ถ้า remember, profile, noti counter — ข้าม tab+วัน), sessionStorage (token ถ้าไม่ remember — เฉพาะ tab) ไม่มี "global loading"/"global error banner" — แต่ละ page จัดการเอง, error ผ่าน alertMessage()/message.error() เป็น toast
Key ที่ใช้บ่อยใน storage — token, refreshToken, isRemember, appUsername, sessionID, lsid, organization, IsTrial, profileDetailPW/profileDetailUN (encrypted password/username สำหรับ remember-me)
กฎเหล็ก — ห้ามอ่าน localStorage ตรงๆ ใช้ getToken()/setToken()/clearAllStorage() เสมอ; ห้ามเก็บ object โดยไม่ JSON.stringify
Styling — ผสม 4 แบบ
- Global CSS จาก SCSS (
assets/sass/main.scss1,764 บรรทัด compile ด้วย Prepros manual →assets/css/main.css4,565 บรรทัด) - AntD override compiled (
config/custom-antd.css21,190 บรรทัด — ใหญ่ที่สุดใน repo ห้ามแก้ตรง เป็น output จาก build) - Utility CSS สั้นๆ (
config/styles.css128 บรรทัด) styled-componentsใช้น้อย ส่วนใหญ่ใน Login page
ลำดับความสำคัญ — AntD default → custom-antd.css → main.css → styles.css → styled-components inline ฟอนต์ default คือ Kanit-Medium
Locale — พ.ศ. กระจาย 3 ทาง
moment.locale('th')(src/index.js:10-12) —.format('LL')ได้ พ.ศ. อัตโนมัติ แต่.format('YYYY')ยังได้ ค.ศ.- override
moment.fn.format/moment.fn.yearแบบ +543 ถูก comment ไว้ (src/index.js:18-33) - helper 22 ตัวใน
utils/formatdate.js(442 บรรทัด) ที่ manual +543 — เช่นchangeFormatdate(),changeYear()
กฎเหล็ก — ห้าม +543 ตรงๆ ใน component ใช้ helper เสมอ; ห้ามใช้ moment().year() ตรงๆ เพื่อแสดงผล ปัญหาที่พบ — monthThai array ประกาศซ้ำใน 7+ function (ควร refactor เป็น constant กลาง)
Axios instances หลายตัวสำหรับ REST — AxiosBO() (instance หลัก มี interceptor refresh token เมื่อ 401), axiosAPI.js (ใช้ใน Login/SSO), axios-i3.js (I3 internal services)
dangerouslySetInnerHTML พบใน 20+ ไฟล์ (render news/policy/notification content จาก server) — ไม่พบ sanitizer ใดๆ ในระบบ (ดู S-4 ด้านล่าง)
Security findings
| ID | ปัญหา | ไฟล์ | ความเสี่ยง |
|---|---|---|---|
| S-1 | Token เก็บใน localStorage/sessionStorage | utils/auth.js:4-35 | XSS → ขโมย token ได้ทันที ถ้าจะแก้ต้อง migrate เป็น HttpOnly cookie (กระทบทั้ง frontend+backend) |
| S-2 | apiKey เข้ารหัส base64 ในบันเดิล | utils/apiKey.js:1-12 | atob() decode ได้ค่าจริงทันที — apiKey จริง (fd77b56f-...) ฝังอยู่ในบันเดิล ระบุ APP_TYPE: WEB ไม่ใช่ authentication จริง |
| S-3 | AES key + IV เดียวกัน hardcoded สำหรับ "remember password" | utils/auth.js:37-47 ("6A45soD%9eB#") | ใครเปิดบันเดิลถอดรหัส password ที่เก็บใน localStorage ได้ทันที — IV ต้องสุ่มและต้องไม่ตรงกับ key |
| S-4 | dangerouslySetInnerHTML ไม่มี sanitizer | 20+ ไฟล์ | ถ้า content จาก server มี <script>/onerror → XSS ควรเพิ่ม DOMPurify.sanitize() |
| S-5 | isShow เป็นเพียง flag ฝั่ง UI ไม่ใช่ auth gate | config/routes.js | ผู้ใช้ไม่มีสิทธิ์เข้า route ได้ (แต่ query จะ FORBIDDEN ที่ server) — ดู 4.1.2 |
| S-6 | Console override ปิดการ debug production | containers/App/index.js:15 | ควร strip ด้วย terser drop_console ตอน build แทน override runtime |
| S-7 | Google Analytics ID เปิดเผย | public/index.html:33-38 | public ID โดยธรรมชาติ แต่เปิดช่องให้ spam event เข้า GA |
| S-8 | ไม่มี Content Security Policy | nginx-config/default.conf | ป้องกัน XSS/data exfiltration ยาก |
Performance findings
| ID | ปัญหา | รายละเอียด |
|---|---|---|
| P-1 | Bundle ใหญ่ ~12 MB (build/ รวม 79 MB) | ไม่มี code splitting ระดับ route, AntD v3 import ทั้ง library, ใช้ PDF library 4 ตัวซ้อนกัน (@react-pdf/renderer, react-pdf, @react-pdf-viewer, pdfjs-dist), custom-antd.css 21,190 บรรทัดไม่ minify |
| P-2 | fetchPolicy: 'no-cache' ทุก query | ทุกการเปลี่ยนหน้าดึงข้อมูลใหม่จาก server เสมอ — แก้ได้เฉพาะจุดด้วย fetchPolicy: 'cache-first' สำหรับ dropdown master data |
| P-3 | SiderLayout query profile ทุกครั้งที่ reload | ทำให้หน้าแรกช้ากว่าที่ควร — แก้ด้วย cache + stale-while-revalidate |
| P-4 | ไม่มี lazy loading รูปภาพ | ทุกรูปโหลดทันทีตอน render |
| P-5 | Webpack 4 (ไม่มี webpack 5 features) | upgrade เป็น breaking change |
| P-6 | console.log กระจายหลายสิบจุด | เพิ่มขนาดบันเดิลแม้ runtime จะปิดไว้ — ควร strip ด้วย terser |
| P-7 | PDF.js โหลด worker แยก | เพิ่ม 1 request ทำให้หน้า Slip ช้ากว่าหน้าอื่น |
Scorecard
| มิติ | คะแนน | หมายเหตุ |
|---|---|---|
| Maintainability | 5/10 | pattern สม่ำเสมอ แต่ legacy 50+ ไฟล์ + bundle ใหญ่ |
| Security | 4/10 | token ใน localStorage + AES key hardcoded + ไม่มี HTML sanitizer |
| Performance | 4/10 | bundle ~12MB + no-cache ทุก query + ไม่มี code splitting |
| Developer Experience | 5/10 | pattern ชัดเจนแต่ console ถูกปิด + build ช้า |
| Documentation | 3/10 | แทบไม่มี inline comment อธิบาย intent |
| Test coverage | 1/10 | ไม่มี unit/integration test |
| Type safety | 1/10 | JavaScript ล้วน ไม่มี TypeScript |
| Accessibility | 3/10 | AntD v3 มี a11y พื้นฐานแต่ไม่มี ARIA เพิ่ม |
| รวม | 3.5/10 | ทำงานได้จริง แต่หนี้เทคนิคสะสมสูง |
จุดแข็ง — pattern สม่ำเสมอ (class+withApollo ทั้งระบบ), GraphQL operations รวมในไฟล์เดียว, runtime env resolver (bundle เดียวใช้หลาย env), error handling รวมศูนย์ที่ errorLink, route table ชัดเจน
Known issues (W-1 ถึง W-10) และลำดับการแก้
Phase 1 — quick win (1-2 วัน): console override → terser drop_console; ลบ helper toGqlTypes/Args/Fields ที่ไม่ใช้; เพิ่ม <ErrorBoundary> ครอบ <App>; เลิกใช้ AES remember-password
Phase 2 — security (1 สัปดาห์): เพิ่ม DOMPurify ก่อน dangerouslySetInnerHTML; เพิ่ม CSP header ที่ nginx; ย้าย apiKey ไป server-side proxy
Phase 3 — performance (2-4 สัปดาห์): babel-plugin-import สำหรับ AntD (ROI สูงสุดด้วย effort น้อยที่สุด); ปรับ fetchPolicy ของ dropdown query; รวม constant เดือนไทยกลาง
Phase 4 — long-term (1-3 เดือน): archive ไฟล์ legacy 50+ ไฟล์; setup jest + เขียน test ของ utils/ (pure function) ก่อน; พิจารณาย้าย TypeScript/React 17+ (breaking change ใหญ่ ต้องมีแผนแยก)
เกณฑ์ก่อนแก้ไฟล์ใดใน vtrc-web
- ไฟล์นี้เป็นของจริงไหม (เช็คใน
routes.js) - pattern ตรงไหม (class + withApollo + Form.create ถ้ามี form)
- endpoint ใช้
checkLinkURL()ไหม (ห้าม hardcode) - error handling ผ่าน errorLink ไหม (ห้าม catch เอง)
- state ใช้
this.state+localStorage ผ่าน helper ไหม (ห้าม Redux/Context) - locale ใช้ helper ใน
formatdate.jsไหม (ห้าม +543 ตรงๆ)