หน้า Public ใต้ /p
ภาพรวม
หน้าในกลุ่ม /p คือหน้าที่เปิดได้โดยไม่ต้องล็อกอินเข้า CMS ออกแบบมาให้ผู้ใช้ปลายทาง (ลูกค้าของ LINE OA) เปิดดูเนื้อหาหรือเมนูผ่านลิงก์ที่มี token
จุดสำคัญที่ทำให้กลุ่มหน้านี้ต่างจากหน้าอื่นทั้งหมดในระบบมี 2 ข้อ
- ไม่ห่อด้วยโครงหน้าจอของ CMS เลย ไม่มีส่วนหัว เมนูด้านข้าง breadcrumb หรือแถบคำสั่งด่วน เพราะโครงหน้าจอหลักตรวจ path แล้วเรนเดอร์เฉพาะเนื้อหาที่อยู่ข้างใน
- ไม่ใช้ตัวเรียก API กลางของ CMS ที่แนบ token ผู้ดูแลระบบและบังคับออกจากระบบเมื่อได้รับสถานะ 401 แต่เรียก API สาธารณะโดยตรง เพราะผู้เปิดหน้าไม่มีบัญชี CMS
กลุ่มนี้มี 3 หน้า ได้แก่ หน้าเนื้อหา (รองรับการล็อกอินผ่าน LIFF และการใส่รหัสผ่าน), หน้าเมนูสาธารณะ และหน้าเครื่องมือทดสอบลิงก์สำหรับทีมพัฒนา
Business Flow
หน้าเนื้อหาสาธารณะ
- อ่าน token จาก query string ก่อน ถ้าไม่พบจะลองอ่านจากค่า
liff.stateที่ LINE ส่งกลับมาในบางกรณี - ขั้นแรกระบบเรียกการตั้งค่า LIFF ของเนื้อหานั้นเพื่อดูว่าจำเป็นต้องยืนยันตัวตนหรือไม่ และต้องใช้ LIFF ID ใด
- ถ้าต้องยืนยันตัวตน ระบบจะโหลดไลบรารี LIFF แบบ dynamic แล้วเริ่มต้นใช้งาน หากผู้ใช้ล็อกอินอยู่แล้วจะเก็บ ID token ไว้ใช้แนบกับคำขอ หากยังไม่ล็อกอินและกำลังเปิดในเบราว์เซอร์ของ LINE ระบบจะพาไปล็อกอินแล้วโหลดหน้าใหม่ ส่วนกรณีที่เริ่มต้น LIFF ไม่สำเร็จจะปล่อยผ่านไปให้ฝั่งเซิร์ฟเวอร์ตอบปฏิเสธการเข้าถึงเอง
- จากนั้นจึงดึงเนื้อหาจริง โดยแนบ ID token เมื่อมี และแนบรหัสผ่านเมื่อผู้ใช้กรอก
- การตีความผลลัพธ์แบ่งเป็น 4 ทาง คือ ต้องกรอกรหัสผ่าน (แสดงการ์ดให้กรอก และแจ้งเตือนเมื่อรหัสผิด), ได้เนื้อหามาแล้ว (ตั้งชื่อหน้าเบราว์เซอร์จากข้อมูลเนื้อหา), ถูกปฏิเสธการเข้าถึง (แสดงการ์ดแจ้งพร้อมข้อความจากเซิร์ฟเวอร์) และข้อผิดพลาดอื่น (แสดงกล่องแจ้งเตือน)
- เมื่อได้เนื้อหาแล้ว หากมีทั้งฉบับภาษาไทยและภาษาอังกฤษ ระบบจะแสดงปุ่มสลับภาษาแบบตรึงไว้ด้านบน แล้วเรนเดอร์เนื้อหาด้วยตัวแสดงผลแบบอ่านอย่างเดียว หากไม่มีเนื้อหาในภาษาที่เลือกจะขึ้นข้อความแจ้ง
หน้าเมนูสาธารณะ
- อ่าน token จาก query string แล้วเรียก API สาธารณะเพื่อดึงโครงเมนูทั้งก้อน
- แสดงสถานะตามผลลัพธ์ ได้แก่ กำลังโหลด, เกิดข้อผิดพลาด, ไม่พบโครงเมนู หรือเรนเดอร์เมนูด้วยตัวแสดงผลสาธารณะ
- ตัวแสดงผลเมนูรองรับการเดินเข้าเมนูย่อย ปุ่มย้อนกลับและกลับหน้าแรกเมื่อไม่ได้อยู่ระดับบนสุด และช่องค้นหาที่ค้นทั้งต้นไม้พร้อมแสดงเส้นทางของรายการที่ชื่อซ้ำกัน
- การกดรายการชนิดโฟลเดอร์จะเดินเข้าไปยังรายการลูก ส่วนชนิดลิงก์ภายนอกจะเปิดแท็บใหม่
- การจัดวางเป็นแบบตารางหรือแบบรายการตามเทมเพลตที่ตั้งไว้ โดยสไตล์ปุ่มมาจากธีมของเมนูแล้วถูกทับด้วยการปรับแต่งรายรายการ
หน้าทดสอบลิงก์
หน้านี้เป็นเครื่องมือช่วยทีมพัฒนา แสดงตัวอย่าง URL ของหน้าเนื้อหาและหน้าเมนูพร้อมปุ่มเปิดและปุ่มคัดลอก และมีการ์ดสรุปขั้นตอนการทดสอบพร้อม endpoint ที่เกี่ยวข้อง
หน้าจอและองค์ประกอบหลัก
การแยกโครงหน้าจอ (src/app/layout.tsx) — ตรวจว่า path ขึ้นต้นด้วย /p/ หรือไม่ ถ้าใช่จะเรนเดอร์เฉพาะเนื้อหาโดยไม่ห่อองค์ประกอบใดของ CMS ส่วน layout ภายในกลุ่ม /p เองเป็นเพียงตัวส่งผ่าน
หน้าเนื้อหา (src/app/p/content/page.tsx) — จัดการลำดับการเริ่มต้น LIFF, การดึงเนื้อหา, ด่านรหัสผ่าน และการสลับภาษา
หน้าเมนู (src/app/p/menu/page.tsx) — โหลดโครงเมนูสาธารณะแล้วส่งต่อให้ตัวแสดงผล
หน้าทดสอบ (src/app/p/test/page.tsx) — เครื่องมือสำหรับทีมพัฒนา
คอมโพเนนต์ที่ใช้ร่วมกับฝั่ง CMS — ตัวแสดงเนื้อหาแบบอ่านอย่างเดียว (TiptapViewer) ซึ่งใช้ชุดส่วนขยายเดียวกับตัวแก้ไขเนื้อหา และตัวแสดงผลเมนูสาธารณะ (MenuPublicView) ซึ่งเป็นตัวเดียวกับที่ตัวสร้างเมนูใช้
Endpoint สาธารณะที่หน้าเหล่านี้เรียกจริง — การตั้งค่า LIFF ของเนื้อหา, การอ่านเนื้อหาด้วย token (รับรหัสผ่านเป็น query และ ID token เป็น header) และการอ่านโครงเมนูด้วย token
จุดเชื่อมต่อกับฟีเจอร์อื่น
- ไม่มีการตรวจสิทธิ์ฝั่งหน้าเว็บ — การควบคุมการเข้าถึงทั้งหมดอยู่ที่ฝั่งเซิร์ฟเวอร์ ผ่าน token, การบังคับยืนยันตัวตนด้วย LIFF ID token และรหัสผ่าน
- จัดการเนื้อหา (Content Management) — เป็นแหล่งที่ออก token สาธารณะ และเป็นที่ตั้งค่าว่าต้องยืนยันตัวตน ต้องใช้รหัสผ่าน หรือจำกัดกลุ่มผู้ชมหรือไม่
- ตัวสร้างเมนู (Menu Builder) — เป็นแหล่งที่ออก token สาธารณะของเมนู
- LINE OA Management — เป็นที่มาของ LIFF ID และ hash ของ OA ที่ฝั่ง CMS ใช้ประกอบลิงก์ก่อนแจกจ่าย
- ไลบรารีภายนอก — ไลบรารี LIFF ของ LINE ถูกโหลดแบบ dynamic เฉพาะเมื่อเนื้อหานั้นต้องยืนยันตัวตนจริง เพื่อไม่ให้กระทบเวลาโหลดของเนื้อหาสาธารณะทั่วไป
- การตั้งค่าระบบ — ทุกคำขอชี้ไปที่ base URL ของ API ที่กำหนดผ่าน
NEXT_PUBLIC_BASE_CONSOLE_API_URL
รายละเอียดฝั่ง Backend (CMS API)
endpoint ที่หน้ากลุ่ม /p เรียกอยู่ในโมดูล internal/modules/publicmod/ ซึ่งทุกเส้นถูกประกาศเป็น public ทั้งหมด ไม่มีการตรวจ JWT เลย สิ่งที่ใช้แทนการตรวจสิทธิ์คือ token ที่เดาไม่ได้, การจำกัดอัตราการเรียก และเงื่อนไขเฉพาะหน้า (รหัสผ่าน / LIFF)
endpoint และการควบคุมของแต่ละเส้น
GET /api/public/contents— ลิสต์คอนเทนต์ที่เผยแพร่แล้ว คืนข้อมูลพร้อมข้อมูลการแบ่งหน้า (จำนวนทั้งหมด, หน้าปัจจุบัน, จำนวนต่อหน้า, จำนวนหน้าทั้งหมด) มี rate limit 10 ครั้ง / 60 วินาทีGET /api/public/contents/categories— ลิสต์หมวดหมู่ที่มีคอนเทนต์อยู่ รับlineOaIdเป็น query มี rate limit 10 ครั้ง / 60 วินาที เช่นกันGET /api/public/content/:token/liff-config— คืนการตั้งค่า LIFF ของหน้านั้นให้หน้าเว็บนำไปเริ่มต้น LIFF ก่อนโหลดเนื้อหา ไม่ถูกจำกัดอัตราGET /api/public/content/:token— อ่านเนื้อหาด้วย token ไม่ถูกจำกัดอัตราGET /api/public/menu/:token— อ่านโครงเมนูด้วย token ไม่ถูกจำกัดอัตรา
เหตุผลที่บางเส้นถูกจำกัดอัตราและบางเส้นไม่ — เส้นที่ลิสต์ข้อมูลเป็นชุดเปิดให้เรียกได้โดยไม่ต้องรู้อะไรล่วงหน้า จึงถูกครอบ throttle ไว้ ส่วนเส้นที่ต้องมี token ถือว่า token เดาไม่ได้อยู่แล้วจึงไม่จำกัด ข้อสังเกตด้านความปลอดภัย: เมื่อ token รั่ว จะไม่มีเพดานการเรียกมาช่วยชะลอเลย การออก token ใหม่จึงเป็นเครื่องมือเดียวที่ตัดการเข้าถึงได้จริง
สิ่งที่ backend ตรวจตอนอ่านเนื้อหาด้วย token
- ถ้าคำขอมี header
x-liff-tokenมาด้วย backend จะใช้ระบุตัวผู้ใช้ LINE ที่กำลังเปิดหน้าอยู่ ทำให้บันทึกการเข้าชมแบบระบุตัวตนได้ — นี่คือกลไกที่ทำให้การจำกัดกลุ่มผู้ชมทำงาน - ถ้าหน้าถูกตั้งรหัสผ่านไว้ ต้องส่ง
passwordมาเป็น query ให้ถูก มิฉะนั้นจะไม่ได้เนื้อหา - ถ้าไม่พบหน้า หรือหน้านั้นยังไม่ถูกเผยแพร่ backend จะตอบด้วยรหัสข้อผิดพลาด
APP_007— หน้าจอไม่แยกความต่างระหว่าง "ไม่มี" กับ "ยังไม่ publish" ซึ่งเป็นพฤติกรรมที่ตั้งใจ เพื่อไม่ให้ผู้เรียกเดาได้ว่ามี token นั้นอยู่จริงหรือไม่
เส้นทางสาธารณะที่อยู่นอกโมดูลนี้
ยังมี endpoint สาธารณะกระจายอยู่ในโมดูลอื่นด้วย ซึ่งควรรู้ไว้เมื่อไล่ตามลิงก์:
GET /api/public/content-pages/:tokenและPOST /api/public/content-pages/:token/track— อยู่ในโมดูลจัดการเนื้อหา (เส้นหลังใช้บันทึกการเข้าชม)GET /api/menu-builder/public/:token— อยู่ในโมดูลตัวสร้างเมนู เป็นเส้นคู่ขนานกับGET /api/public/menu/:token
ข้อควรระวังเรื่อง soft delete ที่กระทบหน้าสาธารณะโดยตรง
- ตาราง
content_page,content_categoryและcontent_subcategoryใช้คอลัมน์deleted_dateที่ ไม่ได้ผูกกับกลไก soft delete อัตโนมัติของ ORM ทุกคำสั่งอ่านจึงต้องใส่เงื่อนไขกรองด้วยตัวเอง หากพลาดที่ใดที่หนึ่ง เนื้อหาที่ถูกลบแล้วอาจโผล่กลับมาบนหน้าสาธารณะ - ตาราง
line_oaผูกกับกลไกอัตโนมัติจริง จึงถูกกรองให้เองโดยไม่ต้องเขียนเพิ่ม - ในโค้ดเดิมมีเงื่อนไขกรอง soft delete ที่ชี้ไปยังฟิลด์ที่ไม่มีอยู่จริง ทำให้ไม่มีผลอะไร (no-op) — เป็นตัวอย่างว่าการพึ่งเงื่อนไขที่เขียนเองพลาดได้ง่าย
การจัดลำดับ route
route ที่เป็นข้อความคงที่ (เช่น /api/public/contents) ต้องถูกลงทะเบียนก่อน route ที่มีพารามิเตอร์ :token มิฉะนั้นคำว่า contents จะถูกจับเป็น token