Skip to main content

Public API for End Users

Overview​

The publicmod module gathers the endpoints that ordinary LINE users can call without logging into the CMS. Its main consumer is client-web, which uses them to render content pages and menus shared through LINE.

Because these endpoints are publicly reachable, the ones that return content listings are wrapped in a rate limit of 10 requests per 60 seconds. Token-addressed endpoints are not throttled, since the token itself is unguessable.

The module also supports LIFF: it returns the configuration client-web needs to initialize LIFF, and it accepts an x-liff-token header to identify the LINE user currently viewing the page.

Business Flow​

Browsing Content Listings​

  1. GET /api/public/contents, with query parameters defined by PublicContentQueryDto, returns published content as a payload containing data, total, page, limit, and totalPages. Throttled at 10 requests per 60 seconds.
  2. GET /api/public/contents/categories, with a lineOaId query parameter, returns the categories that currently hold content. Throttled the same way.

Opening by Token​

  1. GET /api/public/menu/:token opens a menu created in Menu Builder.
  2. GET /api/public/content/:token/liff-config returns that page's LIFF configuration so client-web can call liff.init() before loading content.
  3. GET /api/public/content/:token, with an optional password query parameter, opens a content page.
    • If the x-liff-token header is present, it identifies the LINE user so the visit can be recorded against a known person.
    • Password-protected pages require the correct password.
    • A missing or unpublished page returns error APP_007.

Soft-Delete Parity Worth Knowing​

  1. The content_page, content_category, and content_subcategory tables use a plain deleted_date column, so the service must add the filtering predicate itself — TypeORM does not do it automatically.
  2. The line_oa table has a real @DeleteDateColumn, so GORM filters it automatically. The deletedAt: IsNull() condition that appears in the original TypeScript where clause points at a property that does not exist and is therefore a no-op.

Key Files & Functions​

The code lives in internal/modules/publicmod/, made up of controller.go, service.go, liff.go, and dto.go.

MethodRouteHandlerGuard
GET/api/public/contentsct.listPublicContentspublic + Throttle(10, 60000)
GET/api/public/contents/categoriesct.listPublicCategoriespublic + Throttle(10, 60000)
GET/api/public/menu/:tokenct.getMenuByTokenpublic
GET/api/public/content/:token/liff-configct.getContentLiffConfigpublic
GET/api/public/content/:tokenct.getContentByTokenpublic

Static routes are registered before the :token routes so gin resolves to the correct handler. RegisterRoutes uses only the public group; the _ = authed line confirms the authed group is intentionally unused.

Several parallel public endpoints live in other modules:

RouteModule
GET /api/public/content-pages/:tokenContent Page
POST /api/public/content-pages/:token/trackContent Page
GET /api/menu-builder/public/:tokenMenu Builder

Connections to Other Services​

  • Access control — None; everything here is public. Protection comes from unguessable tokens plus throttling.
  • Tables — content_page, content_page_translation, content_category, content_subcategory, content_link, menu_builder, and line_oa
  • Middleware — middleware.Throttle(10, 60000) (see the Core HTTP Platform)
  • LIFF — liff.go validates x-liff-token and returns the configuration to client-web.
  • Consumer — client-web-2026
  • Related modules — Content Page, Content Link, Menu Builder, and Content Category.