Per-organization App Enable Guard
Overview
A platform admin can enable or disable "apps" — bulletin, loyalty, appointment — for each organization through the line_oa_app table. The CMS already enforces this by hiding menu entries and guarding its APIs, but the original public LIFF links had no check at all, so a disabled app remained fully usable from the customer side.
appguard.AppEnabledGuard is the middleware that closes this gap at the client-api layer, which client-web cannot bypass.
Business Flow
- The middleware is attached with
Use()on route groups bound to:hash. Today it covers two groups:/bulletin/:hash(appIDbulletin) and/loyalty/:hash(appIDloyalty). - If the path carries no
:hash, the request passes through untouched. - A single SQL statement resolves
line_oafromline_oa_hash(requiringstatus <> 'delete'and no soft delete), thenLEFT JOINsline_oa_appon(organization_id, app_id)and returnsCOALESCE(a.enabled, false). A missingline_oa_approw therefore reads as "disabled" instead of dropping the OA from the result set entirely. - The decision philosophy has two branches:
- An explicit
enabled = falsereturns 403 with the messageAPP_DISABLED(fail closed). client-web maps this code to a "this feature is not enabled yet" screen, so the string must never change. - A lookup error, or a hash that matches no OA (
sql.ErrNoRows), fails open and lets the handler decide — which normally means a 404. A momentarily failing governance lookup should not lock customers out of an app that is actually enabled.
- An explicit
- The guard covers both the customer-facing and staff-facing sides of loyalty: disabling the app also disables the staff device.
Key Files & Functions
| Item | Value |
|---|---|
| File | internal/appguard/appguard.go |
| Functions | AppEnabledGuard(db *sqlx.DB, appID string) gin.HandlerFunc and isEnabled |
| Constant | ErrAppDisabled = "APP_DISABLED" |
| SQL | appEnabledSQL (a LEFT JOIN between line_oa and line_oa_app) |
| Attachment points | internal/bulletin/register.go calls g.Use(appguard.AppEnabledGuard(deps.DB, "bulletin")); internal/loyalty/register.go uses loyalty |
Connections to Other Services
- The
line_oaandline_oa_apptables (organization_id,app_id,enabled). - The CMS has a matching guard,
apps.AppEnabledGuardin cms-api-go; both sides must use the same set ofapp_idvalues. - The guard covers the entire bulletin group (feed, post writing, comments, reactions, and reports) and the entire loyalty group (loyalty cards, earning, reward redemption, and the staff side).
- Note:
/appointment/public/*is not covered yet, because those routes key on:tokenrather than:hash. - The related client-web features are
bulletin-boardandloyalty-card, both of which must render a screen for theAPP_DISABLEDresponse.