OTP Verification Settings
Overview
The OTP Verification Settings page (/otp-config) stores the credentials and message templates used to deliver one-time passcodes for a single LINE OA. It does not decide which forms require verification — that is configured in the Form Builder verification tab. Instead, it is the underlying layer that answers the question: through which channels can this LINE OA send an OTP?
The page is aimed at system administrators who configure the account once. Only after that can form owners enable verification on their individual forms.
Two channels are supported:
| Channel | Provider | Required information |
|---|---|---|
| SMS | ThaiBulkSMS | API key and secret |
| The channel-level SMTP server | Email subject and an email body containing the {otp} placeholder |
Characteristics worth knowing:
- The configuration is a single record per LINE OA. There is no list, no create, and no delete — only read the current values and save over them.
- The form always renders, even before anything has been configured; default subject and body text are pre-filled.
- The SMS secret is only ever read back in masked form (for example
ab****yz) and is shown as the field's placeholder. Saving with the secret field left blank means "keep the stored value", not "clear it". - Each channel's readiness is summarised as a badge on its card — configured or not configured. This is the same status Form Builder uses to decide whether a channel can be selected.
- The menu item sits under the Settings group in the side menu and is also reachable from Quick Access.
A channel counts as ready under the following conditions:
| Channel | Considered ready when |
|---|---|
| SMS | The SMS switch is on, an API key is present, and a secret is stored |
The email switch is on, a subject is present, and the body contains {otp} |
Email template placeholders
| Placeholder | Required | Meaning |
|---|---|---|
{otp} | Yes | The generated one-time passcode; validated both in the UI and on the server |
{ref} | No | The reference code for that delivery |
A default subject and body are provided in the user's current language so the channel can be used without writing a template from scratch. The default body looks like this:
Your OTP code is <b>{otp}</b>. It expires in 3 minutes.
These defaults are applied only while the configuration is being loaded. If the interface language is switched while the page is open, reload it to pick up the defaults in the new language.
The copy shown on the respondent's verification screen — heading, description, verify button, and resend label — is not configured here. It is set per form in the Form Builder verification tab.
Business Flow
Configuring credentials
- Opening
/otp-configchecks the user'sVIEWpermission on the OTP Config module, sets the breadcrumb, and highlights the Settings menu. - The current configuration for the signed-in LINE OA is loaded, with a spinner shown until the data arrives; the form is not rendered before then.
- Once loaded, the form is populated with both channel switches, the SMS API key, and the email subject and body. If no subject or body has been saved yet, the standard wording for the user's language is used.
- The SMS secret field is always cleared to blank; the masked value appears as the field placeholder instead, signalling that it can be left untouched.
- The SMS card holds an enable switch, an API key field, and a masked secret field. The email card holds an enable switch, a subject field, and a multi-line body field with a hint listing the available placeholders.
- Client-side validation applies to the email channel only, and only while the email switch is on: the subject must not be empty, and the body must not be empty and must contain the
{otp}placeholder. - The SMS side has no client-side validation. Enabling the switch without filling in the key or secret still allows the save; the server rejects the request and its message is displayed to the user.
- Pressing save assembles the payload and writes over the existing configuration, with a loading state shown while the request is in flight.
- On success the result is reported and the configuration is re-read immediately, so the status badges and the masked secret reflect the latest values. The secret field returns to blank.
- On failure the API's message is surfaced verbatim — for example when SMS is enabled without a key or secret, or when the email body is missing the
{otp}placeholder. If the API returns no message, a standard error is shown instead.
Points to keep in mind:
- There is no unsaved-changes warning; leaving the page without saving discards every edit.
- There is no "send a test OTP" button and no delivery history in this module — readiness can only be confirmed from the status badges on the cards.
- The stored secret can never be read back from the UI. If it is lost, a new key and secret must be issued by the provider and both re-entered.
- Disabling a channel takes effect immediately for every form using it, so check for active forms before turning one off.
Working together with Form Builder
- An administrator configures the credentials here — the ThaiBulkSMS key and secret, and/or the email subject and body — enables the desired channels, and saves.
- The server recalculates each channel's readiness and the card badges switch to configured.
- A form owner opens the target form in Form Builder, enables Profile Mapping, and selects a Member Database. This is mandatory, because the OTP is delivered to a column on the matched member record, not to a value typed into the form.
- In the verification tab, turning the switch on seeds a single configuration row with a ready channel pre-selected; the owner then chooses the destination column.
- If no channel is ready, the tab shows a warning with a link back to this page and keeps the add-field button disabled. Unconfigured channels are also disabled in the channel dropdown.
- If a channel is disabled here at a later date, forms that already have OTP enabled keep their configuration and display a warning instead — the system does not switch off a form's OTP automatically.
Key Screens & Components
The page is a single form made up of two cards with a save button below them.
SMS card
- Status badge — whether the channel is ready, as computed on the server.
- Enable switch — turns SMS delivery on or off.
- API key field — the ThaiBulkSMS credential, readable back in plain form.
- Secret field — a masked input that shows the stored value's masked form as its placeholder; leave it blank to keep the current secret.
Email card
- Status badge — same as the SMS card.
- Enable switch — turns email delivery on or off.
- Subject field — required while the channel is enabled.
- Body field — a monospace multi-line input that accepts HTML directly (it is not a rich-text editor) and must contain the
{otp}placeholder. - Placeholder hint — an inline help label listing the placeholders available in the template.
Primary files: src/app/otp-config/page.tsx, src/components/otp-config/otp-config-form.tsx
API service
All calls live in src/services/otp-config.service.ts under the otp-config base path.
| Capability | Endpoint | Notes |
|---|---|---|
| Read the current configuration | GET /otp-config | One record per LINE OA; returns defaults when nothing has been saved, so a not-found case never occurs |
| Save the configuration | PUT /otp-config | Overwrites the record; a blank secret keeps the stored value; returns the same shape as the read |
The payload carries both channel switches, the SMS API key, the email subject and body, and the server-computed readiness flags. Reads return only the masked secret; the real secret is sent only on write.
Dependencies
- Permissions — this page is gated by the same
form-buildermodule as Form Builder. There is no separate OTP module, so disabling the Form Builder module removes this menu item as well. - Form Builder — the only consumer of this configuration on the CMS side. Its verification tab reads the channel readiness flags as a gate, while this page has no knowledge of which forms currently use OTP.
- Member Database — not directly involved here, but a prerequisite on the form side, since the passcode is delivered to a column on the matched member record.
- ThaiBulkSMS — the external SMS provider. Credentials are stored on the server; the CMS only ever sees the masked value.
- Channel-level SMTP settings — the email channel uses the mail server configured at channel level; this page only controls the subject and body.
- Shared infrastructure — the CMS authentication and HTTP client, which automatically scopes the configuration to the signed-in LINE OA (no OA identifier is passed in the URL), plus the breadcrumb, side menu, and the application-wide loading state.
Backend Details (CMS API)
The backend lives in internal/modules/otpconfig/. It is a newly written module rather than a port of an older system, structurally modelled on the Quick Reply module but differing in one key respect: it is a singleton, holding exactly one record per LINE OA rather than a set of rows.
Why there are only two endpoints
- The
otp_configtable enforces uniqueness on the LINE OA reference, so two configurations for one OA are impossible by construction. - Consequently there is no create endpoint and no delete endpoint — only
GET /api/otp-configto read andPUT /api/otp-config, which performs an upsert (updating the row if it exists, creating it if it does not). - Never having configured anything is treated as a normal state, not an error. The backend returns an empty/default structure rather than a "not found" response, which is why the UI can always render the form without needing an empty-state path.
Required permissions
- Both routes require the shared authentication check (JWT), and the token must carry a selected LINE OA, because that is what the backend uses to locate the right configuration row — no OA identifier is ever passed in the URL or body.
- Note — this module is not wrapped in a module gate, and the declared policy remains unenforced metadata.
- Interestingly, the backend declares its own separate
otp-configpolicy name (matched to the CMS menu name in anticipation of future enforcement), while the front end gates this menu behind theform-buildermodule permission instead. The two sides do not yet agree on how this page should be scoped.
What gets stored, and the side effects
- Everything lives in the single
otp_configtable. This module writes no cache and publishes no queue work. - Saving takes effect immediately for every form with OTP enabled, because forms do not keep their own copy of the credentials — they read from this table at runtime.
- The service that actually sends and verifies OTPs is
line-management-client-api-go, notcms-api. This page is only the credential and template store, so a successful save is no proof that the credentials can actually deliver. - Only once a respondent has verified their code does client-api accept the form submission. With OTP off, the form accepts submissions immediately with no verification step.
Edge cases worth knowing
- Because this is a whole-record upsert, every save replaces the entire configuration rather than patching individual fields. The rule that "leaving the secret blank keeps the stored value" is therefore logic the backend has to implement deliberately, not a natural consequence of overwriting.
- There is no test-send endpoint and no delivery history in this module, so the only way to confirm the credentials work is to submit a real form.
- The module has no knowledge of which forms use which channel, so disabling a channel produces no backend warning and never switches a form's OTP off automatically.
- Data is always scoped to the currently selected LINE OA, so switching OA in the CMS means looking at an entirely different configuration record.