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