Skip to main content

หน้า Public ใต้ /p

ภาพรวม​

หน้าในกลุ่ม /p คือหน้าที่เปิดได้โดยไม่ต้องล็อกอินเข้า CMS ออกแบบมาให้ผู้ใช้ปลายทาง (ลูกค้าของ LINE OA) เปิดดูเนื้อหาหรือเมนูผ่านลิงก์ที่มี token

จุดสำคัญที่ทำให้กลุ่มหน้านี้ต่างจากหน้าอื่นทั้งหมดในระบบมี 2 ข้อ

  • ไม่ห่อด้วยโครงหน้าจอของ CMS เลย ไม่มีส่วนหัว เมนูด้านข้าง breadcrumb หรือแถบคำสั่งด่วน เพราะโครงหน้าจอหลักตรวจ path แล้วเรนเดอร์เฉพาะเนื้อหาที่อยู่ข้างใน
  • ไม่ใช้ตัวเรียก API กลางของ CMS ที่แนบ token ผู้ดูแลระบบและบังคับออกจากระบบเมื่อได้รับสถานะ 401 แต่เรียก API สาธารณะโดยตรง เพราะผู้เปิดหน้าไม่มีบัญชี CMS

กลุ่มนี้มี 3 หน้า ได้แก่ หน้าเนื้อหา (รองรับการล็อกอินผ่าน LIFF และการใส่รหัสผ่าน), หน้าเมนูสาธารณะ และหน้าเครื่องมือทดสอบลิงก์สำหรับทีมพัฒนา

Business Flow​

หน้าเนื้อหาสาธารณะ​

  1. อ่าน token จาก query string ก่อน ถ้าไม่พบจะลองอ่านจากค่า liff.state ที่ LINE ส่งกลับมาในบางกรณี
  2. ขั้นแรกระบบเรียกการตั้งค่า LIFF ของเนื้อหานั้นเพื่อดูว่าจำเป็นต้องยืนยันตัวตนหรือไม่ และต้องใช้ LIFF ID ใด
  3. ถ้าต้องยืนยันตัวตน ระบบจะโหลดไลบรารี LIFF แบบ dynamic แล้วเริ่มต้นใช้งาน หากผู้ใช้ล็อกอินอยู่แล้วจะเก็บ ID token ไว้ใช้แนบกับคำขอ หากยังไม่ล็อกอินและกำลังเปิดในเบราว์เซอร์ของ LINE ระบบจะพาไปล็อกอินแล้วโหลดหน้าใหม่ ส่วนกรณีที่เริ่มต้น LIFF ไม่สำเร็จจะปล่อยผ่านไปให้ฝั่งเซิร์ฟเวอร์ตอบปฏิเสธการเข้าถึงเอง
  4. จากนั้นจึงดึงเนื้อหาจริง โดยแนบ ID token เมื่อมี และแนบรหัสผ่านเมื่อผู้ใช้กรอก
  5. การตีความผลลัพธ์แบ่งเป็น 4 ทาง คือ ต้องกรอกรหัสผ่าน (แสดงการ์ดให้กรอก และแจ้งเตือนเมื่อรหัสผิด), ได้เนื้อหามาแล้ว (ตั้งชื่อหน้าเบราว์เซอร์จากข้อมูลเนื้อหา), ถูกปฏิเสธการเข้าถึง (แสดงการ์ดแจ้งพร้อมข้อความจากเซิร์ฟเวอร์) และข้อผิดพลาดอื่น (แสดงกล่องแจ้งเตือน)
  6. เมื่อได้เนื้อหาแล้ว หากมีทั้งฉบับภาษาไทยและภาษาอังกฤษ ระบบจะแสดงปุ่มสลับภาษาแบบตรึงไว้ด้านบน แล้วเรนเดอร์เนื้อหาด้วยตัวแสดงผลแบบอ่านอย่างเดียว หากไม่มีเนื้อหาในภาษาที่เลือกจะขึ้นข้อความแจ้ง

หน้าเมนูสาธารณะ​

  1. อ่าน token จาก query string แล้วเรียก API สาธารณะเพื่อดึงโครงเมนูทั้งก้อน
  2. แสดงสถานะตามผลลัพธ์ ได้แก่ กำลังโหลด, เกิดข้อผิดพลาด, ไม่พบโครงเมนู หรือเรนเดอร์เมนูด้วยตัวแสดงผลสาธารณะ
  3. ตัวแสดงผลเมนูรองรับการเดินเข้าเมนูย่อย ปุ่มย้อนกลับและกลับหน้าแรกเมื่อไม่ได้อยู่ระดับบนสุด และช่องค้นหาที่ค้นทั้งต้นไม้พร้อมแสดงเส้นทางของรายการที่ชื่อซ้ำกัน
  4. การกดรายการชนิดโฟลเดอร์จะเดินเข้าไปยังรายการลูก ส่วนชนิดลิงก์ภายนอกจะเปิดแท็บใหม่
  5. การจัดวางเป็นแบบตารางหรือแบบรายการตามเทมเพลตที่ตั้งไว้ โดยสไตล์ปุ่มมาจากธีมของเมนูแล้วถูกทับด้วยการปรับแต่งรายรายการ

หน้าทดสอบลิงก์​

หน้านี้เป็นเครื่องมือช่วยทีมพัฒนา แสดงตัวอย่าง 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​

  1. ถ้าคำขอมี header x-liff-token มาด้วย backend จะใช้ระบุตัวผู้ใช้ LINE ที่กำลังเปิดหน้าอยู่ ทำให้บันทึกการเข้าชมแบบระบุตัวตนได้ — นี่คือกลไกที่ทำให้การจำกัดกลุ่มผู้ชมทำงาน
  2. ถ้าหน้าถูกตั้งรหัสผ่านไว้ ต้องส่ง password มาเป็น query ให้ถูก มิฉะนั้นจะไม่ได้เนื้อหา
  3. ถ้าไม่พบหน้า หรือหน้านั้นยังไม่ถูกเผยแพร่ 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