Skip to main content

Public Menu (Menu Builder) with Audience Filtering

Overview

A multi-level menu designed by an administrator in the CMS and published via a publicToken. When a user opens the link, this endpoint returns the whole menu structure — template, theme, version, and the item tree — with items already filtered by the viewer's audience, server-side.

The important point: this filtering is a real gate, not just UI hiding. Items a user is not entitled to see are never sent at all — neither their labels nor their destination URLs.

Business Flow

GET /api/menu-builder/:token

The x-liff-token header is optional.

  1. Look up the menu_builder row by public_token — not found returns 404 with Menu not found.
  2. If an x-liff-token is present, call VerifyAndGetLineUser(token, menu.lineOaId) to obtain line_user.audience_ids. All failures are swallowed, leaving the user with only the public items. No 401 is returned; the endpoint degrades gracefully.
  3. Normalize audience_ids into a slice of integers, accepting both an array of numbers and an array of objects carrying an id key. A JSON null, or an object without an id key, is discarded rather than coerced to 0.
  4. filterItemsByAudience runs recursively:
    • Children are filtered first, descending the whole tree, while distinguishing "no children property at all" (the key disappears from the JSON) from "children present but empty" (an empty array is emitted) to match the original behaviour.
    • An item with no audienceIds is visible to everyone; an item that has them is visible when at least one id overlaps.
    • A node item — a folder — whose children are all filtered away is itself removed, and an absent children property counts as empty.
    • Surviving items are rebuilt with all 18 keys: id, name, type, url, contentPageToken, contentLinkToken, contentLinkBehavior, contentLinkDisplayStyle, audienceIds, children, icon, level, parentId, order, customStyle, image, showText, and textPosition.
  5. Assemble the response {id, name, publicToken, lineOaHash, lineLiffId, config}, where config contains items, template, theme, and version.
    • template and theme pass through as raw JSON so their original shape is preserved exactly.
    • lineLiffId uses nullish-coalescing semantics rather than a logical OR, so a stored empty string stays an empty string and does not become null. It is null only when line_login_info is absent, fails to parse, or lacks a lineLiffId key (or holds JSON null there).

Key Files & Functions

ItemValue
RouteGET /api/menu-builder/:token
Registerinternal/menubuilder/register.goRegister(r, deps), which constructs its own liff.Service in this file since it is the only consumer — server.Deps does not hold a shared instance
Handlerinternal/menubuilder/handler.go(*Handler).FindOne
Serviceinternal/menubuilder/service.go(*Service).FindOne, filterItemsByAudience, extractAudienceIDs
Repositoryinternal/menubuilder/repository.goFindByPublicToken
Entityinternal/menubuilder/entity.goMenuBuilder, MenuItemConfig, ItemTypeNode
ResponseFilteredMenuResponse, responseConfig

Connections to Other Services

  • Database — the menu_builder table (jsonb config, public_token, line_oa_id) joined to line_oa for line_oa_hash and line_login_info
  • LIFF authentication — calls VerifyAndGetLineUser optionally
  • Related features — item destinations lead to the content page viewer via contentPageToken and to the content link viewer via contentLinkToken
  • client-web — corresponds to the menu-viewer feature