เส้นทาง [hash] และการระบุ LINE OA
ภาพรวม
mini-app ฝั่งลูกค้าทุกตัวอยู่ภายใต้ path รูปแบบ /:hash/... โดย hash คือรหัสสาธารณะของ LINE OA (ใช้ค่าเดียวกับ webhook key hash) ค่านี้เป็นกุญแจที่หน้าเว็บใช้ระบุว่ากำลังทำงานในบริบทของ OA ใด
หน้าเพจใช้ hash เพื่อค้นหาข้อมูลสี่อย่าง
lineLiffIdสำหรับ init LIFFformLiffIdซึ่งเป็น LIFF แยกสำหรับหน้าฟอร์ม (หากมีการตั้งค่าไว้)botBasicIdในรูปแบบ@xxxxสำหรับสร้างลิงก์ไปยังห้องแชทของ OA- ชื่อและรูปภาพของ OA เพื่อใช้ทำ Open Graph metadata เวลาแชร์ลิงก์
นอกจากการ resolve ข้อมูลแล้ว layout ของ segment นี้ (src/app/[hash]/layout.tsx) ยังสร้าง link preview ระดับ OA ให้ทุกหน้าที่อยู่ภายใต้ path นี้แบบ server-side ด้วย
Business Flow
- ผู้ใช้เปิด URL ในรูปแบบ
/:hash/:feature/... - ฝั่งเซิร์ฟเวอร์: ฟังก์ชัน
generateMetadata()ของ layout ยิงคำขอไปที่ endpoint ดึงข้อมูล OA โดยลองใช้ internal API URL ก่อน แล้วค่อย fallback ไปยัง public URL พร้อมกำหนด cache ไว้ 300 วินาที เมื่อได้ชื่อและรูปภาพ OA จะประกอบเป็นtitle,description,openGraphและtwittercard หากเรียกไม่สำเร็จจะ fallback ไปใช้ค่าจาก environment variable - ฝั่ง Client: หน้าเพจเรียก
useFetchGetLineOaByHash(hash)ผ่าน React Query โดยตั้ง retry ไว้เพียง 1 ครั้ง - เก็บ
lineLiffIdที่ได้ลงในuseAppStoreแล้วส่งต่อให้useLiffAuthเพื่อเริ่มกระบวนการล็อกอิน - หน้าลูกสามารถประกาศ metadata ของตัวเองทับได้ Next.js จะ merge ค่าของ child ทับ parent ให้อัตโนมัติ
- path
/:hashซึ่งเป็นหน้า index ของ OA ทำหน้าที่เป็นหน้าตรวจสอบสิทธิ์แบบเรียบง่าย คือล็อกอิน LIFF แล้วแสดงโปรไฟล์ผู้ใช้ หากไม่พบโปรไฟล์จะคืนค่าเป็นหน้า Not Found
หน้าจอและองค์ประกอบหลัก
Layout และหน้า index
- Layout ของ segment (
src/app/[hash]/layout.tsx) รวมทั้งgenerateMetadata()และฟังก์ชันดึงข้อมูล OA ฝั่งเซิร์ฟเวอร์ - หน้า index (
src/app/[hash]/page.tsx) เป็นหน้าตรวจ auth และแสดงโปรไฟล์
การ resolve ข้อมูล OA
useFetchGetLineOaByHash(hash)(src/hooks/use-fetch-line-oa-by-hash.ts) เป็น hook ฝั่ง client- LINE OA service (
src/service/line-oa.service.ts) รองรับทั้งรูปแบบใหม่ที่คืนlineLiffIdมาตรง ๆ และรูปแบบเดิมที่ซ้อนอยู่ใต้ออบเจ็กต์ข้อมูลล็อกอิน - Endpoint ที่ใช้คือ
GET /line-oa/get-by-hash/:hash - Environment variable ที่เกี่ยวข้อง:
BASE_API_INTERNAL_URL,NEXT_PUBLIC_BASE_API_CLIENT_URL,NEXT_PUBLIC_APP_URL,NEXT_PUBLIC_SEO_TITLEและNEXT_PUBLIC_SEO_DESCRIPTION
เส้นทางทั้งหมดภายใต้ segment นี้
| Route | ฟีเจอร์ |
|---|---|
/:hash | หน้าตรวจสอบสิทธิ์และแสดงโปรไฟล์ |
/:hash/form | หน้า catch สำหรับรับ liff.state แล้ว redirect ไปยังฟอร์ม |
/:hash/form/:id | หน้ากรอกฟอร์ม |
/:hash/form/:id/thank-you | หน้าขอบคุณหลังส่งฟอร์ม |
/:hash/content/:token | หน้าอ่านบทความ |
/:hash/pages/:linkToken | หน้ารายการเนื้อหาตาม content link |
/:hash/menu/:token | หน้าเมนูที่สร้างจาก menu builder |
/:hash/friend-track | แคมเปญเพิ่มเพื่อน โดยค้นหาแคมเปญจาก hash |
/:hash/friend-track/:token | แคมเปญเพิ่มเพื่อนที่ระบุด้วย token |
/:hash/bulletin | บอร์ดประกาศ |
/:hash/bulletin/:postId | รายละเอียดประกาศ |
/:hash/loyalty | บัตรสะสมแต้มสำหรับลูกค้า |
/:hash/loyalty-staff | เครื่องมือสำหรับพนักงาน |
/:hash/booking/:token | การจองนัดหมาย |
/:hash/r/:id | tracking redirect |
เส้นทางที่อยู่นอก segment นี้
หน้าแรก (/), หน้า Not Found, หน้า verify-line-login, campaign redirect proxy ที่ /c/:token, หน้า sandbox สำหรับทดลอง Ant Design และ API route ตัวอย่างของ Sentry
จุดเชื่อมต่อกับฟีเจอร์อื่น
- เชื่อมกับ การล็อกอิน LINE ผ่าน LIFF โดยตรง เพราะทุกหน้าต้องเริ่มจากการ resolve LIFF ID ที่ segment นี้
generateMetadata()ใช้fetchของ Next.js แทน axios เพื่อให้สามารถกำหนด revalidate interval ได้- ค่า
formLiffIdถูกใช้เฉพาะใน กรอกฟอร์ม และ หน้าขอบคุณหลังส่งฟอร์ม - ค่า
botBasicIdถูกใช้ใน หน้าขอบคุณหลังส่งฟอร์ม และ แคมเปญติดตามการเพิ่มเพื่อน
รายละเอียดฝั่ง Backend (Client API)
endpoint GET /api/line-oa/get-by-hash/:hash เป็น endpoint แรกที่ทุกหน้า LIFF เรียก และเป็นเส้นทางเดียวที่ออกแบบให้ ไม่ต้องยืนยันตัวตนเลย เพราะหน้าเว็บยังไม่มี LIFF ID จึงยังไม่สามารถทำ liff.init() เพื่อขอ token ได้ — เป็นปัญหาไก่กับไข่ที่ endpoint นี้แก้ให้
กฎการค้นหาที่ทำให้เกิดอาการ "resolve ได้แต่ล็อกอินไม่ผ่าน"
จุดนี้สำคัญและมักทำให้เข้าใจผิด: backend ค้น OA ด้วยเงื่อนไขที่ หลวมกว่า ขั้นตอน verify token
- endpoint นี้ยอมรับ OA ที่ยังไม่ถูกลบ (ไม่ได้บังคับว่าต้อง active)
- แต่ขั้นตอน verify token ของ การล็อกอิน LINE ผ่าน LIFF บังคับว่า OA ต้อง active
- ผลคือ OA ที่ถูกปิดใช้งาน (inactive) จะ resolve hash สำเร็จ หน้าเว็บได้ LIFF ID ไปแล้ว
liff.init()ผ่าน แต่พอเรียก API จริงจะได้ 401Invalid channel— อาการดูเหมือนปัญหา auth แต่ต้นเหตุอยู่ที่สถานะของ OA - hash ที่ไม่มีอยู่จริงจะได้ 400 (ไม่ใช่ 404) พร้อม message เป็นรหัส
APP_007ตาม parity กับระบบเดิม ฝั่งเว็บจึงควรจับที่ status 400 ของ endpoint นี้ว่า "OA ไม่พบ"
รูปแบบค่าที่ตอบกลับ และเหตุผลที่หลายฟิลด์เป็น null ได้
lineLiffIdและformLiffIdอ่านมาจากข้อมูลตั้งค่า LINE Login ของ OA โดย ค่าว่างหรือ key ที่ไม่มีอยู่จะกลายเป็นnullไม่ใช่ string ว่าง — ฝั่งเว็บต้องเช็ค null ก่อนใช้ ไม่ควรพึ่ง truthiness ของ string เพียงอย่างเดียวformLiffIdเป็นnullได้ตามปกติ (OA ที่ไม่ได้ตั้ง LIFF แยกสำหรับฟอร์ม) กรณีนั้นหน้าฟอร์มต้องใช้ LIFF ID หลักimageUrlเกิดจากการแปลง path ของรูปปกใน storage ให้เป็น public URL โดยมีกฎรอบคอบไว้แล้ว: path ว่าง →null(ไม่ยิง URL ที่ชี้ไป bucket root เปล่าๆ ซึ่งจะทำให้ og:image เสีย), ค่าที่เป็น absolute URL อยู่แล้วจะส่งผ่านไปตรงๆ ไม่แปลงซ้ำ และถ้าระบบไม่ได้ตั้งค่า storage ไว้จะได้nullโดยไม่ errorbotBasicIdคือ@-id สาธารณะ ใช้ประกอบลิงก์ห้องแชท OA รูปแบบhttps://line.me/R/ti/p/<id>ซึ่งจำเป็นตอนเปิดหน้าขอบคุณใน browser ภายนอก (นอกแอป LINE)nameคือชื่อ OA ที่หน้าเว็บนำไปใช้เป็น document title และ OG title
ข้อสังเกตด้าน security
- endpoint นี้ เปิดสาธารณะโดยเจตนา จึงคืนเฉพาะฟิลด์ที่ปลอดภัยจริง — ทุกค่าที่ส่งกลับเป็นข้อมูลที่ปรากฏบนหน้า LINE สาธารณะของ OA อยู่แล้ว
- ห้ามเพิ่ม credential ใดๆ (channel secret, access token, ค่า config ภายใน) ลงใน response นี้เด็ดขาด — ถ้าต้องการข้อมูลเพิ่มสำหรับหน้าเว็บ ต้องไปสร้าง endpoint ที่ต้อง verify token แยก
- การที่ hash ถูกเดาได้ไม่ถือเป็นช่องโหว่ในตัวเอง เพราะทุกเส้นทางที่มีข้อมูลจริงยังต้องผ่าน channel binding อีกชั้น
Edge case ที่ควรรู้
- endpoint นี้ไม่มีการ cache ฝั่ง backend — การ cache 300 วินาทีที่เห็นในเอกสารนี้เป็นของ
generateMetadata()ฝั่ง Next.js เท่านั้น ดังนั้นถ้าแอดมินแก้ชื่อหรือรูปปก OA ค่าใหม่จะมาถึงหน้า client ทันที แต่ OG metadata อาจยังเป็นค่าเดิมจนกว่า cache จะหมดอายุ - ข้อมูลตัวเดียวกันนี้ยังถูกใช้ต่อโดยฟีเจอร์อื่นผ่านการ resolve hash ของโดเมนตัวเอง (บอร์ดประกาศ, บัตรสะสมแต้ม) ซึ่งอาจมีเงื่อนไขสถานะ OA ต่างจาก endpoint นี้เล็กน้อย