Files
onebox/ts/classes/onebox.ts
Juergen Kunz 5cd7e7c252 feat(backup): Add backup system: BackupManager, DB schema, API endpoints and UI support
Introduce a complete service backup/restore subsystem with encrypted archives, database records and REST endpoints. Implements BackupManager with export/import for service config, platform resources (MongoDB, MinIO, ClickHouse), and Docker images; adds BackupRepository and migrations for backups table and include_image_in_backup; integrates backup flows into the HTTP API and the UI client; exposes backup password management and restore modes (restore/import/clone). Wire BackupManager into Onebox initialization.
2025-11-27 13:48:11 +00:00

361 lines
11 KiB
TypeScript

/**
* Main Onebox coordinator class
*
* Coordinates all components and provides the main API
*/
import { logger } from '../logging.ts';
import { getErrorMessage } from '../utils/error.ts';
import { OneboxDatabase } from './database.ts';
import { OneboxDockerManager } from './docker.ts';
import { OneboxServicesManager } from './services.ts';
import { OneboxRegistriesManager } from './registries.ts';
import { OneboxReverseProxy } from './reverseproxy.ts';
import { OneboxDnsManager } from './dns.ts';
import { OneboxSslManager } from './ssl.ts';
import { OneboxDaemon } from './daemon.ts';
import { OneboxHttpServer } from './httpserver.ts';
import { CloudflareDomainSync } from './cloudflare-sync.ts';
import { CertRequirementManager } from './cert-requirement-manager.ts';
import { RegistryManager } from './registry.ts';
import { PlatformServicesManager } from './platform-services/index.ts';
import { CaddyLogReceiver } from './caddy-log-receiver.ts';
import { BackupManager } from './backup-manager.ts';
export class Onebox {
public database: OneboxDatabase;
public docker: OneboxDockerManager;
public services: OneboxServicesManager;
public registries: OneboxRegistriesManager;
public reverseProxy: OneboxReverseProxy;
public dns: OneboxDnsManager;
public ssl: OneboxSslManager;
public daemon: OneboxDaemon;
public httpServer: OneboxHttpServer;
public cloudflareDomainSync: CloudflareDomainSync;
public certRequirementManager: CertRequirementManager;
public registry: RegistryManager;
public platformServices: PlatformServicesManager;
public caddyLogReceiver: CaddyLogReceiver;
public backupManager: BackupManager;
private initialized = false;
constructor() {
// Initialize database first
this.database = new OneboxDatabase();
// Initialize managers (passing reference to main Onebox instance)
this.docker = new OneboxDockerManager();
this.services = new OneboxServicesManager(this);
this.registries = new OneboxRegistriesManager(this);
this.reverseProxy = new OneboxReverseProxy(this);
this.dns = new OneboxDnsManager(this);
this.ssl = new OneboxSslManager(this);
this.daemon = new OneboxDaemon(this);
this.httpServer = new OneboxHttpServer(this);
this.registry = new RegistryManager({
dataDir: './.nogit/registry-data',
port: 4000,
baseUrl: 'localhost:5000',
});
// Initialize domain management
this.cloudflareDomainSync = new CloudflareDomainSync(this.database);
this.certRequirementManager = new CertRequirementManager(this.database, this.ssl);
// Initialize platform services manager
this.platformServices = new PlatformServicesManager(this);
// Initialize Caddy log receiver
this.caddyLogReceiver = new CaddyLogReceiver(9999);
// Initialize Backup manager
this.backupManager = new BackupManager(this);
}
/**
* Initialize all components
*/
async init(): Promise<void> {
try {
logger.info('Initializing Onebox...');
// Initialize database
await this.database.init();
// Ensure default admin user exists
await this.ensureDefaultUser();
// Initialize Docker
await this.docker.init();
// Start Caddy log receiver BEFORE reverse proxy (so Caddy can connect to it)
try {
await this.caddyLogReceiver.start();
} catch (error) {
logger.warn(`Failed to start Caddy log receiver: ${getErrorMessage(error)}`);
}
// Initialize Reverse Proxy
await this.reverseProxy.init();
// Load routes and certificates for reverse proxy
await this.reverseProxy.reloadRoutes();
await this.reverseProxy.reloadCertificates();
// Start HTTP reverse proxy (non-critical - don't fail init if ports are busy)
// Use 8080/8443 in dev mode to avoid permission issues
const isDev = Deno.env.get('ONEBOX_DEV') === 'true' || Deno.args.includes('--ephemeral');
const httpPort = isDev ? 8080 : 80;
const httpsPort = isDev ? 8443 : 443;
try {
await this.reverseProxy.startHttp(httpPort);
} catch (error) {
logger.warn(`Failed to start HTTP reverse proxy: ${getErrorMessage(error)}`);
}
// Start HTTPS reverse proxy if certificates are available
try {
await this.reverseProxy.startHttps(httpsPort);
} catch (error) {
logger.warn(`Failed to start HTTPS reverse proxy: ${getErrorMessage(error)}`);
}
// Initialize DNS (non-critical)
try {
await this.dns.init();
} catch (error) {
logger.warn('DNS initialization failed - DNS features will be disabled');
}
// Initialize SSL (non-critical)
try {
await this.ssl.init();
} catch (error) {
logger.warn('SSL initialization failed - SSL features will be limited');
}
// Initialize Cloudflare domain sync (non-critical)
try {
await this.cloudflareDomainSync.init();
} catch (error) {
logger.warn('Cloudflare domain sync initialization failed - domain sync will be limited');
}
// Initialize Onebox Registry (non-critical)
try {
await this.registry.init();
} catch (error) {
logger.warn('Onebox Registry initialization failed - local registry will be disabled');
logger.warn(`Error: ${getErrorMessage(error)}`);
}
// Initialize Platform Services (non-critical)
try {
await this.platformServices.init();
} catch (error) {
logger.warn('Platform services initialization failed - MongoDB/S3 features will be limited');
logger.warn(`Error: ${getErrorMessage(error)}`);
}
// Login to all registries
await this.registries.loginToAllRegistries();
// Start auto-update monitoring for registry services
this.services.startAutoUpdateMonitoring();
this.initialized = true;
logger.success('Onebox initialized successfully');
} catch (error) {
logger.error(`Failed to initialize Onebox: ${getErrorMessage(error)}`);
throw error;
}
}
/**
* Ensure default admin user exists
*/
private async ensureDefaultUser(): Promise<void> {
try {
const adminUser = this.database.getUserByUsername('admin');
if (!adminUser) {
logger.info('Creating default admin user...');
// Simple base64 encoding for now - should use bcrypt in production
const passwordHash = btoa('admin');
await this.database.createUser({
username: 'admin',
passwordHash,
role: 'admin',
createdAt: Date.now(),
updatedAt: Date.now(),
});
logger.warn('Default admin user created with username: admin, password: admin');
logger.warn('IMPORTANT: Change the default password immediately!');
}
} catch (error) {
logger.error(`Failed to create default user: ${getErrorMessage(error)}`);
}
}
/**
* Check if Onebox is initialized
*/
isInitialized(): boolean {
return this.initialized;
}
/**
* Get system status
*/
async getSystemStatus() {
try {
const dockerRunning = await this.docker.isDockerRunning();
const proxyStatus = this.reverseProxy.getStatus();
const dnsConfigured = this.dns.isConfigured();
const sslConfigured = this.ssl.isConfigured();
const services = this.services.listServices();
const runningServices = services.filter((s) => s.status === 'running').length;
const totalServices = services.length;
// Get platform services status with resource counts
const platformServices = this.platformServices.getAllPlatformServices();
const providers = this.platformServices.getAllProviders();
const platformServicesStatus = providers.map((provider) => {
const service = platformServices.find((s) => s.type === provider.type);
// For Caddy, check actual runtime status since it starts without a DB record
let status = service?.status || 'not-deployed';
if (provider.type === 'caddy') {
status = proxyStatus.http.running ? 'running' : 'stopped';
}
// Count resources for this platform service
const resourceCount = service?.id
? this.database.getPlatformResourcesByPlatformService(service.id).length
: 0;
return {
type: provider.type,
displayName: provider.displayName,
status,
resourceCount,
};
});
// Get certificate health summary
const certificates = this.ssl.listCertificates();
const now = Date.now();
const thirtyDaysMs = 30 * 24 * 60 * 60 * 1000;
let validCount = 0;
let expiringCount = 0;
let expiredCount = 0;
const expiringDomains: { domain: string; daysRemaining: number }[] = [];
for (const cert of certificates) {
if (cert.expiryDate <= now) {
expiredCount++;
} else if (cert.expiryDate <= now + thirtyDaysMs) {
expiringCount++;
const daysRemaining = Math.floor((cert.expiryDate - now) / (24 * 60 * 60 * 1000));
expiringDomains.push({ domain: cert.domain, daysRemaining });
} else {
validCount++;
}
}
// Sort expiring domains by days remaining (ascending)
expiringDomains.sort((a, b) => a.daysRemaining - b.daysRemaining);
return {
docker: {
running: dockerRunning,
version: dockerRunning ? await this.docker.getDockerVersion() : null,
},
reverseProxy: proxyStatus,
dns: {
configured: dnsConfigured,
},
ssl: {
configured: sslConfigured,
certificateCount: this.ssl.listCertificates().length,
},
services: {
total: totalServices,
running: runningServices,
stopped: totalServices - runningServices,
},
platformServices: platformServicesStatus,
certificateHealth: {
valid: validCount,
expiringSoon: expiringCount,
expired: expiredCount,
expiringDomains: expiringDomains.slice(0, 5), // Top 5 expiring
},
};
} catch (error) {
logger.error(`Failed to get system status: ${getErrorMessage(error)}`);
throw error;
}
}
/**
* Start daemon mode
*/
async startDaemon(): Promise<void> {
await this.daemon.start();
}
/**
* Stop daemon mode
*/
async stopDaemon(): Promise<void> {
await this.daemon.stop();
}
/**
* Start HTTP server
*/
async startHttpServer(port?: number): Promise<void> {
await this.httpServer.start(port);
}
/**
* Stop HTTP server
*/
async stopHttpServer(): Promise<void> {
await this.httpServer.stop();
}
/**
* Shutdown Onebox gracefully
*/
async shutdown(): Promise<void> {
try {
logger.info('Shutting down Onebox...');
// Stop daemon if running
await this.daemon.stop();
// Stop HTTP server if running
await this.httpServer.stop();
// Stop reverse proxy if running
await this.reverseProxy.stop();
// Stop Caddy log receiver
await this.caddyLogReceiver.stop();
// Close database
this.database.close();
logger.success('Onebox shutdown complete');
} catch (error) {
logger.error(`Error during shutdown: ${getErrorMessage(error)}`);
}
}
}