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.
  • Tablescontent_page, content_page_translation, content_category, content_subcategory, content_link, menu_builder, and line_oa
  • Middlewaremiddleware.Throttle(10, 60000) (see the Core HTTP Platform)
  • LIFFliff.go validates x-liff-token and returns the configuration to client-web.
  • Consumer — client-web-2026
  • Related modulesContent Page, Content Link, Menu Builder, and Content Category.