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'; import { isValidHostname, normalizeHostname } from '../../utils/domain.ts'; export class SettingsHandler { public typedrouter = new plugins.typedrequest.TypedRouter(); constructor(private opsServerRef: OpsServer) { this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter); this.registerHandlers(); } private async getSettingsObject(): Promise { const db = this.opsServerRef.oneboxRef.database; const cloudflareToken = await db.getSecretSetting('cloudflareToken'); const dcrouterGatewayApiToken = await db.getSecretSetting('dcrouterGatewayApiToken'); const settingsMap = db.getAllSettings(); const managedDcRouter = this.opsServerRef.oneboxRef.managedDcRouter; return { cloudflareToken: cloudflareToken || '', cloudflareZoneId: settingsMap['cloudflareZoneId'] || '', adminUiDomain: settingsMap['adminUiDomain'] || '', dcrouterMode: managedDcRouter.getMode(), dcrouterManagedImage: managedDcRouter.getImage(), dcrouterManagedOpsPort: managedDcRouter.getOpsPort(), dcrouterManagedHttpPort: managedDcRouter.getHttpPort(), dcrouterManagedHttpsPort: managedDcRouter.getHttpsPort(), dcrouterManagedDataDir: managedDcRouter.getDataDir(), dcrouterGatewayUrl: settingsMap['dcrouterGatewayUrl'] || '', dcrouterGatewayApiToken: dcrouterGatewayApiToken || '', dcrouterGatewayClientId: settingsMap['dcrouterGatewayClientId'] || settingsMap['dcrouterWorkHosterId'] || '', dcrouterWorkHosterId: settingsMap['dcrouterWorkHosterId'] || settingsMap['dcrouterGatewayClientId'] || '', dcrouterTargetHost: settingsMap['dcrouterTargetHost'] || '', dcrouterTargetPort: parseInt(settingsMap['dcrouterTargetPort'] || '0', 10), autoRenewCerts: settingsMap['autoRenewCerts'] === 'true', renewalThreshold: parseInt(settingsMap['renewalThreshold'] || '30', 10), acmeEmail: settingsMap['acmeEmail'] || '', httpPort: parseInt(settingsMap['httpPort'] || '80', 10), httpsPort: parseInt(settingsMap['httpsPort'] || '443', 10), forceHttps: settingsMap['forceHttps'] === 'true', }; } private registerHandlers(): void { this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'getSettings', async (dataArg) => { await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg); const settings = await this.getSettingsObject(); return { settings }; }, ), ); this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'updateSettings', async (dataArg) => { await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg); const db = this.opsServerRef.oneboxRef.database; const updates = dataArg.settings; const normalizedUpdates = this.normalizeUpdates(updates); // Store each setting as key-value pair for (const [key, value] of Object.entries(normalizedUpdates)) { if (value !== undefined) { if (db.isSecretSettingKey(key)) { await db.setSecretSetting(key, String(value)); } else { db.setSetting(key, String(value)); } } } if (this.hasRouteSyncSetting(normalizedUpdates)) { this.refreshGatewayRoutes(normalizedUpdates).catch((error) => { logger.warn(`dcrouter gateway settings refresh failed: ${getErrorMessage(error)}`); }); } const settings = await this.getSettingsObject(); return { settings }; }, ), ); this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'setBackupPassword', async (dataArg) => { await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg); await this.opsServerRef.oneboxRef.database.setSecretSetting('backupPassword', dataArg.password); return { ok: true }; }, ), ); this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'getBackupPasswordStatus', async (dataArg) => { await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg); const isConfigured = await this.opsServerRef.oneboxRef.database.hasSecretSetting('backupPassword'); return { status: { isConfigured } }; }, ), ); } private normalizeUpdates( settings: Partial, ): Partial { const normalizedUpdates = { ...settings }; if (Object.prototype.hasOwnProperty.call(normalizedUpdates, 'adminUiDomain')) { const normalizedDomain = normalizeHostname(String(normalizedUpdates.adminUiDomain || '')); if (!isValidHostname(normalizedDomain)) { throw new plugins.typedrequest.TypedResponseError('Invalid Admin UI domain'); } normalizedUpdates.adminUiDomain = normalizedDomain; } return normalizedUpdates; } private hasRouteSyncSetting(settings: Partial): boolean { return [ 'adminUiDomain', 'dcrouterMode', 'dcrouterManagedImage', 'dcrouterManagedOpsPort', 'dcrouterManagedHttpPort', 'dcrouterManagedHttpsPort', 'dcrouterManagedDataDir', 'dcrouterGatewayUrl', 'dcrouterGatewayApiToken', 'dcrouterGatewayClientId', 'dcrouterWorkHosterId', 'dcrouterTargetHost', 'dcrouterTargetPort', ].some((key) => Object.prototype.hasOwnProperty.call(settings, key)); } private hasManagedDcRouterRuntimeSetting(settings: Partial): boolean { return [ 'dcrouterMode', 'dcrouterManagedImage', 'dcrouterManagedOpsPort', 'dcrouterManagedHttpPort', 'dcrouterManagedHttpsPort', 'dcrouterManagedDataDir', ].some((key) => Object.prototype.hasOwnProperty.call(settings, key)); } private async refreshGatewayRoutes(settings: Partial): Promise { const onebox = this.opsServerRef.oneboxRef; if (this.hasManagedDcRouterRuntimeSetting(settings)) { if (onebox.managedDcRouter.getMode() === 'managed') { await onebox.managedDcRouter.restart(); } else { await onebox.managedDcRouter.stop(); } } await onebox.reverseProxy.reloadRoutes(); await onebox.externalGateway.syncDomains(); await onebox.externalGateway.syncServiceRoutes(); } }