feat: add secret settings manager and migration for legacy settings
- Implemented SecretSettingsManager to handle secret settings with encryption. - Added functionality to migrate legacy plaintext settings into encrypted storage. - Introduced methods for setting, getting, and clearing secret settings. - Created tests for verifying the migration and canonicalization of secret settings. - Updated app state to handle service updates via socket communication. - Added interface for push service updates to manage service state changes.
This commit is contained in:
@@ -65,7 +65,7 @@ export class BackupManager {
|
||||
*/
|
||||
async init(): Promise<void> {
|
||||
const repoPath = this.getArchiveRepoPath();
|
||||
const passphrase = this.getBackupPassword() || undefined;
|
||||
const passphrase = await this.getBackupPassword() || undefined;
|
||||
|
||||
try {
|
||||
// Try to open existing repo
|
||||
@@ -503,7 +503,7 @@ export class BackupManager {
|
||||
await Deno.remove(tempDir, { recursive: true });
|
||||
|
||||
// Encrypt for transport
|
||||
const password = this.getBackupPassword();
|
||||
const password = await this.getBackupPassword();
|
||||
if (password) {
|
||||
const encPath = `${tarPath}.enc`;
|
||||
await this.encryptFile(tarPath, encPath, password);
|
||||
@@ -526,9 +526,8 @@ export class BackupManager {
|
||||
/**
|
||||
* Get backup password from settings
|
||||
*/
|
||||
private getBackupPassword(): string | null {
|
||||
return this.oneboxRef.database.getSetting('backup_encryption_password')
|
||||
|| this.oneboxRef.database.getSetting('backupPassword');
|
||||
private async getBackupPassword(): Promise<string | null> {
|
||||
return await this.oneboxRef.database.getSecretSetting('backupPassword');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -551,7 +550,7 @@ export class BackupManager {
|
||||
* Restore from a legacy .tar.enc file
|
||||
*/
|
||||
private async restoreLegacyBackup(backupPath: string, options: IRestoreOptions): Promise<IRestoreResult> {
|
||||
const backupPassword = this.getBackupPassword();
|
||||
const backupPassword = await this.getBackupPassword();
|
||||
if (!backupPassword) {
|
||||
throw new Error('Backup password not configured.');
|
||||
}
|
||||
|
||||
@@ -24,8 +24,7 @@ export class CloudflareDomainSync {
|
||||
*/
|
||||
async init(): Promise<void> {
|
||||
try {
|
||||
const apiKey = this.database.getSetting('cloudflareAPIKey')
|
||||
|| this.database.getSetting('cloudflareToken');
|
||||
const apiKey = await this.database.getSecretSetting('cloudflareToken');
|
||||
|
||||
if (!apiKey) {
|
||||
logger.warn('Cloudflare API key not configured. Domain sync will be limited.');
|
||||
|
||||
+2
-3
@@ -27,13 +27,12 @@ export class OneboxDnsManager {
|
||||
async init(): Promise<void> {
|
||||
try {
|
||||
// Get Cloudflare credentials from settings
|
||||
const apiKey = this.database.getSetting('cloudflareAPIKey')
|
||||
|| this.database.getSetting('cloudflareToken');
|
||||
const apiKey = await this.database.getSecretSetting('cloudflareToken');
|
||||
const serverIP = this.database.getSetting('serverIP');
|
||||
|
||||
if (!apiKey) {
|
||||
logger.warn('Cloudflare credentials not configured. DNS management will be disabled.');
|
||||
logger.info('Configure with: onebox config set cloudflareAPIKey <key>');
|
||||
logger.info('Configure with: onebox config set cloudflareToken <key>');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import { OneboxDnsManager } from './dns.ts';
|
||||
import { OneboxSslManager } from './ssl.ts';
|
||||
import { OneboxDaemon } from './daemon.ts';
|
||||
import { OneboxSystemd } from './systemd.ts';
|
||||
import { OneboxHttpServer } from './httpserver.ts';
|
||||
import type { OneboxHttpServer } from './httpserver.ts';
|
||||
import { CloudflareDomainSync } from './cloudflare-sync.ts';
|
||||
import { CertRequirementManager } from './cert-requirement-manager.ts';
|
||||
import { RegistryManager } from './registry.ts';
|
||||
@@ -37,7 +37,7 @@ export class Onebox {
|
||||
public ssl: OneboxSslManager;
|
||||
public daemon: OneboxDaemon;
|
||||
public systemd: OneboxSystemd;
|
||||
public httpServer: OneboxHttpServer;
|
||||
public httpServer: OneboxHttpServer | null;
|
||||
public cloudflareDomainSync: CloudflareDomainSync;
|
||||
public certRequirementManager: CertRequirementManager;
|
||||
public registry: RegistryManager;
|
||||
@@ -63,7 +63,7 @@ export class Onebox {
|
||||
this.ssl = new OneboxSslManager(this);
|
||||
this.daemon = new OneboxDaemon(this);
|
||||
this.systemd = new OneboxSystemd();
|
||||
this.httpServer = new OneboxHttpServer(this);
|
||||
this.httpServer = null;
|
||||
this.registry = new RegistryManager({
|
||||
dataDir: './.nogit/registry-data',
|
||||
port: 4000,
|
||||
|
||||
+23
-13
@@ -23,6 +23,17 @@ export class OneboxServicesManager {
|
||||
this.docker = oneboxRef.docker;
|
||||
}
|
||||
|
||||
private async broadcastServiceUpdate(
|
||||
serviceName: string,
|
||||
action: 'created' | 'updated' | 'deleted' | 'started' | 'stopped',
|
||||
): Promise<void> {
|
||||
await this.oneboxRef.opsServer.broadcastServiceUpdate(
|
||||
serviceName,
|
||||
action,
|
||||
this.database.getServiceByName(serviceName),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deploy a new service (full workflow)
|
||||
*/
|
||||
@@ -197,7 +208,9 @@ export class OneboxServicesManager {
|
||||
|
||||
logger.success(`Service deployed successfully: ${options.name}`);
|
||||
|
||||
return this.database.getServiceByName(options.name)!;
|
||||
const deployedService = this.database.getServiceByName(options.name)!;
|
||||
await this.broadcastServiceUpdate(options.name, 'created');
|
||||
return deployedService;
|
||||
} catch (error) {
|
||||
logger.error(`Failed to deploy service ${options.name}: ${getErrorMessage(error)}`);
|
||||
throw error;
|
||||
@@ -236,12 +249,14 @@ export class OneboxServicesManager {
|
||||
}
|
||||
|
||||
logger.success(`Service started: ${name}`);
|
||||
await this.broadcastServiceUpdate(name, 'started');
|
||||
} catch (error) {
|
||||
logger.error(`Failed to start service ${name}: ${getErrorMessage(error)}`);
|
||||
this.database.updateService(
|
||||
this.database.getServiceByName(name)?.id!,
|
||||
{ status: 'failed' }
|
||||
);
|
||||
await this.broadcastServiceUpdate(name, 'updated');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -274,6 +289,7 @@ export class OneboxServicesManager {
|
||||
}
|
||||
|
||||
logger.success(`Service stopped: ${name}`);
|
||||
await this.broadcastServiceUpdate(name, 'stopped');
|
||||
} catch (error) {
|
||||
logger.error(`Failed to stop service ${name}: ${getErrorMessage(error)}`);
|
||||
throw error;
|
||||
@@ -301,6 +317,7 @@ export class OneboxServicesManager {
|
||||
this.database.updateService(service.id!, { status: 'running' });
|
||||
|
||||
logger.success(`Service restarted: ${name}`);
|
||||
await this.broadcastServiceUpdate(name, 'updated');
|
||||
} catch (error) {
|
||||
logger.error(`Failed to restart service ${name}: ${getErrorMessage(error)}`);
|
||||
throw error;
|
||||
@@ -357,6 +374,7 @@ export class OneboxServicesManager {
|
||||
this.database.deleteService(service.id!);
|
||||
|
||||
logger.success(`Service removed: ${name}`);
|
||||
await this.oneboxRef.opsServer.broadcastServiceUpdate(name, 'deleted');
|
||||
} catch (error) {
|
||||
logger.error(`Failed to remove service ${name}: ${getErrorMessage(error)}`);
|
||||
throw error;
|
||||
@@ -625,7 +643,9 @@ export class OneboxServicesManager {
|
||||
logger.success(`Service ${name} updated (not started)`);
|
||||
}
|
||||
|
||||
return this.database.getServiceByName(name)!;
|
||||
const refreshedService = this.database.getServiceByName(name)!;
|
||||
await this.broadcastServiceUpdate(name, 'updated');
|
||||
return refreshedService;
|
||||
} catch (error) {
|
||||
logger.error(`Failed to update service ${name}: ${getErrorMessage(error)}`);
|
||||
throw error;
|
||||
@@ -659,11 +679,7 @@ export class OneboxServicesManager {
|
||||
// Only update and broadcast if status changed
|
||||
if (service.status !== ourStatus) {
|
||||
this.database.updateService(service.id!, { status: ourStatus });
|
||||
|
||||
// Broadcast status change via WebSocket
|
||||
if (this.oneboxRef.httpServer) {
|
||||
this.oneboxRef.httpServer.broadcastServiceStatus(name, ourStatus);
|
||||
}
|
||||
await this.broadcastServiceUpdate(name, 'updated');
|
||||
}
|
||||
} catch (error) {
|
||||
logger.debug(`Failed to sync status for service ${name}: ${getErrorMessage(error)}`);
|
||||
@@ -756,12 +772,6 @@ export class OneboxServicesManager {
|
||||
// Restart service
|
||||
logger.info(`Auto-restarting service: ${service.name}`);
|
||||
await this.restartService(service.name);
|
||||
|
||||
// Broadcast update via WebSocket
|
||||
this.oneboxRef.httpServer.broadcastServiceUpdate({
|
||||
action: 'updated',
|
||||
service: this.database.getServiceByName(service.name)!,
|
||||
});
|
||||
} else if (!service.imageDigest) {
|
||||
// First time - just store the digest
|
||||
this.database.updateService(service.id!, {
|
||||
|
||||
+2
-3
@@ -39,12 +39,11 @@ export class OneboxSslManager {
|
||||
this.acmeEmail = acmeEmail;
|
||||
|
||||
// Get Cloudflare API key (reuse from DNS manager)
|
||||
const cfApiKey = this.database.getSetting('cloudflareAPIKey')
|
||||
|| this.database.getSetting('cloudflareToken');
|
||||
const cfApiKey = await this.database.getSecretSetting('cloudflareToken');
|
||||
|
||||
if (!cfApiKey) {
|
||||
logger.warn('Cloudflare API key not configured. SSL certificate management will be limited.');
|
||||
logger.info('Configure with: onebox config set cloudflareAPIKey <key>');
|
||||
logger.info('Configure with: onebox config set cloudflareToken <key>');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -382,7 +382,17 @@ async function handleSystemdCommand(subcommand: string, _args: string[]) {
|
||||
async function handleConfigCommand(onebox: Onebox, subcommand: string, args: string[]) {
|
||||
switch (subcommand) {
|
||||
case 'show': {
|
||||
for (const secretKey of onebox.database.getCanonicalSecretSettingKeys()) {
|
||||
await onebox.database.getSecretSetting(secretKey);
|
||||
}
|
||||
|
||||
const settings = onebox.database.getAllSettings();
|
||||
for (const secretKey of onebox.database.getCanonicalSecretSettingKeys()) {
|
||||
if (await onebox.database.hasSecretSetting(secretKey)) {
|
||||
settings[secretKey] = '********';
|
||||
}
|
||||
}
|
||||
|
||||
logger.table(
|
||||
['Key', 'Value'],
|
||||
Object.entries(settings).map(([k, v]) => [k, v])
|
||||
@@ -391,7 +401,11 @@ async function handleConfigCommand(onebox: Onebox, subcommand: string, args: str
|
||||
}
|
||||
|
||||
case 'set':
|
||||
onebox.database.setSetting(args[0], args[1]);
|
||||
if (onebox.database.isSecretSettingKey(args[0])) {
|
||||
await onebox.database.setSecretSetting(args[0], args[1]);
|
||||
} else {
|
||||
onebox.database.setSetting(args[0], args[1]);
|
||||
}
|
||||
logger.success(`Setting ${args[0]} updated`);
|
||||
break;
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import type { TBindValue } from './types.ts';
|
||||
import { logger } from '../logging.ts';
|
||||
import { getErrorMessage } from '../utils/error.ts';
|
||||
import { MigrationRunner } from './migrations/index.ts';
|
||||
import { SecretSettingsManager } from './secret-settings.ts';
|
||||
|
||||
// Import repositories
|
||||
import {
|
||||
@@ -50,6 +51,7 @@ export class OneboxDatabase {
|
||||
private metricsRepo!: MetricsRepository;
|
||||
private platformRepo!: PlatformRepository;
|
||||
private backupRepo!: BackupRepository;
|
||||
public secretSettings!: SecretSettingsManager;
|
||||
|
||||
constructor(dbPath = './.nogit/onebox.db') {
|
||||
this.dbPath = dbPath;
|
||||
@@ -84,6 +86,7 @@ export class OneboxDatabase {
|
||||
this.metricsRepo = new MetricsRepository(queryFn);
|
||||
this.platformRepo = new PlatformRepository(queryFn);
|
||||
this.backupRepo = new BackupRepository(queryFn);
|
||||
this.secretSettings = new SecretSettingsManager(this.authRepo);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to initialize database: ${getErrorMessage(error)}`);
|
||||
throw error;
|
||||
@@ -229,6 +232,14 @@ export class OneboxDatabase {
|
||||
)
|
||||
`);
|
||||
|
||||
this.query(`
|
||||
CREATE TABLE IF NOT EXISTS secret_settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
)
|
||||
`);
|
||||
|
||||
// Version table for migrations
|
||||
this.query(`
|
||||
CREATE TABLE IF NOT EXISTS migrations (
|
||||
@@ -333,10 +344,34 @@ export class OneboxDatabase {
|
||||
this.authRepo.setSetting(key, value);
|
||||
}
|
||||
|
||||
deleteSetting(key: string): void {
|
||||
this.authRepo.deleteSetting(key);
|
||||
}
|
||||
|
||||
getAllSettings(): Record<string, string> {
|
||||
return this.authRepo.getAllSettings();
|
||||
}
|
||||
|
||||
async getSecretSetting(key: string): Promise<string | null> {
|
||||
return await this.secretSettings.get(key);
|
||||
}
|
||||
|
||||
async setSecretSetting(key: string, value: string | null): Promise<void> {
|
||||
await this.secretSettings.set(key, value);
|
||||
}
|
||||
|
||||
async hasSecretSetting(key: string): Promise<boolean> {
|
||||
return await this.secretSettings.has(key);
|
||||
}
|
||||
|
||||
isSecretSettingKey(key: string): boolean {
|
||||
return this.secretSettings.isSecretKey(key);
|
||||
}
|
||||
|
||||
getCanonicalSecretSettingKeys(): string[] {
|
||||
return this.secretSettings.getCanonicalKeys();
|
||||
}
|
||||
|
||||
// ============ Users CRUD (delegated to repository) ============
|
||||
|
||||
async createUser(user: Omit<IUser, 'id'>): Promise<IUser> {
|
||||
|
||||
@@ -70,6 +70,10 @@ export class AuthRepository extends BaseRepository {
|
||||
);
|
||||
}
|
||||
|
||||
deleteSetting(key: string): void {
|
||||
this.query('DELETE FROM settings WHERE key = ?', [key]);
|
||||
}
|
||||
|
||||
getAllSettings(): Record<string, string> {
|
||||
const rows = this.query('SELECT key, value FROM settings');
|
||||
const settings: Record<string, string> = {};
|
||||
@@ -80,4 +84,24 @@ export class AuthRepository extends BaseRepository {
|
||||
}
|
||||
return settings;
|
||||
}
|
||||
|
||||
getSecretSetting(key: string): string | null {
|
||||
const rows = this.query('SELECT value FROM secret_settings WHERE key = ?', [key]);
|
||||
if (rows.length === 0) return null;
|
||||
|
||||
const value = (rows[0] as any).value || rows[0][0];
|
||||
return value ? String(value) : null;
|
||||
}
|
||||
|
||||
setSecretSetting(key: string, value: string): void {
|
||||
const now = Date.now();
|
||||
this.query(
|
||||
'INSERT OR REPLACE INTO secret_settings (key, value, updated_at) VALUES (?, ?, ?)',
|
||||
[key, value, now],
|
||||
);
|
||||
}
|
||||
|
||||
deleteSecretSetting(key: string): void {
|
||||
this.query('DELETE FROM secret_settings WHERE key = ?', [key]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
import { credentialEncryption } from '../classes/encryption.ts';
|
||||
import type { AuthRepository } from './repositories/auth.repository.ts';
|
||||
|
||||
const encryptedSecretPrefix = 'enc:v1:';
|
||||
|
||||
const secretSettingAliases = {
|
||||
backupPassword: ['backup_encryption_password'],
|
||||
cloudflareToken: ['cloudflareAPIKey'],
|
||||
} as const;
|
||||
|
||||
type TCanonicalSecretSettingKey = keyof typeof secretSettingAliases;
|
||||
|
||||
export class SecretSettingsManager {
|
||||
constructor(private authRepo: AuthRepository) {}
|
||||
|
||||
public isSecretKey(key: string): boolean {
|
||||
return this.resolveCanonicalKey(key) !== null;
|
||||
}
|
||||
|
||||
public getCanonicalKeys(): TCanonicalSecretSettingKey[] {
|
||||
return Object.keys(secretSettingAliases) as TCanonicalSecretSettingKey[];
|
||||
}
|
||||
|
||||
public async get(key: string): Promise<string | null> {
|
||||
const canonicalKey = this.resolveCanonicalKey(key);
|
||||
if (!canonicalKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const candidateKey of this.getCandidateKeys(canonicalKey)) {
|
||||
const secretValue = this.authRepo.getSecretSetting(candidateKey);
|
||||
if (secretValue !== null) {
|
||||
const decryptedValue = await this.decodeStoredValue(secretValue);
|
||||
await this.normalizeStoredSecret(canonicalKey, candidateKey, secretValue, decryptedValue);
|
||||
return decryptedValue;
|
||||
}
|
||||
|
||||
const legacyValue = this.authRepo.getSetting(candidateKey);
|
||||
if (legacyValue !== null) {
|
||||
await this.set(canonicalKey, legacyValue);
|
||||
if (candidateKey !== canonicalKey) {
|
||||
this.authRepo.deleteSetting(candidateKey);
|
||||
}
|
||||
this.authRepo.deleteSetting(canonicalKey);
|
||||
return legacyValue;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public async set(key: string, value: string | null): Promise<void> {
|
||||
const canonicalKey = this.resolveCanonicalKey(key);
|
||||
if (!canonicalKey) {
|
||||
throw new Error(`Unsupported secret setting key: ${key}`);
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
this.clear(canonicalKey);
|
||||
return;
|
||||
}
|
||||
|
||||
const encryptedValue = await credentialEncryption.encrypt({ value });
|
||||
this.authRepo.setSecretSetting(canonicalKey, `${encryptedSecretPrefix}${encryptedValue}`);
|
||||
|
||||
for (const aliasKey of secretSettingAliases[canonicalKey]) {
|
||||
this.authRepo.deleteSecretSetting(aliasKey);
|
||||
this.authRepo.deleteSetting(aliasKey);
|
||||
}
|
||||
|
||||
this.authRepo.deleteSetting(canonicalKey);
|
||||
}
|
||||
|
||||
public async has(key: string): Promise<boolean> {
|
||||
return (await this.get(key)) !== null;
|
||||
}
|
||||
|
||||
public clear(key: string): void {
|
||||
const canonicalKey = this.resolveCanonicalKey(key);
|
||||
if (!canonicalKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.authRepo.deleteSecretSetting(canonicalKey);
|
||||
this.authRepo.deleteSetting(canonicalKey);
|
||||
|
||||
for (const aliasKey of secretSettingAliases[canonicalKey]) {
|
||||
this.authRepo.deleteSecretSetting(aliasKey);
|
||||
this.authRepo.deleteSetting(aliasKey);
|
||||
}
|
||||
}
|
||||
|
||||
private resolveCanonicalKey(key: string): TCanonicalSecretSettingKey | null {
|
||||
if (key in secretSettingAliases) {
|
||||
return key as TCanonicalSecretSettingKey;
|
||||
}
|
||||
|
||||
for (const [canonicalKey, aliases] of Object.entries(secretSettingAliases)) {
|
||||
if ((aliases as readonly string[]).includes(key)) {
|
||||
return canonicalKey as TCanonicalSecretSettingKey;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private getCandidateKeys(canonicalKey: TCanonicalSecretSettingKey): string[] {
|
||||
return [canonicalKey, ...secretSettingAliases[canonicalKey]];
|
||||
}
|
||||
|
||||
private async decodeStoredValue(value: string): Promise<string> {
|
||||
if (value.startsWith(encryptedSecretPrefix)) {
|
||||
const decrypted = await credentialEncryption.decrypt<{ value: string }>(
|
||||
value.slice(encryptedSecretPrefix.length),
|
||||
);
|
||||
return decrypted.value;
|
||||
}
|
||||
|
||||
// Compatibility for any earlier secret_settings rows stored without encryption.
|
||||
return value;
|
||||
}
|
||||
|
||||
private async normalizeStoredSecret(
|
||||
canonicalKey: TCanonicalSecretSettingKey,
|
||||
sourceKey: string,
|
||||
storedValue: string,
|
||||
decryptedValue: string,
|
||||
): Promise<void> {
|
||||
if (sourceKey !== canonicalKey || !storedValue.startsWith(encryptedSecretPrefix)) {
|
||||
await this.set(canonicalKey, decryptedValue);
|
||||
if (sourceKey !== canonicalKey) {
|
||||
this.authRepo.deleteSecretSetting(sourceKey);
|
||||
}
|
||||
}
|
||||
|
||||
this.authRepo.deleteSetting(canonicalKey);
|
||||
for (const aliasKey of secretSettingAliases[canonicalKey]) {
|
||||
this.authRepo.deleteSetting(aliasKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as plugins from '../plugins.ts';
|
||||
import { logger } from '../logging.ts';
|
||||
import type { Onebox } from '../classes/onebox.ts';
|
||||
import * as interfaces from '../../ts_interfaces/index.ts';
|
||||
import * as handlers from './handlers/index.ts';
|
||||
import { files as bundledFiles } from '../../ts_bundled/bundle.ts';
|
||||
|
||||
@@ -77,4 +78,28 @@ export class OpsServer {
|
||||
logger.success('OpsServer stopped');
|
||||
}
|
||||
}
|
||||
|
||||
public async pushDashboardEvent(method: string, payload: unknown): Promise<void> {
|
||||
const typedsocket = (this.server as any)?.typedserver?.typedsocket;
|
||||
if (!typedsocket) {
|
||||
return;
|
||||
}
|
||||
|
||||
const connections = await typedsocket.findAllTargetConnectionsByTag('role', 'ops_dashboard');
|
||||
await Promise.allSettled(
|
||||
connections.map((connection: any) => typedsocket.createTypedRequest(method, connection).fire(payload)),
|
||||
);
|
||||
}
|
||||
|
||||
public async broadcastServiceUpdate(
|
||||
serviceName: string,
|
||||
action: interfaces.requests.IReq_PushServiceUpdate['request']['action'],
|
||||
service?: interfaces.data.IService | null,
|
||||
): Promise<void> {
|
||||
await this.pushDashboardEvent('pushServiceUpdate', {
|
||||
action,
|
||||
serviceName,
|
||||
service: service || undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,21 +91,8 @@ export class PlatformHandler {
|
||||
line: string,
|
||||
isError: boolean,
|
||||
): void {
|
||||
const typedsocket = (this.opsServerRef.server as any)?.typedserver?.typedsocket;
|
||||
if (!typedsocket) return;
|
||||
|
||||
const entry = this.parseLogLine(line, isError);
|
||||
|
||||
typedsocket.findAllTargetConnectionsByTag('role', 'ops_dashboard')
|
||||
.then((connections: any[]) => {
|
||||
for (const conn of connections) {
|
||||
typedsocket.createTypedRequest(
|
||||
'pushPlatformServiceLog',
|
||||
conn,
|
||||
).fire({ serviceType, entry }).catch(() => {});
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
void this.opsServerRef.pushDashboardEvent('pushPlatformServiceLog', { serviceType, entry });
|
||||
}
|
||||
|
||||
private pushServiceLogToClients(
|
||||
@@ -113,21 +100,8 @@ export class PlatformHandler {
|
||||
line: string,
|
||||
isError: boolean,
|
||||
): void {
|
||||
const typedsocket = (this.opsServerRef.server as any)?.typedserver?.typedsocket;
|
||||
if (!typedsocket) return;
|
||||
|
||||
const entry = this.parseLogLine(line, isError);
|
||||
|
||||
typedsocket.findAllTargetConnectionsByTag('role', 'ops_dashboard')
|
||||
.then((connections: any[]) => {
|
||||
for (const conn of connections) {
|
||||
typedsocket.createTypedRequest(
|
||||
'pushServiceLog',
|
||||
conn,
|
||||
).fire({ serviceName, entry }).catch(() => {});
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
void this.opsServerRef.pushDashboardEvent('pushServiceLog', { serviceName, entry });
|
||||
}
|
||||
|
||||
private registerHandlers(): void {
|
||||
|
||||
@@ -11,12 +11,13 @@ export class SettingsHandler {
|
||||
this.registerHandlers();
|
||||
}
|
||||
|
||||
private getSettingsObject(): interfaces.data.ISettings {
|
||||
private async getSettingsObject(): Promise<interfaces.data.ISettings> {
|
||||
const db = this.opsServerRef.oneboxRef.database;
|
||||
const settingsMap = db.getAllSettings(); // Returns Record<string, string>
|
||||
const cloudflareToken = await db.getSecretSetting('cloudflareToken');
|
||||
const settingsMap = db.getAllSettings();
|
||||
|
||||
return {
|
||||
cloudflareToken: settingsMap['cloudflareToken'] || settingsMap['cloudflareAPIKey'] || '',
|
||||
cloudflareToken: cloudflareToken || '',
|
||||
cloudflareZoneId: settingsMap['cloudflareZoneId'] || '',
|
||||
autoRenewCerts: settingsMap['autoRenewCerts'] === 'true',
|
||||
renewalThreshold: parseInt(settingsMap['renewalThreshold'] || '30', 10),
|
||||
@@ -33,7 +34,7 @@ export class SettingsHandler {
|
||||
'getSettings',
|
||||
async (dataArg) => {
|
||||
await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
|
||||
const settings = this.getSettingsObject();
|
||||
const settings = await this.getSettingsObject();
|
||||
return { settings };
|
||||
},
|
||||
),
|
||||
@@ -50,16 +51,15 @@ export class SettingsHandler {
|
||||
// Store each setting as key-value pair
|
||||
for (const [key, value] of Object.entries(updates)) {
|
||||
if (value !== undefined) {
|
||||
if (key === 'cloudflareToken') {
|
||||
db.setSetting('cloudflareToken', String(value));
|
||||
db.setSetting('cloudflareAPIKey', String(value));
|
||||
if (db.isSecretSettingKey(key)) {
|
||||
await db.setSecretSetting(key, String(value));
|
||||
} else {
|
||||
db.setSetting(key, String(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const settings = this.getSettingsObject();
|
||||
const settings = await this.getSettingsObject();
|
||||
return { settings };
|
||||
},
|
||||
),
|
||||
@@ -70,8 +70,7 @@ export class SettingsHandler {
|
||||
'setBackupPassword',
|
||||
async (dataArg) => {
|
||||
await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
|
||||
this.opsServerRef.oneboxRef.database.setSetting('backup_encryption_password', dataArg.password);
|
||||
this.opsServerRef.oneboxRef.database.setSetting('backupPassword', dataArg.password);
|
||||
await this.opsServerRef.oneboxRef.database.setSecretSetting('backupPassword', dataArg.password);
|
||||
return { ok: true };
|
||||
},
|
||||
),
|
||||
@@ -82,9 +81,7 @@ export class SettingsHandler {
|
||||
'getBackupPasswordStatus',
|
||||
async (dataArg) => {
|
||||
await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
|
||||
const backupPassword = this.opsServerRef.oneboxRef.database.getSetting('backupPassword')
|
||||
|| this.opsServerRef.oneboxRef.database.getSetting('backup_encryption_password');
|
||||
const isConfigured = !!backupPassword;
|
||||
const isConfigured = await this.opsServerRef.oneboxRef.database.hasSecretSetting('backupPassword');
|
||||
return { status: { isConfigured } };
|
||||
},
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user