feat: expose dcrouter gateway settings
This commit is contained in:
@@ -2,6 +2,8 @@ import * as plugins from '../../plugins.ts';
|
|||||||
import type { OpsServer } from '../classes.opsserver.ts';
|
import type { OpsServer } from '../classes.opsserver.ts';
|
||||||
import * as interfaces from '../../../ts_interfaces/index.ts';
|
import * as interfaces from '../../../ts_interfaces/index.ts';
|
||||||
import { requireAdminIdentity } from '../helpers/guards.ts';
|
import { requireAdminIdentity } from '../helpers/guards.ts';
|
||||||
|
import { logger } from '../../logging.ts';
|
||||||
|
import { getErrorMessage } from '../../utils/error.ts';
|
||||||
|
|
||||||
export class SettingsHandler {
|
export class SettingsHandler {
|
||||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
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();
|
const settings = await this.getSettingsObject();
|
||||||
return { settings };
|
return { settings };
|
||||||
},
|
},
|
||||||
@@ -93,4 +101,28 @@ export class SettingsHandler {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private hasExternalGatewaySetting(settings: Partial<interfaces.data.ISettings>): boolean {
|
||||||
|
return [
|
||||||
|
'dcrouterGatewayUrl',
|
||||||
|
'dcrouterGatewayApiToken',
|
||||||
|
'dcrouterWorkHosterId',
|
||||||
|
'dcrouterTargetHost',
|
||||||
|
'dcrouterTargetPort',
|
||||||
|
].some((key) => Object.prototype.hasOwnProperty.call(settings, key));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async refreshExternalGateway(): Promise<void> {
|
||||||
|
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)}`);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,100 @@ export class ObViewSettings extends DeesElement {
|
|||||||
public static styles = [
|
public static styles = [
|
||||||
cssManager.defaultStyles,
|
cssManager.defaultStyles,
|
||||||
shared.viewHostCss,
|
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() {
|
async connectedCallback() {
|
||||||
@@ -57,6 +150,7 @@ export class ObViewSettings extends DeesElement {
|
|||||||
public render(): TemplateResult {
|
public render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<ob-sectionheading>Settings</ob-sectionheading>
|
<ob-sectionheading>Settings</ob-sectionheading>
|
||||||
|
${this.renderExternalGatewaySettings()}
|
||||||
<sz-settings-view
|
<sz-settings-view
|
||||||
.settings=${this.settingsState.settings || {
|
.settings=${this.settingsState.settings || {
|
||||||
darkMode: true,
|
darkMode: true,
|
||||||
@@ -90,4 +184,78 @@ export class ObViewSettings extends DeesElement {
|
|||||||
></sz-settings-view>
|
></sz-settings-view>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private renderExternalGatewaySettings(): TemplateResult {
|
||||||
|
const settings = this.settingsState.settings;
|
||||||
|
return html`
|
||||||
|
<section class="gateway-card">
|
||||||
|
<div class="gateway-header">
|
||||||
|
<div class="gateway-title">External dcrouter Gateway</div>
|
||||||
|
<div class="gateway-subtitle">Delegate public WorkApp routing, DNS, and certificates to a dcrouter edge authority.</div>
|
||||||
|
</div>
|
||||||
|
<div class="gateway-content">
|
||||||
|
${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')}
|
||||||
|
</div>
|
||||||
|
<div class="gateway-footer">
|
||||||
|
<button class="save-button" @click=${() => this.saveExternalGatewaySettings()}>Save Gateway Settings</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderGatewayInput(
|
||||||
|
key: keyof NonNullable<appstate.ISettingsState['settings']>,
|
||||||
|
label: string,
|
||||||
|
value: string,
|
||||||
|
placeholder: string,
|
||||||
|
hint: string,
|
||||||
|
type: 'text' | 'password' | 'number' = 'text',
|
||||||
|
): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<label class="gateway-field ${key === 'dcrouterGatewayUrl' ? 'full' : ''}">
|
||||||
|
<span class="field-label">${label}</span>
|
||||||
|
<input
|
||||||
|
type=${type}
|
||||||
|
.value=${value}
|
||||||
|
placeholder=${placeholder}
|
||||||
|
@input=${(event: Event) => this.updateGatewayDraft(key, (event.target as HTMLInputElement).value)}
|
||||||
|
/>
|
||||||
|
<span class="field-hint">${hint}</span>
|
||||||
|
</label>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateGatewayDraft(
|
||||||
|
key: keyof NonNullable<appstate.ISettingsState['settings']>,
|
||||||
|
value: string,
|
||||||
|
): void {
|
||||||
|
const currentSettings = this.settingsState.settings || {} as NonNullable<appstate.ISettingsState['settings']>;
|
||||||
|
const nextValue = key === 'dcrouterTargetPort' ? Number(value) || 0 : value;
|
||||||
|
this.settingsState = {
|
||||||
|
...this.settingsState,
|
||||||
|
settings: {
|
||||||
|
...currentSettings,
|
||||||
|
[key]: nextValue,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async saveExternalGatewaySettings(): Promise<void> {
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user