2025-09-07 17:21:30 +00:00
|
|
|
import * as plugins from '../plugins.js';
|
|
|
|
|
import type { Cloudly } from '../classes.cloudly.js';
|
|
|
|
|
import * as servezoneInterfaces from '@serve.zone/interfaces';
|
|
|
|
|
|
|
|
|
|
export class CloudlySettingsManager {
|
|
|
|
|
public cloudlyRef: Cloudly;
|
|
|
|
|
public readyDeferred = plugins.smartpromise.defer();
|
2026-05-08 13:56:20 +00:00
|
|
|
public settingsStore!: plugins.smartdata.EasyStore<servezoneInterfaces.data.ICloudlySettings>;
|
2025-09-07 17:21:30 +00:00
|
|
|
|
|
|
|
|
constructor(cloudlyRefArg: Cloudly) {
|
|
|
|
|
this.cloudlyRef = cloudlyRefArg;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Initialize the settings manager and create the EasyStore
|
|
|
|
|
*/
|
|
|
|
|
public async init() {
|
|
|
|
|
this.settingsStore = await this.cloudlyRef.mongodbConnector.smartdataDb
|
|
|
|
|
.createEasyStore('cloudly-settings') as plugins.smartdata.EasyStore<servezoneInterfaces.data.ICloudlySettings>;
|
|
|
|
|
|
|
|
|
|
// Setup API route handlers
|
|
|
|
|
await this.setupRoutes();
|
|
|
|
|
|
|
|
|
|
this.readyDeferred.resolve();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get all settings
|
|
|
|
|
*/
|
|
|
|
|
public async getSettings(): Promise<servezoneInterfaces.data.ICloudlySettings> {
|
|
|
|
|
await this.readyDeferred.promise;
|
|
|
|
|
return await this.settingsStore.readAll();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get all settings with masked sensitive values (for API responses)
|
|
|
|
|
*/
|
|
|
|
|
public async getSettingsMasked(): Promise<servezoneInterfaces.data.ICloudlySettingsMasked> {
|
|
|
|
|
await this.readyDeferred.promise;
|
|
|
|
|
const settings = await this.getSettings();
|
|
|
|
|
const masked: servezoneInterfaces.data.ICloudlySettingsMasked = {};
|
|
|
|
|
|
|
|
|
|
for (const [key, value] of Object.entries(settings)) {
|
2026-04-29 15:57:02 +00:00
|
|
|
if (this.isSensitiveSettingKey(key) && typeof value === 'string' && value.length > 4) {
|
2025-09-07 17:21:30 +00:00
|
|
|
// Mask the token, showing only last 4 characters
|
|
|
|
|
masked[key] = '****' + value.slice(-4);
|
|
|
|
|
} else {
|
|
|
|
|
masked[key] = value;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return masked;
|
|
|
|
|
}
|
2026-04-29 15:57:02 +00:00
|
|
|
|
|
|
|
|
private isSensitiveSettingKey(key: string): boolean {
|
2026-05-07 19:49:56 +00:00
|
|
|
if (key === 'corebuildWorkersJson') {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2026-04-29 15:57:02 +00:00
|
|
|
const normalizedKey = key.toLowerCase();
|
|
|
|
|
return [
|
|
|
|
|
'token',
|
|
|
|
|
'secret',
|
|
|
|
|
'apikey',
|
|
|
|
|
'accesskey',
|
|
|
|
|
'applicationkey',
|
|
|
|
|
'consumerkey',
|
|
|
|
|
'keyjson',
|
|
|
|
|
'privatekey',
|
|
|
|
|
'password',
|
|
|
|
|
].some((sensitivePart) => normalizedKey.includes(sensitivePart));
|
|
|
|
|
}
|
2025-09-07 17:21:30 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Update multiple settings at once
|
|
|
|
|
*/
|
|
|
|
|
public async updateSettings(updates: Partial<servezoneInterfaces.data.ICloudlySettings>): Promise<void> {
|
|
|
|
|
await this.readyDeferred.promise;
|
|
|
|
|
for (const [key, value] of Object.entries(updates)) {
|
|
|
|
|
if (value !== undefined && value !== '') {
|
|
|
|
|
await this.settingsStore.writeKey(key as keyof servezoneInterfaces.data.ICloudlySettings, value);
|
|
|
|
|
} else if (value === '') {
|
|
|
|
|
// Empty string means clear the setting
|
|
|
|
|
await this.settingsStore.deleteKey(key as keyof servezoneInterfaces.data.ICloudlySettings);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-29 15:57:02 +00:00
|
|
|
|
|
|
|
|
if (Object.keys(updates).some((key) => this.isExternalGatewaySettingKey(key))) {
|
|
|
|
|
this.refreshExternalGatewayConfig().catch((error) => {
|
|
|
|
|
console.log(`External gateway settings refresh failed: ${(error as Error).message}`);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private isExternalGatewaySettingKey(key: string): boolean {
|
|
|
|
|
return [
|
|
|
|
|
'dcrouterGatewayUrl',
|
|
|
|
|
'dcrouterGatewayApiToken',
|
|
|
|
|
'dcrouterWorkHosterId',
|
|
|
|
|
'dcrouterTargetHost',
|
|
|
|
|
'dcrouterTargetPort',
|
|
|
|
|
].includes(key);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async refreshExternalGatewayConfig(): Promise<void> {
|
|
|
|
|
await Promise.all([
|
|
|
|
|
this.cloudlyRef.domainManager.syncExternalGatewayDomains(),
|
|
|
|
|
this.cloudlyRef.coreflowManager.pushClusterConfigToConnectedCoreflows(),
|
|
|
|
|
]);
|
2025-09-07 17:21:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get a specific setting value
|
|
|
|
|
*/
|
|
|
|
|
public async getSetting<K extends keyof servezoneInterfaces.data.ICloudlySettings>(key: K): Promise<servezoneInterfaces.data.ICloudlySettings[K]> {
|
|
|
|
|
await this.readyDeferred.promise;
|
|
|
|
|
return await this.settingsStore.readKey(key);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Set a specific setting value
|
|
|
|
|
*/
|
|
|
|
|
public async setSetting<K extends keyof servezoneInterfaces.data.ICloudlySettings>(key: K, value: servezoneInterfaces.data.ICloudlySettings[K]): Promise<void> {
|
|
|
|
|
await this.readyDeferred.promise;
|
|
|
|
|
if (value !== undefined && value !== '') {
|
|
|
|
|
await this.settingsStore.writeKey(key, value);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Clear a specific setting
|
|
|
|
|
*/
|
|
|
|
|
public async clearSetting(key: keyof servezoneInterfaces.data.ICloudlySettings): Promise<void> {
|
|
|
|
|
await this.readyDeferred.promise;
|
|
|
|
|
await this.settingsStore.deleteKey(key);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Clear all settings
|
|
|
|
|
*/
|
|
|
|
|
public async clearAllSettings(): Promise<void> {
|
|
|
|
|
await this.readyDeferred.promise;
|
|
|
|
|
await this.settingsStore.wipe();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Test connection for a specific provider
|
|
|
|
|
*/
|
|
|
|
|
public async testProviderConnection(provider: string): Promise<{success: boolean; message: string}> {
|
|
|
|
|
await this.readyDeferred.promise;
|
|
|
|
|
try {
|
|
|
|
|
switch (provider) {
|
|
|
|
|
case 'hetzner':
|
|
|
|
|
const hetznerToken = await this.getSetting('hetznerToken');
|
|
|
|
|
if (!hetznerToken) {
|
|
|
|
|
return { success: false, message: 'No Hetzner token configured' };
|
|
|
|
|
}
|
|
|
|
|
// TODO: Implement actual Hetzner API test
|
|
|
|
|
return { success: true, message: 'Hetzner connection test successful' };
|
|
|
|
|
|
|
|
|
|
case 'cloudflare':
|
|
|
|
|
const cloudflareToken = await this.getSetting('cloudflareToken');
|
|
|
|
|
if (!cloudflareToken) {
|
|
|
|
|
return { success: false, message: 'No Cloudflare token configured' };
|
|
|
|
|
}
|
|
|
|
|
// TODO: Implement actual Cloudflare API test
|
|
|
|
|
return { success: true, message: 'Cloudflare connection test successful' };
|
|
|
|
|
|
|
|
|
|
case 'aws':
|
|
|
|
|
const awsKey = await this.getSetting('awsAccessKey');
|
|
|
|
|
const awsSecret = await this.getSetting('awsSecretKey');
|
|
|
|
|
if (!awsKey || !awsSecret) {
|
|
|
|
|
return { success: false, message: 'AWS credentials not configured' };
|
|
|
|
|
}
|
|
|
|
|
// TODO: Implement actual AWS API test
|
|
|
|
|
return { success: true, message: 'AWS connection test successful' };
|
|
|
|
|
|
|
|
|
|
case 'digitalocean':
|
|
|
|
|
const doToken = await this.getSetting('digitalOceanToken');
|
|
|
|
|
if (!doToken) {
|
|
|
|
|
return { success: false, message: 'No DigitalOcean token configured' };
|
|
|
|
|
}
|
|
|
|
|
// TODO: Implement actual DigitalOcean API test
|
|
|
|
|
return { success: true, message: 'DigitalOcean connection test successful' };
|
|
|
|
|
|
|
|
|
|
case 'azure':
|
|
|
|
|
const azureClientId = await this.getSetting('azureClientId');
|
|
|
|
|
const azureClientSecret = await this.getSetting('azureClientSecret');
|
|
|
|
|
const azureTenantId = await this.getSetting('azureTenantId');
|
|
|
|
|
if (!azureClientId || !azureClientSecret || !azureTenantId) {
|
|
|
|
|
return { success: false, message: 'Azure credentials not configured' };
|
|
|
|
|
}
|
|
|
|
|
// TODO: Implement actual Azure API test
|
|
|
|
|
return { success: true, message: 'Azure connection test successful' };
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
return { success: false, message: `Unknown provider: ${provider}` };
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
2026-05-08 13:56:20 +00:00
|
|
|
return { success: false, message: `Connection test failed: ${error instanceof Error ? error.message : String(error)}` };
|
2025-09-07 17:21:30 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Setup API route handlers for settings management
|
|
|
|
|
*/
|
|
|
|
|
private async setupRoutes() {
|
|
|
|
|
// Get Settings Handler
|
|
|
|
|
this.cloudlyRef.typedrouter.addTypedHandler<servezoneInterfaces.requests.settings.IRequest_GetSettings>(
|
|
|
|
|
new plugins.typedrequest.TypedHandler<servezoneInterfaces.requests.settings.IRequest_GetSettings>(
|
|
|
|
|
'getSettings',
|
|
|
|
|
async (requestData) => {
|
|
|
|
|
// TODO: Add authentication check for admin users
|
|
|
|
|
const maskedSettings = await this.getSettingsMasked();
|
|
|
|
|
return {
|
|
|
|
|
settings: maskedSettings
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Update Settings Handler
|
|
|
|
|
this.cloudlyRef.typedrouter.addTypedHandler<servezoneInterfaces.requests.settings.IRequest_UpdateSettings>(
|
|
|
|
|
new plugins.typedrequest.TypedHandler<servezoneInterfaces.requests.settings.IRequest_UpdateSettings>(
|
|
|
|
|
'updateSettings',
|
|
|
|
|
async (requestData) => {
|
|
|
|
|
// TODO: Add authentication check for admin users
|
|
|
|
|
try {
|
|
|
|
|
await this.updateSettings(requestData.updates);
|
|
|
|
|
return {
|
|
|
|
|
success: true,
|
|
|
|
|
message: 'Settings updated successfully'
|
|
|
|
|
};
|
|
|
|
|
} catch (error) {
|
2026-05-08 13:56:20 +00:00
|
|
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
2025-09-07 17:21:30 +00:00
|
|
|
return {
|
|
|
|
|
success: false,
|
2026-05-08 13:56:20 +00:00
|
|
|
message: `Failed to update settings: ${errorMessage}`
|
2025-09-07 17:21:30 +00:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Clear Setting Handler
|
|
|
|
|
this.cloudlyRef.typedrouter.addTypedHandler<servezoneInterfaces.requests.settings.IRequest_ClearSetting>(
|
|
|
|
|
new plugins.typedrequest.TypedHandler<servezoneInterfaces.requests.settings.IRequest_ClearSetting>(
|
|
|
|
|
'clearSetting',
|
|
|
|
|
async (requestData) => {
|
|
|
|
|
// TODO: Add authentication check for admin users
|
|
|
|
|
try {
|
|
|
|
|
await this.clearSetting(requestData.key);
|
|
|
|
|
return {
|
|
|
|
|
success: true,
|
|
|
|
|
message: `Setting ${requestData.key} cleared successfully`
|
|
|
|
|
};
|
|
|
|
|
} catch (error) {
|
2026-05-08 13:56:20 +00:00
|
|
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
2025-09-07 17:21:30 +00:00
|
|
|
return {
|
|
|
|
|
success: false,
|
2026-05-08 13:56:20 +00:00
|
|
|
message: `Failed to clear setting: ${errorMessage}`
|
2025-09-07 17:21:30 +00:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Test Provider Connection Handler
|
|
|
|
|
this.cloudlyRef.typedrouter.addTypedHandler<servezoneInterfaces.requests.settings.IRequest_TestProviderConnection>(
|
|
|
|
|
new plugins.typedrequest.TypedHandler<servezoneInterfaces.requests.settings.IRequest_TestProviderConnection>(
|
|
|
|
|
'testProviderConnection',
|
|
|
|
|
async (requestData) => {
|
|
|
|
|
// TODO: Add authentication check for admin users
|
|
|
|
|
const testResult = await this.testProviderConnection(requestData.provider);
|
|
|
|
|
return {
|
|
|
|
|
success: testResult.success,
|
|
|
|
|
message: testResult.message,
|
|
|
|
|
connectionValid: testResult.success
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Get Single Setting Handler (for internal use)
|
|
|
|
|
this.cloudlyRef.typedrouter.addTypedHandler<servezoneInterfaces.requests.settings.IRequest_GetSetting>(
|
|
|
|
|
new plugins.typedrequest.TypedHandler<servezoneInterfaces.requests.settings.IRequest_GetSetting>(
|
|
|
|
|
'getSetting',
|
|
|
|
|
async (requestData) => {
|
|
|
|
|
// TODO: Add authentication check for admin users
|
|
|
|
|
const value = await this.getSetting(requestData.key);
|
|
|
|
|
return {
|
|
|
|
|
value
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-04-29 15:57:02 +00:00
|
|
|
}
|