From c04be7117ebb6118c8f8f49d3a373a93f0a38051 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Wed, 29 Apr 2026 15:57:10 +0000 Subject: [PATCH] feat: expose dcrouter gateway settings --- ts/opsserver/handlers/settings.handler.ts | 32 ++++ ts_web/elements/ob-view-settings.ts | 170 +++++++++++++++++++++- 2 files changed, 201 insertions(+), 1 deletion(-) diff --git a/ts/opsserver/handlers/settings.handler.ts b/ts/opsserver/handlers/settings.handler.ts index 9662df5..eab7bf8 100644 --- a/ts/opsserver/handlers/settings.handler.ts +++ b/ts/opsserver/handlers/settings.handler.ts @@ -2,6 +2,8 @@ import * as plugins from '../../plugins.ts'; import type { OpsServer } from '../classes.opsserver.ts'; import * as interfaces from '../../../ts_interfaces/index.ts'; import { requireAdminIdentity } from '../helpers/guards.ts'; +import { logger } from '../../logging.ts'; +import { getErrorMessage } from '../../utils/error.ts'; export class SettingsHandler { public typedrouter = new plugins.typedrequest.TypedRouter(); @@ -65,6 +67,12 @@ export class SettingsHandler { } } + if (this.hasExternalGatewaySetting(updates)) { + this.refreshExternalGateway().catch((error) => { + logger.warn(`External gateway settings refresh failed: ${getErrorMessage(error)}`); + }); + } + const settings = await this.getSettingsObject(); return { settings }; }, @@ -93,4 +101,28 @@ export class SettingsHandler { ), ); } + + private hasExternalGatewaySetting(settings: Partial): boolean { + return [ + 'dcrouterGatewayUrl', + 'dcrouterGatewayApiToken', + 'dcrouterWorkHosterId', + 'dcrouterTargetHost', + 'dcrouterTargetPort', + ].some((key) => Object.prototype.hasOwnProperty.call(settings, key)); + } + + private async refreshExternalGateway(): Promise { + const onebox = this.opsServerRef.oneboxRef; + await onebox.externalGateway.syncDomains(); + + const services = onebox.database.getAllServices().filter((service) => service.domain); + await Promise.all(services.map(async (service) => { + try { + await onebox.externalGateway.syncServiceRoute(service); + } catch (error) { + logger.warn(`Failed to sync external gateway route for ${service.domain}: ${getErrorMessage(error)}`); + } + })); + } } diff --git a/ts_web/elements/ob-view-settings.ts b/ts_web/elements/ob-view-settings.ts index 129e01b..615bf62 100644 --- a/ts_web/elements/ob-view-settings.ts +++ b/ts_web/elements/ob-view-settings.ts @@ -46,7 +46,100 @@ export class ObViewSettings extends DeesElement { public static styles = [ cssManager.defaultStyles, shared.viewHostCss, - css``, + css` + .gateway-card { + margin-bottom: 24px; + border: 1px solid var(--dees-color-border-subtle); + border-radius: 12px; + background: var(--dees-color-background, #ffffff); + overflow: hidden; + } + + .gateway-header { + padding: 16px 20px; + border-bottom: 1px solid var(--dees-color-border-subtle); + } + + .gateway-title { + font-size: 15px; + font-weight: 600; + color: var(--dees-color-text-primary); + } + + .gateway-subtitle { + margin-top: 4px; + font-size: 13px; + color: var(--dees-color-text-muted); + } + + .gateway-content { + padding: 20px; + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 16px; + } + + .gateway-field.full { + grid-column: 1 / -1; + } + + .field-label { + display: block; + margin-bottom: 6px; + font-size: 13px; + font-weight: 500; + color: var(--dees-color-text-secondary); + } + + input { + width: 100%; + box-sizing: border-box; + padding: 10px 12px; + border: 1px solid var(--dees-color-border-subtle); + border-radius: 8px; + background: transparent; + color: var(--dees-color-text-primary); + font-size: 14px; + } + + input:focus { + outline: none; + border-color: #3b82f6; + } + + .field-hint { + margin-top: 5px; + font-size: 12px; + color: var(--dees-color-text-muted); + } + + .gateway-footer { + display: flex; + justify-content: flex-end; + padding: 0 20px 20px; + } + + .save-button { + border: none; + border-radius: 8px; + background: #2563eb; + color: white; + cursor: pointer; + font-size: 13px; + font-weight: 600; + padding: 9px 14px; + } + + .save-button:hover { + background: #1d4ed8; + } + + @media (max-width: 700px) { + .gateway-content { + grid-template-columns: 1fr; + } + } + `, ]; async connectedCallback() { @@ -57,6 +150,7 @@ export class ObViewSettings extends DeesElement { public render(): TemplateResult { return html` Settings + ${this.renderExternalGatewaySettings()} `; } + + private renderExternalGatewaySettings(): TemplateResult { + const settings = this.settingsState.settings; + return html` +
+
+
External dcrouter Gateway
+
Delegate public WorkApp routing, DNS, and certificates to a dcrouter edge authority.
+
+
+ ${this.renderGatewayInput('dcrouterGatewayUrl', 'Gateway URL', settings?.dcrouterGatewayUrl || '', 'https://edge.example.com', 'Base URL of the dcrouter OpsServer.')} + ${this.renderGatewayInput('dcrouterGatewayApiToken', 'API Token', settings?.dcrouterGatewayApiToken || '', 'dcrouter API token', 'Requires workhosters and certificates scopes.', 'password')} + ${this.renderGatewayInput('dcrouterWorkHosterId', 'WorkHoster ID', settings?.dcrouterWorkHosterId || '', 'optional stable owner ID', 'Leave empty to let Onebox create a stable ID.')} + ${this.renderGatewayInput('dcrouterTargetHost', 'Target Host', settings?.dcrouterTargetHost || '', 'public or private host/IP', 'Defaults to the configured server IP when empty.')} + ${this.renderGatewayInput('dcrouterTargetPort', 'Target Port', String(settings?.dcrouterTargetPort || 80), '80', 'Internal HTTP port dcrouter forwards to.', 'number')} +
+ +
+ `; + } + + private renderGatewayInput( + key: keyof NonNullable, + label: string, + value: string, + placeholder: string, + hint: string, + type: 'text' | 'password' | 'number' = 'text', + ): TemplateResult { + return html` + + `; + } + + private updateGatewayDraft( + key: keyof NonNullable, + value: string, + ): void { + const currentSettings = this.settingsState.settings || {} as NonNullable; + const nextValue = key === 'dcrouterTargetPort' ? Number(value) || 0 : value; + this.settingsState = { + ...this.settingsState, + settings: { + ...currentSettings, + [key]: nextValue, + }, + }; + } + + private async saveExternalGatewaySettings(): Promise { + const settings = this.settingsState.settings; + if (!settings) return; + + await appstate.settingsStatePart.dispatchAction(appstate.updateSettingsAction, { + settings: { + dcrouterGatewayUrl: settings.dcrouterGatewayUrl || '', + dcrouterGatewayApiToken: settings.dcrouterGatewayApiToken || '', + dcrouterWorkHosterId: settings.dcrouterWorkHosterId || '', + dcrouterTargetHost: settings.dcrouterTargetHost || '', + dcrouterTargetPort: Number(settings.dcrouterTargetPort) || 80, + }, + }); + } }