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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user