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.
- Look up the
menu_builderrow bypublic_token— not found returns 404 withMenu not found. - If an
x-liff-tokenis present, callVerifyAndGetLineUser(token, menu.lineOaId)to obtainline_user.audience_ids. All failures are swallowed, leaving the user with only the public items. No 401 is returned; the endpoint degrades gracefully. - Normalize
audience_idsinto a slice of integers, accepting both an array of numbers and an array of objects carrying anidkey. A JSON null, or an object without anidkey, is discarded rather than coerced to 0. filterItemsByAudienceruns 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
audienceIdsis visible to everyone; an item that has them is visible when at least one id overlaps. - A
nodeitem — 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, andtextPosition.
- Assemble the response
{id, name, publicToken, lineOaHash, lineLiffId, config}, whereconfigcontainsitems,template,theme, andversion.templateandthemepass through as raw JSON so their original shape is preserved exactly.lineLiffIduses 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 whenline_login_infois absent, fails to parse, or lacks alineLiffIdkey (or holds JSON null there).
Key Files & Functions
| Item | Value |
|---|---|
| Route | GET /api/menu-builder/:token |
| Register | internal/menubuilder/register.go → Register(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 |
| Handler | internal/menubuilder/handler.go → (*Handler).FindOne |
| Service | internal/menubuilder/service.go → (*Service).FindOne, filterItemsByAudience, extractAudienceIDs |
| Repository | internal/menubuilder/repository.go → FindByPublicToken |
| Entity | internal/menubuilder/entity.go → MenuBuilder, MenuItemConfig, ItemTypeNode |
| Response | FilteredMenuResponse, responseConfig |
Connections to Other Services
- Database — the
menu_buildertable (jsonbconfig,public_token,line_oa_id) joined toline_oaforline_oa_hashandline_login_info - LIFF authentication — calls
VerifyAndGetLineUseroptionally - Related features — item destinations lead to the content page viewer via
contentPageTokenand to the content link viewer viacontentLinkToken - client-web — corresponds to the
menu-viewerfeature