Skip to main content

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

  1. The middleware is attached with Use() on route groups bound to :hash. Today it covers two groups: /bulletin/:hash (appID bulletin) and /loyalty/:hash (appID loyalty).
  2. If the path carries no :hash, the request passes through untouched.
  3. A single SQL statement resolves line_oa from line_oa_hash (requiring status <> 'delete' and no soft delete), then LEFT JOINs line_oa_app on (organization_id, app_id) and returns COALESCE(a.enabled, false). A missing line_oa_app row therefore reads as "disabled" instead of dropping the OA from the result set entirely.
  4. The decision philosophy has two branches:
    • An explicit enabled = false returns 403 with the message APP_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.
  5. The guard covers both the customer-facing and staff-facing sides of loyalty: disabling the app also disables the staff device.

Key Files & Functions

ItemValue
Fileinternal/appguard/appguard.go
FunctionsAppEnabledGuard(db *sqlx.DB, appID string) gin.HandlerFunc and isEnabled
ConstantErrAppDisabled = "APP_DISABLED"
SQLappEnabledSQL (a LEFT JOIN between line_oa and line_oa_app)
Attachment pointsinternal/bulletin/register.go calls g.Use(appguard.AppEnabledGuard(deps.DB, "bulletin")); internal/loyalty/register.go uses loyalty

Connections to Other Services

  • The line_oa and line_oa_app tables (organization_id, app_id, enabled).
  • The CMS has a matching guard, apps.AppEnabledGuard in cms-api-go; both sides must use the same set of app_id values.
  • 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 :token rather than :hash.
  • The related client-web features are bulletin-board and loyalty-card, both of which must render a screen for the APP_DISABLED response.