diff --git a/assets/css/swagger.css b/assets/css/swagger.css new file mode 100644 index 0000000000..fe4e48d911 --- /dev/null +++ b/assets/css/swagger.css @@ -0,0 +1,368 @@ +/* ------------------------- + OpenAPI / Swagger UI + Hextra dark: #111 bg, neutral grays +------------------------- */ + +/* --- Color palette (dark mode) --- */ +.dark .swagger-ui { + --sw-text: #d1d5db; + --sw-text-bright: #f3f4f6; + --sw-text-muted: #9ca3af; + --sw-text-faint: #6b7280; + --sw-bg-panel: #171717; + --sw-bg-deep: #111; + --sw-bg-raised: #1a1a1a; + --sw-bg-input: #0a0a0a; + --sw-border: #262626; + --sw-border-faint: #1a1a1a; + --sw-border-strong: #404040; + --sw-accent: #2563eb; + --sw-accent-hover: #1d4ed8; +} + +/* --- Per-method colors --- */ +.dark .swagger-ui .opblock-get { --method: 22, 163, 74; } +.dark .swagger-ui .opblock-post { --method: 37, 99, 235; } +.dark .swagger-ui .opblock-put { --method: 217, 119, 6; } +.dark .swagger-ui .opblock-delete { --method: 220, 38, 38; } +.dark .swagger-ui .opblock-patch { --method: 147, 51, 234; } + +/* Deprecated method badge — darker gray for better contrast */ +.swagger-ui .opblock.opblock-deprecated .opblock-summary-method { + background: #525252; +} + +/* --- Hidden elements (no live server connection) --- */ +#swagger-ui .information-container, +.scheme-container, +.filter-container, +.swagger-ui .parameters-container input, +.swagger-ui .parameters-container select { + display: none; +} + +/* Code blocks — prevent Hextra's pre/code styles from interfering + with Swagger's own microlight syntax highlighting */ +.swagger-ui pre, +.swagger-ui code { + background-color: unset; + border: unset; + color: unset; +} + +/* --- Dark mode (nested) --- */ + +.dark .swagger-ui { + color: var(--sw-text); + + /* Bright text — headings, labels, active tabs */ + & .info .title, + & .opblock-tag, + & .opblock-section-header h4, + & .opblock-section-header label, + & .parameter__name, + & .response-col_status, + & .model-title, + & table thead tr th, + & table thead tr td, + & section.models h4, + & .tab li.active { + color: var(--sw-text-bright); + } + + /* Muted text — types, secondary info, inactive tabs */ + & .info .base-url, + & .parameter__type, + & .parameter__in, + & .prop-type, + & .tab li { + color: var(--sw-text-muted); + } + + /* Body text — override Swagger's dark-on-light defaults */ + & .opblock-summary-description, + & .opblock-summary-path, + & .opblock-description-wrapper p, + & .renderedMarkdown p, + & .markdown p, + & .markdown pre, + & .response-col_description, + & .model, + & table tbody tr td { + color: var(--sw-text); + } + + /* --- Backgrounds --- */ + + & .opblock { + background: var(--sw-bg-panel); + border-color: var(--sw-border); + } + + & .opblock-body { + background: var(--sw-bg-deep); + } + + & .opblock-section-header { + background: var(--sw-bg-raised); + border-bottom: 1px solid var(--sw-border); + box-shadow: none; + } + + /* Tinted per-method backgrounds */ + & .opblock.opblock-get, + & .opblock.opblock-post, + & .opblock.opblock-put, + & .opblock.opblock-delete, + & .opblock.opblock-patch { + background: rgba(var(--method), 0.05); + border-color: rgba(var(--method), 0.3); + } + + /* Containers sharing the same treatment */ + & .scheme-container, + & .model-box, + & section.models .model-container, + & .filter-container { + background: var(--sw-bg-panel); + border-color: var(--sw-border); + } + + & section.models { + border-color: var(--sw-border); + } + + /* Clear default white backgrounds */ + & .responses-wrapper, + & .responses-inner, + & .parameters-container { + background: transparent; + } + + /* --- Borders --- */ + + & .opblock-tag, + & .opblock .opblock-summary, + & table thead tr th, + & table thead tr td { + border-bottom-color: var(--sw-border); + } + + & table tbody tr td, + & .response { + border-bottom-color: var(--sw-border-faint); + } + + /* --- HTTP method badges --- */ + + & .opblock .opblock-summary-method { + color: var(--sw-text-bright); + } + + & .opblock.opblock-get .opblock-summary-method, + & .opblock.opblock-post .opblock-summary-method, + & .opblock.opblock-put .opblock-summary-method, + & .opblock.opblock-delete .opblock-summary-method, + & .opblock.opblock-patch .opblock-summary-method { + background: rgb(var(--method)); + } + + /* --- Form controls --- */ + + & select, + & textarea, + & input[type=text], + & input[type=password], + & input[type=search] { + background: var(--sw-bg-input); + color: var(--sw-text); + border: 1px solid var(--sw-border); + } + + & select { + background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='%23D1D5DB' d='m8 10.293 3.146-3.147.708.708L8 11.707 4.146 7.854l.708-.708L8 10.293z'/%3E%3C/svg%3E") !important; + background-repeat: no-repeat !important; + background-position: right 16px center !important; + background-size: 14px 14px !important; + padding-right: 40px !important; + } + + /* --- Buttons --- */ + + & .btn { + background: var(--sw-border); + color: var(--sw-text-bright); + border: 1px solid var(--sw-border-strong); + } + + & .btn:hover { + background: var(--sw-border-strong); + } + + & .btn.authorize { + background: var(--sw-accent); + border-color: var(--sw-accent); + color: #fff; + } + + & .btn.authorize:hover { + background: var(--sw-accent-hover); + } + + /* --- SVG icons & tab buttons --- */ + + & svg { + fill: var(--sw-text); + } + + & .btn svg { + fill: currentColor; + } + + & .tab li button.tablinks { + color: inherit; + background: transparent; + } + + /* --- Button focus outline --- */ + + & button:focus { + outline: 1px solid var(--sw-border-strong); + } + + /* --- JSON Schema 2020-12 --- */ + + & .json-schema-2020-12 { + background-color: rgba(255, 255, 255, 0.03); + } + + & .model-box .json-schema-2020-12, + & .json-schema-2020-12--embedded { + background-color: transparent; + } + + /* Titles and labels */ + & .json-schema-2020-12__title, + & .json-schema-2020-12-expand-deep-button { + color: var(--sw-text-bright); + } + + /* Primary text (property names, values) */ + & .json-schema-2020-12-keyword__name--primary, + & .json-schema-2020-12-keyword__value--primary, + & .json-schema-2020-12-json-viewer__name--primary, + & .json-schema-2020-12-json-viewer__value--primary, + & .json-schema-2020-12__attribute, + & .json-schema-2020-12-property .json-schema-2020-12__title { + color: var(--sw-text); + } + + /* Secondary / muted text */ + & .json-schema-2020-12-keyword__name--secondary, + & .json-schema-2020-12-keyword__value, + & .json-schema-2020-12-keyword__value--secondary, + & .json-schema-2020-12-json-viewer__name--secondary, + & .json-schema-2020-12-json-viewer__value, + & .json-schema-2020-12-json-viewer__value--secondary, + & .json-schema-2020-12-keyword--description { + color: var(--sw-text-muted); + } + + /* Extension text */ + & .json-schema-2020-12-keyword__name--extension, + & .json-schema-2020-12-keyword__value--extension, + & .json-schema-2020-12-json-viewer__name--extension, + & .json-schema-2020-12-json-viewer__value--extension { + color: var(--sw-text-faint); + } + + /* Primary attribute (type labels) */ + & .json-schema-2020-12__attribute--primary { + color: #60a5fa; + } + + /* Dashed borders */ + & .json-schema-2020-12-keyword__children, + & .json-schema-2020-12-json-viewer__children, + & .json-schema-2020-12-body, + & .json-schema-2020-12-keyword--$vocabulary ul { + border-left-color: rgba(255, 255, 255, 0.1); + } + + /* Accordion buttons */ + & .json-schema-2020-12-accordion { + background-color: transparent; + } + + /* --- Links --- */ + + & a { + color: hsl(var(--primary-hue) var(--primary-saturation) var(--primary-lightness)); + } + + /* --- x-extensions (dark) --- */ + + & .x-since { + background: rgba(34, 197, 94, 0.08); + border-color: rgba(34, 197, 94, 0.25); + color: #86efac; + } + + & .x-permissions { + background: rgba(99, 102, 241, 0.08); + border-color: rgba(99, 102, 241, 0.25); + color: #c7d2fe; + } + + & .x-permissions__pill { + background: rgba(99, 102, 241, 0.15); + border-color: rgba(99, 102, 241, 0.3); + } +} + +/* --- x-extensions (light) --- */ + +.x-extensions { + display: flex; + flex-wrap: wrap; + align-items: flex-start; + gap: 0.5rem; + margin: 0.75rem 1.25rem 1rem; +} + +.x-since { + padding: 0.625rem 0.875rem; + background: #f0fdf4; + border: 1px solid #bbf7d0; + border-radius: 6px; + font-size: 0.8rem; + font-weight: 600; + color: #166534; +} + +.x-permissions { + padding: calc(0.625rem - 1px) 0.875rem; + background: #eff6ff; + border: 1px solid #bfdbfe; + border-radius: 6px; + font-size: 0.8rem; + color: #1e3a8a; +} + +.x-permissions__line { + display: block; +} + +.x-permissions__label { + font-weight: 600; +} + +.swagger-ui .x-permissions__pill { + display: inline-block; + padding: 0 0.5em; + margin: 0 0.25em; + background: #dbeafe; + border: 1px solid #93c5fd; + border-radius: 4px; + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; +} diff --git a/assets/js/swagger.js b/assets/js/swagger.js new file mode 100644 index 0000000000..7c994cc9fd --- /dev/null +++ b/assets/js/swagger.js @@ -0,0 +1,88 @@ +const buildPermissionPill = (text) => { + const pill = document.createElement('code'); + pill.className = 'x-permissions__pill'; + pill.textContent = text; + return pill; +}; + +const buildPermissionsBadge = (perms) => { + const badge = document.createElement('div'); + badge.className = 'x-permissions'; + const line = document.createElement('span'); + line.className = 'x-permissions__line'; + + const label = document.createElement('span'); + label.className = 'x-permissions__label'; + label.textContent = 'Permissions - '; + line.appendChild(label); + + (perms.hasAll || []).forEach(p => line.appendChild(buildPermissionPill(p))); + + if (perms.hasAny?.length) { + line.appendChild(document.createTextNode(' any of: (')); + perms.hasAny.forEach(p => line.appendChild(buildPermissionPill(p))); + line.appendChild(document.createTextNode(')')); + } + + badge.appendChild(line); + return badge; +}; + +const buildSinceBadge = (version) => { + const badge = document.createElement('div'); + badge.className = 'x-since'; + badge.textContent = `Added in ${version}`; + return badge; +}; + +const extendDescriptionBlock = (perms, since) => (node) => { + const descWrapper = node?.querySelector('.opblock-description-wrapper'); + if (!descWrapper || descWrapper.querySelector('.x-extensions')) { + return; + } + + const container = document.createElement('div'); + container.className = 'x-extensions'; + if (since) { + container.appendChild(buildSinceBadge(since)); + } + if (perms) { + container.appendChild(buildPermissionsBadge(perms)); + } + descWrapper.prepend(container); +}; + +const XExtensionsPlugin = () => ({ + wrapComponents: { + operation: (Original, system) => (props) => { + const h = system.React.createElement; + const op = props.operation.get('op'); + const perms = op.get?.('x-permissions')?.toJS(); + const since = op.get?.('x-since'); + + if (!perms && !since) { + return h(Original, props); + } + + return h('div', { ref: extendDescriptionBlock(perms, since) }, h(Original, props)); + } + } +}); + +window.addEventListener('DOMContentLoaded', () => { + SwaggerUIBundle({ + dom_id: '#swagger-ui', + // TODO Update + url: 'https://gist.githubusercontent.com/jkuester/a738de6aa6f96e5957b1f4ce56a3692a/raw/openapi.json', + presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset], + plugins: [XExtensionsPlugin], + filter: true, + operationsSorter: 'alpha', + tagsSorter: 'alpha', + docExpansion: 'none', + deepLinking: true, + supportedSubmitMethods: [], + defaultModelsExpandDepth: -1, + defaultModelExpandDepth: 3 + }); +}); diff --git a/content/en/building/reference/app-settings/token_login.md b/content/en/building/reference/app-settings/token_login.md index 018a4bc1a5..eb72897995 100644 --- a/content/en/building/reference/app-settings/token_login.md +++ b/content/en/building/reference/app-settings/token_login.md @@ -11,6 +11,34 @@ aliases: - /apps/reference/app-settings/token_login --- +When creating or updating a user, sending a truthy value for the field `token_login` enables Login by SMS for this user. +This action resets the user's password to an unknown string and generates a complex 64-character token, used to generate a token-login URL. +The URL is sent to the user's phone number by SMS, along with another (configurable) SMS that can contain additional information. +Accessing this link before its expiration time logs the user in directly - without the need of any other credentials. +The link can only be accessed once, and the token becomes invalid after one login. +The token expires in 24 hours, after which logging in is only possible by either generating a new token, or disabling `token_login` and manually setting a password. + +The SMS messages are stored in a doc of type `login_token`. These docs cannot be viewed as reports from the webapp, and can only be edited by admins, but their messages are visible in the Admin Message Queue page. + +To disable login by SMS for a user, update the user sending `token_login` with a `false` value. +To regenerate the token, update the user sending `token_login` with a `true` value. + +| `token_login` | user state | action | +|---------------|----------------------|------------------------------------------------------------------------------------------------------------------------| +| undefined | new | None | +| undefined | existing, no token | None | +| undefined | existing, with token | None. Login by SMS remains enabled. Token is unchanged. | +| true | new | Enables Login by SMS. Generates token and sends SMS. | +| true | existing, no token | Resets password. Enables Login by SMS. Generates token and sends SMS. Invalidates existing sessions. | +| true | existing, with token | Resets password. Enables Login by SMS. Generates new token and sends SMS. Invalidates old token and existing sessions. | +| false | new | None | +| false | existing, no token | None | +| false | existing, with token | Requires a password. Disables Login by SMS. Invalidates old token and existing sessions. | + +{{< see-also page="building/login" anchor="remote-login" >}} + +## Configuration + Login via SMS settings are defined under the `token_login` key, as an object supporting the following properties: {{< callout >}} diff --git a/content/en/building/reference/openapi.md b/content/en/building/reference/openapi.md new file mode 100644 index 0000000000..6c36924606 --- /dev/null +++ b/content/en/building/reference/openapi.md @@ -0,0 +1,9 @@ +--- +title: "REST API to interact with CHT Applications" +description: RESTful Application Programming Interfaces for integrating with CHT applications +linkTitle: "OpenAPI" +weight: 2 +toc: false +--- + +{{< swagger >}} diff --git a/layouts/shortcodes/swagger.html b/layouts/shortcodes/swagger.html new file mode 100644 index 0000000000..506d952d12 --- /dev/null +++ b/layouts/shortcodes/swagger.html @@ -0,0 +1,15 @@ + + + + + +{{- $swaggerCss := resources.Get "css/swagger.css" -}} + + +
+ + + + +{{- $swaggerJs := resources.Get "js/swagger.js" -}} +