ด่านเปิด/ปิดแอปรายองค์กร (App Enabled Guard)
ภาพรวม
Platform admin สามารถเปิดหรือปิด "แอป" อย่าง bulletin, loyalty และ appointment ให้แต่ละองค์กรได้ผ่านตาราง line_oa_app ฝั่ง CMS บังคับกฎนี้อยู่แล้วทั้งการซ่อนเมนูและ guard บน API แต่ ลิงก์ LIFF สาธารณะเดิมกลับไม่มีการตรวจสอบเลย ทำให้แอปที่ถูกปิดยังใช้งานได้ตามปกติจากฝั่งลูกค้า
appguard.AppEnabledGuard คือ middleware ที่ปิดช่องโหว่นี้ที่ระดับ client-api ซึ่งเป็นชั้นที่ client-web ข้ามไปไม่ได้
Business Flow
- middleware ถูกติดตั้งด้วย
Use()บน route group ที่ผูกกับ:hashปัจจุบันใช้กับ 2 กลุ่มคือ/bulletin/:hash(appID เป็นbulletin) และ/loyalty/:hash(appID เป็นloyalty) - หากไม่มี
:hashใน path จะปล่อยผ่านทันที - รัน SQL เพียงคำสั่งเดียว โดย resolve
line_oaจากline_oa_hash(ต้องมีstatus <> 'delete'และไม่ถูก soft-delete) แล้วLEFT JOIN line_oa_appด้วยคู่(organization_id, app_id)และคืนค่าCOALESCE(a.enabled, false)ทำให้แถวline_oa_appที่ไม่มีถูกอ่านว่า "ปิด" แทนที่จะทำให้ OA หลุดจากผลลัพธ์ไปเลย - ปรัชญาการตัดสินแบ่งเป็น 2 กรณี
- เมื่อ
enabled = falseอย่างชัดเจน จะตอบ 403 พร้อม messageAPP_DISABLED(fail closed) ซึ่ง client-web จะแปลงโค้ดนี้เป็นหน้า "ฟีเจอร์นี้ยังไม่เปิดใช้งาน" ดังนั้นสตริงนี้ห้ามเปลี่ยน - เมื่อเกิด lookup error หรือ hash ไม่ตรงกับ OA ใด (
sql.ErrNoRows) จะ fail open ปล่อยผ่านให้ handler ตัดสินเอง ซึ่งโดยปกติจะตอบ 404 เหตุผลคือ governance lookup ที่สะดุดชั่วคราวไม่ควรล็อกลูกค้าออกจากแอปที่เปิดใช้งานอยู่
- เมื่อ
- guard ตัวนี้ครอบทั้งฝั่งลูกค้าและฝั่งพนักงานของ loyalty กล่าวคือเมื่อปิดแอปแล้ว เครื่องของพนักงานก็ใช้งานไม่ได้เช่นกัน
ไฟล์และฟังก์ชันหลัก
| รายการ | ค่า |
|---|---|
| ไฟล์ | internal/appguard/appguard.go |
| ฟังก์ชัน | AppEnabledGuard(db *sqlx.DB, appID string) gin.HandlerFunc และ isEnabled |
| ค่าคงที่ | ErrAppDisabled = "APP_DISABLED" |
| SQL | appEnabledSQL (LEFT JOIN ระหว่าง line_oa และ line_oa_app) |
| จุดติดตั้ง | internal/bulletin/register.go เรียก g.Use(appguard.AppEnabledGuard(deps.DB, "bulletin")) และ internal/loyalty/register.go ใช้ค่า loyalty |
จุดเชื่อมต่อกับ Service อื่น
- ตาราง
line_oaและline_oa_app(คอลัมน์organization_id,app_id,enabled) - ฝั่ง CMS มี guard คู่กันชื่อ
apps.AppEnabledGuardใน cms-api-go ซึ่งทั้งสองฝั่งต้องใช้ชุดapp_idเดียวกัน - feature ที่ถูกครอบด้วย guard นี้ ได้แก่ กลุ่ม bulletin ทั้งหมด (feed, การเขียนโพสต์, คอมเมนต์, reaction และ report) และกลุ่ม loyalty ทั้งหมด (บัตรสะสมแต้ม, การสะสมแต้ม, การแลกรางวัล และฝั่งพนักงาน)
- ข้อควรทราบ: เส้นทาง
/appointment/public/*ยังไม่มี guard ตัวนี้ เนื่องจาก route ใช้:tokenไม่ใช่:hash - ฝั่ง client-web ที่เกี่ยวข้องคือ feature
bulletin-boardและloyalty-cardซึ่งต้องรองรับการแสดงหน้าจอเมื่อได้รับAPP_DISABLED