Initial commit: Onebox v1.0.0

- Complete Deno-based architecture following nupst/spark patterns
- SQLite database with full schema
- Docker container management
- Service orchestration (Docker + Nginx + DNS + SSL)
- Registry authentication
- Nginx reverse proxy configuration
- Cloudflare DNS integration
- Let's Encrypt SSL automation
- Background daemon with metrics collection
- HTTP API server
- Comprehensive CLI
- Cross-platform compilation setup
- NPM distribution wrapper
- Shell installer script

Core features:
- Deploy containers with single command
- Automatic domain configuration
- Automatic SSL certificates
- Multi-registry support
- Metrics and logging
- Systemd integration

Ready for Angular UI implementation and testing.
This commit is contained in:
2025-10-28 13:05:42 +00:00
commit 246a6073e0
29 changed files with 5227 additions and 0 deletions

24
ts/index.ts Normal file
View File

@@ -0,0 +1,24 @@
/**
* Main exports and CLI entry point for Onebox
*/
export { Onebox } from './onebox.classes.onebox.ts';
export { runCli } from './onebox.cli.ts';
export { OneboxDatabase } from './onebox.classes.database.ts';
export { OneboxDockerManager } from './onebox.classes.docker.ts';
export { OneboxServicesManager } from './onebox.classes.services.ts';
export { OneboxRegistriesManager } from './onebox.classes.registries.ts';
export { OneboxNginxManager } from './onebox.classes.nginx.ts';
export { OneboxDnsManager } from './onebox.classes.dns.ts';
export { OneboxSslManager } from './onebox.classes.ssl.ts';
export { OneboxDaemon } from './onebox.classes.daemon.ts';
export { OneboxHttpServer } from './onebox.classes.httpserver.ts';
// Types
export * from './onebox.types.ts';
// Logging
export { logger } from './onebox.logging.ts';
// Version info
export { projectInfo } from './onebox.info.ts';

292
ts/onebox.classes.daemon.ts Normal file
View File

@@ -0,0 +1,292 @@
/**
* Daemon Manager for Onebox
*
* Handles background monitoring, metrics collection, and automatic tasks
*/
import * as plugins from './onebox.plugins.ts';
import { logger } from './onebox.logging.ts';
import { projectInfo } from './onebox.info.ts';
import type { Onebox } from './onebox.classes.onebox.ts';
export class OneboxDaemon {
private oneboxRef: Onebox;
private smartdaemon: plugins.smartdaemon.SmartDaemon;
private running = false;
private monitoringInterval: number | null = null;
private metricsInterval = 60000; // 1 minute
constructor(oneboxRef: Onebox) {
this.oneboxRef = oneboxRef;
this.smartdaemon = new plugins.smartdaemon.SmartDaemon();
// Get metrics interval from settings
const customInterval = this.oneboxRef.database.getSetting('metricsInterval');
if (customInterval) {
this.metricsInterval = parseInt(customInterval, 10);
}
}
/**
* Install systemd service
*/
async installService(): Promise<void> {
try {
logger.info('Installing Onebox daemon service...');
// Get installation directory
const execPath = Deno.execPath();
const service = await this.smartdaemon.addService({
name: 'onebox',
version: projectInfo.version,
command: `${execPath} run --allow-all ${Deno.cwd()}/mod.ts daemon start`,
description: 'Onebox - Self-hosted container platform',
workingDir: Deno.cwd(),
});
await service.save();
await service.enable();
logger.success('Onebox daemon service installed');
logger.info('Start with: sudo systemctl start smartdaemon_onebox');
} catch (error) {
logger.error(`Failed to install daemon service: ${error.message}`);
throw error;
}
}
/**
* Uninstall systemd service
*/
async uninstallService(): Promise<void> {
try {
logger.info('Uninstalling Onebox daemon service...');
const service = await this.smartdaemon.getService('onebox');
if (service) {
await service.stop();
await service.disable();
await service.delete();
}
logger.success('Onebox daemon service uninstalled');
} catch (error) {
logger.error(`Failed to uninstall daemon service: ${error.message}`);
throw error;
}
}
/**
* Start daemon mode (background monitoring)
*/
async start(): Promise<void> {
try {
if (this.running) {
logger.warn('Daemon already running');
return;
}
logger.info('Starting Onebox daemon...');
this.running = true;
// Start monitoring loop
this.startMonitoring();
// Start HTTP server
const httpPort = parseInt(this.oneboxRef.database.getSetting('httpPort') || '3000', 10);
await this.oneboxRef.httpServer.start(httpPort);
logger.success('Onebox daemon started');
logger.info(`Web UI available at http://localhost:${httpPort}`);
// Keep process alive
await this.keepAlive();
} catch (error) {
logger.error(`Failed to start daemon: ${error.message}`);
this.running = false;
throw error;
}
}
/**
* Stop daemon mode
*/
async stop(): Promise<void> {
try {
if (!this.running) {
return;
}
logger.info('Stopping Onebox daemon...');
this.running = false;
// Stop monitoring
this.stopMonitoring();
// Stop HTTP server
await this.oneboxRef.httpServer.stop();
logger.success('Onebox daemon stopped');
} catch (error) {
logger.error(`Failed to stop daemon: ${error.message}`);
throw error;
}
}
/**
* Start monitoring loop
*/
private startMonitoring(): void {
logger.info('Starting monitoring loop...');
this.monitoringInterval = setInterval(async () => {
await this.monitoringTick();
}, this.metricsInterval);
// Run first tick immediately
this.monitoringTick();
}
/**
* Stop monitoring loop
*/
private stopMonitoring(): void {
if (this.monitoringInterval !== null) {
clearInterval(this.monitoringInterval);
this.monitoringInterval = null;
logger.debug('Monitoring loop stopped');
}
}
/**
* Single monitoring tick
*/
private async monitoringTick(): Promise<void> {
try {
logger.debug('Running monitoring tick...');
// Collect metrics for all services
await this.collectMetrics();
// Sync service statuses
await this.oneboxRef.services.syncAllServiceStatuses();
// Check SSL certificate expiration
await this.checkSSLExpiration();
// Check service health (TODO: implement health checks)
logger.debug('Monitoring tick complete');
} catch (error) {
logger.error(`Monitoring tick failed: ${error.message}`);
}
}
/**
* Collect metrics for all services
*/
private async collectMetrics(): Promise<void> {
try {
const services = this.oneboxRef.services.listServices();
for (const service of services) {
if (service.status === 'running' && service.containerID) {
try {
const stats = await this.oneboxRef.docker.getContainerStats(service.containerID);
if (stats) {
this.oneboxRef.database.addMetric({
serviceId: service.id!,
timestamp: Date.now(),
cpuPercent: stats.cpuPercent,
memoryUsed: stats.memoryUsed,
memoryLimit: stats.memoryLimit,
networkRxBytes: stats.networkRx,
networkTxBytes: stats.networkTx,
});
}
} catch (error) {
logger.debug(`Failed to collect metrics for ${service.name}: ${error.message}`);
}
}
}
} catch (error) {
logger.error(`Failed to collect metrics: ${error.message}`);
}
}
/**
* Check SSL certificate expiration
*/
private async checkSSLExpiration(): Promise<void> {
try {
if (!this.oneboxRef.ssl.isConfigured()) {
return;
}
await this.oneboxRef.ssl.renewExpiring();
} catch (error) {
logger.error(`Failed to check SSL expiration: ${error.message}`);
}
}
/**
* Keep process alive
*/
private async keepAlive(): Promise<void> {
// Set up signal handlers
const signalHandler = () => {
logger.info('Received shutdown signal');
this.stop().then(() => {
Deno.exit(0);
});
};
Deno.addSignalListener('SIGINT', signalHandler);
Deno.addSignalListener('SIGTERM', signalHandler);
// Keep event loop alive
while (this.running) {
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}
/**
* Get daemon status
*/
isRunning(): boolean {
return this.running;
}
/**
* Get service status from systemd
*/
async getServiceStatus(): Promise<string> {
try {
const command = new Deno.Command('systemctl', {
args: ['status', 'smartdaemon_onebox'],
stdout: 'piped',
stderr: 'piped',
});
const { code, stdout } = await command.output();
const output = new TextDecoder().decode(stdout);
if (code === 0 || output.includes('active (running)')) {
return 'running';
} else if (output.includes('inactive') || output.includes('dead')) {
return 'stopped';
} else if (output.includes('failed')) {
return 'failed';
} else {
return 'unknown';
}
} catch (error) {
return 'not-installed';
}
}
}

View File

@@ -0,0 +1,659 @@
/**
* Database layer for Onebox using SQLite
*/
import * as plugins from './onebox.plugins.ts';
import type {
IService,
IRegistry,
INginxConfig,
ISslCertificate,
IDnsRecord,
IMetric,
ILogEntry,
IUser,
ISetting,
} from './onebox.types.ts';
import { logger } from './onebox.logging.ts';
export class OneboxDatabase {
private db: plugins.sqlite.DB | null = null;
private dbPath: string;
constructor(dbPath = '/var/lib/onebox/onebox.db') {
this.dbPath = dbPath;
}
/**
* Initialize database connection and create tables
*/
async init(): Promise<void> {
try {
// Ensure data directory exists
const dbDir = plugins.path.dirname(this.dbPath);
await Deno.mkdir(dbDir, { recursive: true });
// Open database
this.db = new plugins.sqlite.DB(this.dbPath);
logger.info(`Database initialized at ${this.dbPath}`);
// Create tables
await this.createTables();
// Run migrations if needed
await this.runMigrations();
} catch (error) {
logger.error(`Failed to initialize database: ${error.message}`);
throw error;
}
}
/**
* Create all database tables
*/
private async createTables(): Promise<void> {
if (!this.db) throw new Error('Database not initialized');
// Services table
this.db.query(`
CREATE TABLE IF NOT EXISTS services (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
image TEXT NOT NULL,
registry TEXT,
env_vars TEXT NOT NULL,
port INTEGER NOT NULL,
domain TEXT,
container_id TEXT,
status TEXT NOT NULL DEFAULT 'stopped',
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
)
`);
// Registries table
this.db.query(`
CREATE TABLE IF NOT EXISTS registries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
url TEXT NOT NULL UNIQUE,
username TEXT NOT NULL,
password_encrypted TEXT NOT NULL,
created_at INTEGER NOT NULL
)
`);
// Nginx configs table
this.db.query(`
CREATE TABLE IF NOT EXISTS nginx_configs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
service_id INTEGER NOT NULL,
domain TEXT NOT NULL,
port INTEGER NOT NULL,
ssl_enabled INTEGER NOT NULL DEFAULT 0,
config_template TEXT NOT NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
FOREIGN KEY (service_id) REFERENCES services(id) ON DELETE CASCADE
)
`);
// SSL certificates table
this.db.query(`
CREATE TABLE IF NOT EXISTS ssl_certificates (
id INTEGER PRIMARY KEY AUTOINCREMENT,
domain TEXT NOT NULL UNIQUE,
cert_path TEXT NOT NULL,
key_path TEXT NOT NULL,
full_chain_path TEXT NOT NULL,
expiry_date INTEGER NOT NULL,
issuer TEXT NOT NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
)
`);
// DNS records table
this.db.query(`
CREATE TABLE IF NOT EXISTS dns_records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
domain TEXT NOT NULL UNIQUE,
type TEXT NOT NULL,
value TEXT NOT NULL,
cloudflare_id TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
)
`);
// Metrics table
this.db.query(`
CREATE TABLE IF NOT EXISTS metrics (
id INTEGER PRIMARY KEY AUTOINCREMENT,
service_id INTEGER NOT NULL,
timestamp INTEGER NOT NULL,
cpu_percent REAL NOT NULL,
memory_used INTEGER NOT NULL,
memory_limit INTEGER NOT NULL,
network_rx_bytes INTEGER NOT NULL,
network_tx_bytes INTEGER NOT NULL,
FOREIGN KEY (service_id) REFERENCES services(id) ON DELETE CASCADE
)
`);
// Create index for metrics queries
this.db.query(`
CREATE INDEX IF NOT EXISTS idx_metrics_service_timestamp
ON metrics(service_id, timestamp DESC)
`);
// Logs table
this.db.query(`
CREATE TABLE IF NOT EXISTS logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
service_id INTEGER NOT NULL,
timestamp INTEGER NOT NULL,
message TEXT NOT NULL,
level TEXT NOT NULL,
source TEXT NOT NULL,
FOREIGN KEY (service_id) REFERENCES services(id) ON DELETE CASCADE
)
`);
// Create index for logs queries
this.db.query(`
CREATE INDEX IF NOT EXISTS idx_logs_service_timestamp
ON logs(service_id, timestamp DESC)
`);
// Users table
this.db.query(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'user',
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
)
`);
// Settings table
this.db.query(`
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
updated_at INTEGER NOT NULL
)
`);
// Version table for migrations
this.db.query(`
CREATE TABLE IF NOT EXISTS migrations (
version INTEGER PRIMARY KEY,
applied_at INTEGER NOT NULL
)
`);
logger.debug('Database tables created successfully');
}
/**
* Run database migrations
*/
private async runMigrations(): Promise<void> {
if (!this.db) throw new Error('Database not initialized');
const currentVersion = this.getMigrationVersion();
logger.debug(`Current database version: ${currentVersion}`);
// Add migration logic here as needed
// For now, just set version to 1
if (currentVersion === 0) {
this.setMigrationVersion(1);
}
}
/**
* Get current migration version
*/
private getMigrationVersion(): number {
if (!this.db) throw new Error('Database not initialized');
try {
const result = this.db.query('SELECT MAX(version) as version FROM migrations');
return result.length > 0 && result[0][0] !== null ? Number(result[0][0]) : 0;
} catch {
return 0;
}
}
/**
* Set migration version
*/
private setMigrationVersion(version: number): void {
if (!this.db) throw new Error('Database not initialized');
this.db.query('INSERT INTO migrations (version, applied_at) VALUES (?, ?)', [
version,
Date.now(),
]);
logger.debug(`Migration version set to ${version}`);
}
/**
* Close database connection
*/
close(): void {
if (this.db) {
this.db.close();
this.db = null;
logger.debug('Database connection closed');
}
}
/**
* Execute a raw query
*/
query<T = unknown[]>(sql: string, params: unknown[] = []): T[] {
if (!this.db) throw new Error('Database not initialized');
return this.db.query(sql, params) as T[];
}
// ============ Services CRUD ============
async createService(service: Omit<IService, 'id'>): Promise<IService> {
if (!this.db) throw new Error('Database not initialized');
const now = Date.now();
this.db.query(
`INSERT INTO services (name, image, registry, env_vars, port, domain, container_id, status, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
service.name,
service.image,
service.registry || null,
JSON.stringify(service.envVars),
service.port,
service.domain || null,
service.containerID || null,
service.status,
now,
now,
]
);
return this.getServiceByName(service.name)!;
}
getServiceByName(name: string): IService | null {
if (!this.db) throw new Error('Database not initialized');
const rows = this.db.query('SELECT * FROM services WHERE name = ?', [name]);
return rows.length > 0 ? this.rowToService(rows[0]) : null;
}
getServiceByID(id: number): IService | null {
if (!this.db) throw new Error('Database not initialized');
const rows = this.db.query('SELECT * FROM services WHERE id = ?', [id]);
return rows.length > 0 ? this.rowToService(rows[0]) : null;
}
getAllServices(): IService[] {
if (!this.db) throw new Error('Database not initialized');
const rows = this.db.query('SELECT * FROM services ORDER BY created_at DESC');
return rows.map((row) => this.rowToService(row));
}
updateService(id: number, updates: Partial<IService>): void {
if (!this.db) throw new Error('Database not initialized');
const fields: string[] = [];
const values: unknown[] = [];
if (updates.image !== undefined) {
fields.push('image = ?');
values.push(updates.image);
}
if (updates.registry !== undefined) {
fields.push('registry = ?');
values.push(updates.registry);
}
if (updates.envVars !== undefined) {
fields.push('env_vars = ?');
values.push(JSON.stringify(updates.envVars));
}
if (updates.port !== undefined) {
fields.push('port = ?');
values.push(updates.port);
}
if (updates.domain !== undefined) {
fields.push('domain = ?');
values.push(updates.domain);
}
if (updates.containerID !== undefined) {
fields.push('container_id = ?');
values.push(updates.containerID);
}
if (updates.status !== undefined) {
fields.push('status = ?');
values.push(updates.status);
}
fields.push('updated_at = ?');
values.push(Date.now());
values.push(id);
this.db.query(`UPDATE services SET ${fields.join(', ')} WHERE id = ?`, values);
}
deleteService(id: number): void {
if (!this.db) throw new Error('Database not initialized');
this.db.query('DELETE FROM services WHERE id = ?', [id]);
}
private rowToService(row: unknown[]): IService {
return {
id: Number(row[0]),
name: String(row[1]),
image: String(row[2]),
registry: row[3] ? String(row[3]) : undefined,
envVars: JSON.parse(String(row[4])),
port: Number(row[5]),
domain: row[6] ? String(row[6]) : undefined,
containerID: row[7] ? String(row[7]) : undefined,
status: String(row[8]) as IService['status'],
createdAt: Number(row[9]),
updatedAt: Number(row[10]),
};
}
// ============ Registries CRUD ============
async createRegistry(registry: Omit<IRegistry, 'id'>): Promise<IRegistry> {
if (!this.db) throw new Error('Database not initialized');
const now = Date.now();
this.db.query(
'INSERT INTO registries (url, username, password_encrypted, created_at) VALUES (?, ?, ?, ?)',
[registry.url, registry.username, registry.passwordEncrypted, now]
);
return this.getRegistryByURL(registry.url)!;
}
getRegistryByURL(url: string): IRegistry | null {
if (!this.db) throw new Error('Database not initialized');
const rows = this.db.query('SELECT * FROM registries WHERE url = ?', [url]);
return rows.length > 0 ? this.rowToRegistry(rows[0]) : null;
}
getAllRegistries(): IRegistry[] {
if (!this.db) throw new Error('Database not initialized');
const rows = this.db.query('SELECT * FROM registries ORDER BY created_at DESC');
return rows.map((row) => this.rowToRegistry(row));
}
deleteRegistry(url: string): void {
if (!this.db) throw new Error('Database not initialized');
this.db.query('DELETE FROM registries WHERE url = ?', [url]);
}
private rowToRegistry(row: unknown[]): IRegistry {
return {
id: Number(row[0]),
url: String(row[1]),
username: String(row[2]),
passwordEncrypted: String(row[3]),
createdAt: Number(row[4]),
};
}
// ============ Settings CRUD ============
getSetting(key: string): string | null {
if (!this.db) throw new Error('Database not initialized');
const rows = this.db.query('SELECT value FROM settings WHERE key = ?', [key]);
return rows.length > 0 ? String(rows[0][0]) : null;
}
setSetting(key: string, value: string): void {
if (!this.db) throw new Error('Database not initialized');
const now = Date.now();
this.db.query(
'INSERT OR REPLACE INTO settings (key, value, updated_at) VALUES (?, ?, ?)',
[key, value, now]
);
}
getAllSettings(): Record<string, string> {
if (!this.db) throw new Error('Database not initialized');
const rows = this.db.query('SELECT key, value FROM settings');
const settings: Record<string, string> = {};
for (const row of rows) {
settings[String(row[0])] = String(row[1]);
}
return settings;
}
// ============ Users CRUD ============
async createUser(user: Omit<IUser, 'id'>): Promise<IUser> {
if (!this.db) throw new Error('Database not initialized');
const now = Date.now();
this.db.query(
'INSERT INTO users (username, password_hash, role, created_at, updated_at) VALUES (?, ?, ?, ?, ?)',
[user.username, user.passwordHash, user.role, now, now]
);
return this.getUserByUsername(user.username)!;
}
getUserByUsername(username: string): IUser | null {
if (!this.db) throw new Error('Database not initialized');
const rows = this.db.query('SELECT * FROM users WHERE username = ?', [username]);
return rows.length > 0 ? this.rowToUser(rows[0]) : null;
}
getAllUsers(): IUser[] {
if (!this.db) throw new Error('Database not initialized');
const rows = this.db.query('SELECT * FROM users ORDER BY created_at DESC');
return rows.map((row) => this.rowToUser(row));
}
updateUserPassword(username: string, passwordHash: string): void {
if (!this.db) throw new Error('Database not initialized');
this.db.query('UPDATE users SET password_hash = ?, updated_at = ? WHERE username = ?', [
passwordHash,
Date.now(),
username,
]);
}
deleteUser(username: string): void {
if (!this.db) throw new Error('Database not initialized');
this.db.query('DELETE FROM users WHERE username = ?', [username]);
}
private rowToUser(row: unknown[]): IUser {
return {
id: Number(row[0]),
username: String(row[1]),
passwordHash: String(row[2]),
role: String(row[3]) as IUser['role'],
createdAt: Number(row[4]),
updatedAt: Number(row[5]),
};
}
// ============ Metrics ============
addMetric(metric: Omit<IMetric, 'id'>): void {
if (!this.db) throw new Error('Database not initialized');
this.db.query(
`INSERT INTO metrics (service_id, timestamp, cpu_percent, memory_used, memory_limit, network_rx_bytes, network_tx_bytes)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
[
metric.serviceId,
metric.timestamp,
metric.cpuPercent,
metric.memoryUsed,
metric.memoryLimit,
metric.networkRxBytes,
metric.networkTxBytes,
]
);
}
getMetrics(serviceId: number, limit = 100): IMetric[] {
if (!this.db) throw new Error('Database not initialized');
const rows = this.db.query(
'SELECT * FROM metrics WHERE service_id = ? ORDER BY timestamp DESC LIMIT ?',
[serviceId, limit]
);
return rows.map((row) => this.rowToMetric(row));
}
private rowToMetric(row: unknown[]): IMetric {
return {
id: Number(row[0]),
serviceId: Number(row[1]),
timestamp: Number(row[2]),
cpuPercent: Number(row[3]),
memoryUsed: Number(row[4]),
memoryLimit: Number(row[5]),
networkRxBytes: Number(row[6]),
networkTxBytes: Number(row[7]),
};
}
// ============ Logs ============
addLog(log: Omit<ILogEntry, 'id'>): void {
if (!this.db) throw new Error('Database not initialized');
this.db.query(
'INSERT INTO logs (service_id, timestamp, message, level, source) VALUES (?, ?, ?, ?, ?)',
[log.serviceId, log.timestamp, log.message, log.level, log.source]
);
}
getLogs(serviceId: number, limit = 1000): ILogEntry[] {
if (!this.db) throw new Error('Database not initialized');
const rows = this.db.query(
'SELECT * FROM logs WHERE service_id = ? ORDER BY timestamp DESC LIMIT ?',
[serviceId, limit]
);
return rows.map((row) => this.rowToLog(row));
}
private rowToLog(row: unknown[]): ILogEntry {
return {
id: Number(row[0]),
serviceId: Number(row[1]),
timestamp: Number(row[2]),
message: String(row[3]),
level: String(row[4]) as ILogEntry['level'],
source: String(row[5]) as ILogEntry['source'],
};
}
// ============ SSL Certificates ============
async createSSLCertificate(cert: Omit<ISslCertificate, 'id'>): Promise<ISslCertificate> {
if (!this.db) throw new Error('Database not initialized');
const now = Date.now();
this.db.query(
`INSERT INTO ssl_certificates (domain, cert_path, key_path, full_chain_path, expiry_date, issuer, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
[
cert.domain,
cert.certPath,
cert.keyPath,
cert.fullChainPath,
cert.expiryDate,
cert.issuer,
now,
now,
]
);
return this.getSSLCertificate(cert.domain)!;
}
getSSLCertificate(domain: string): ISslCertificate | null {
if (!this.db) throw new Error('Database not initialized');
const rows = this.db.query('SELECT * FROM ssl_certificates WHERE domain = ?', [domain]);
return rows.length > 0 ? this.rowToSSLCert(rows[0]) : null;
}
getAllSSLCertificates(): ISslCertificate[] {
if (!this.db) throw new Error('Database not initialized');
const rows = this.db.query('SELECT * FROM ssl_certificates ORDER BY expiry_date ASC');
return rows.map((row) => this.rowToSSLCert(row));
}
updateSSLCertificate(domain: string, updates: Partial<ISslCertificate>): void {
if (!this.db) throw new Error('Database not initialized');
const fields: string[] = [];
const values: unknown[] = [];
if (updates.certPath) {
fields.push('cert_path = ?');
values.push(updates.certPath);
}
if (updates.keyPath) {
fields.push('key_path = ?');
values.push(updates.keyPath);
}
if (updates.fullChainPath) {
fields.push('full_chain_path = ?');
values.push(updates.fullChainPath);
}
if (updates.expiryDate) {
fields.push('expiry_date = ?');
values.push(updates.expiryDate);
}
fields.push('updated_at = ?');
values.push(Date.now());
values.push(domain);
this.db.query(`UPDATE ssl_certificates SET ${fields.join(', ')} WHERE domain = ?`, values);
}
deleteSSLCertificate(domain: string): void {
if (!this.db) throw new Error('Database not initialized');
this.db.query('DELETE FROM ssl_certificates WHERE domain = ?', [domain]);
}
private rowToSSLCert(row: unknown[]): ISslCertificate {
return {
id: Number(row[0]),
domain: String(row[1]),
certPath: String(row[2]),
keyPath: String(row[3]),
fullChainPath: String(row[4]),
expiryDate: Number(row[5]),
issuer: String(row[6]),
createdAt: Number(row[7]),
updatedAt: Number(row[8]),
};
}
}

270
ts/onebox.classes.dns.ts Normal file
View File

@@ -0,0 +1,270 @@
/**
* DNS Manager for Onebox
*
* Manages DNS records via Cloudflare API
*/
import * as plugins from './onebox.plugins.ts';
import { logger } from './onebox.logging.ts';
import { OneboxDatabase } from './onebox.classes.database.ts';
export class OneboxDnsManager {
private oneboxRef: any;
private database: OneboxDatabase;
private cloudflareClient: plugins.cloudflare.Cloudflare | null = null;
private zoneID: string | null = null;
private serverIP: string | null = null;
constructor(oneboxRef: any) {
this.oneboxRef = oneboxRef;
this.database = oneboxRef.database;
}
/**
* Initialize DNS manager with Cloudflare credentials
*/
async init(): Promise<void> {
try {
// Get Cloudflare credentials from settings
const apiKey = this.database.getSetting('cloudflareAPIKey');
const email = this.database.getSetting('cloudflareEmail');
const zoneID = this.database.getSetting('cloudflareZoneID');
const serverIP = this.database.getSetting('serverIP');
if (!apiKey || !email || !zoneID) {
logger.warn('Cloudflare credentials not configured. DNS management will be disabled.');
logger.info('Configure with: onebox config set cloudflareAPIKey <key>');
return;
}
this.zoneID = zoneID;
this.serverIP = serverIP;
// Initialize Cloudflare client
this.cloudflareClient = new plugins.cloudflare.Cloudflare({
apiKey,
email,
});
logger.info('DNS manager initialized with Cloudflare');
} catch (error) {
logger.error(`Failed to initialize DNS manager: ${error.message}`);
throw error;
}
}
/**
* Check if DNS manager is configured
*/
isConfigured(): boolean {
return this.cloudflareClient !== null && this.zoneID !== null;
}
/**
* Add a DNS record for a domain
*/
async addDNSRecord(domain: string, ip?: string): Promise<void> {
try {
if (!this.isConfigured()) {
throw new Error('DNS manager not configured');
}
logger.info(`Adding DNS record for ${domain}`);
const targetIP = ip || this.serverIP;
if (!targetIP) {
throw new Error('Server IP not configured. Set with: onebox config set serverIP <ip>');
}
// Check if record already exists
const existing = await this.getDNSRecord(domain);
if (existing) {
logger.info(`DNS record already exists for ${domain}`);
return;
}
// Create A record
const response = await this.cloudflareClient!.zones.dns.records.create(this.zoneID!, {
type: 'A',
name: domain,
content: targetIP,
ttl: 1, // Auto
proxied: false, // Don't proxy through Cloudflare for direct SSL
});
// Store in database
await this.database.query(
'INSERT INTO dns_records (domain, type, value, cloudflare_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)',
[domain, 'A', targetIP, response.result.id, Date.now(), Date.now()]
);
logger.success(`DNS record created for ${domain}${targetIP}`);
} catch (error) {
logger.error(`Failed to add DNS record for ${domain}: ${error.message}`);
throw error;
}
}
/**
* Remove a DNS record
*/
async removeDNSRecord(domain: string): Promise<void> {
try {
if (!this.isConfigured()) {
throw new Error('DNS manager not configured');
}
logger.info(`Removing DNS record for ${domain}`);
// Get record from database
const rows = this.database.query('SELECT cloudflare_id FROM dns_records WHERE domain = ?', [
domain,
]);
if (rows.length === 0) {
logger.warn(`DNS record not found for ${domain}`);
return;
}
const cloudflareID = String(rows[0][0]);
// Delete from Cloudflare
if (cloudflareID) {
await this.cloudflareClient!.zones.dns.records.delete(this.zoneID!, cloudflareID);
}
// Delete from database
this.database.query('DELETE FROM dns_records WHERE domain = ?', [domain]);
logger.success(`DNS record removed for ${domain}`);
} catch (error) {
logger.error(`Failed to remove DNS record for ${domain}: ${error.message}`);
throw error;
}
}
/**
* Get DNS record for a domain
*/
async getDNSRecord(domain: string): Promise<any> {
try {
if (!this.isConfigured()) {
return null;
}
// Get from database first
const rows = this.database.query('SELECT * FROM dns_records WHERE domain = ?', [domain]);
if (rows.length > 0) {
return {
domain: String(rows[0][1]),
type: String(rows[0][2]),
value: String(rows[0][3]),
cloudflareID: rows[0][4] ? String(rows[0][4]) : null,
};
}
return null;
} catch (error) {
logger.error(`Failed to get DNS record for ${domain}: ${error.message}`);
return null;
}
}
/**
* List all DNS records
*/
listDNSRecords(): any[] {
try {
const rows = this.database.query('SELECT * FROM dns_records ORDER BY created_at DESC');
return rows.map((row) => ({
id: Number(row[0]),
domain: String(row[1]),
type: String(row[2]),
value: String(row[3]),
cloudflareID: row[4] ? String(row[4]) : null,
createdAt: Number(row[5]),
updatedAt: Number(row[6]),
}));
} catch (error) {
logger.error(`Failed to list DNS records: ${error.message}`);
return [];
}
}
/**
* Sync DNS records from Cloudflare
*/
async syncFromCloudflare(): Promise<void> {
try {
if (!this.isConfigured()) {
throw new Error('DNS manager not configured');
}
logger.info('Syncing DNS records from Cloudflare...');
const response = await this.cloudflareClient!.zones.dns.records.list(this.zoneID!);
const records = response.result;
// Only sync A records
const aRecords = records.filter((r: any) => r.type === 'A');
for (const record of aRecords) {
// Check if exists in database
const existing = await this.getDNSRecord(record.name);
if (!existing) {
// Add to database
await this.database.query(
'INSERT INTO dns_records (domain, type, value, cloudflare_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)',
[record.name, record.type, record.content, record.id, Date.now(), Date.now()]
);
logger.info(`Synced DNS record: ${record.name}`);
}
}
logger.success('DNS records synced from Cloudflare');
} catch (error) {
logger.error(`Failed to sync DNS records: ${error.message}`);
throw error;
}
}
/**
* Check if domain DNS is properly configured
*/
async checkDNS(domain: string): Promise<boolean> {
try {
logger.info(`Checking DNS for ${domain}...`);
// Use dig or nslookup to check DNS resolution
const command = new Deno.Command('dig', {
args: ['+short', domain],
stdout: 'piped',
stderr: 'piped',
});
const { code, stdout } = await command.output();
if (code !== 0) {
logger.warn(`DNS check failed for ${domain}`);
return false;
}
const ip = new TextDecoder().decode(stdout).trim();
if (ip === this.serverIP) {
logger.success(`DNS correctly points to ${ip}`);
return true;
} else {
logger.warn(`DNS points to ${ip}, expected ${this.serverIP}`);
return false;
}
} catch (error) {
logger.error(`Failed to check DNS for ${domain}: ${error.message}`);
return false;
}
}
}

489
ts/onebox.classes.docker.ts Normal file
View File

@@ -0,0 +1,489 @@
/**
* Docker Manager for Onebox
*
* Handles all Docker operations: containers, images, networks, volumes
*/
import * as plugins from './onebox.plugins.ts';
import type { IService, IContainerStats } from './onebox.types.ts';
import { logger } from './onebox.logging.ts';
export class OneboxDockerManager {
private dockerClient: plugins.docker.Docker | null = null;
private networkName = 'onebox-network';
/**
* Initialize Docker client and create onebox network
*/
async init(): Promise<void> {
try {
// Initialize Docker client (connects to /var/run/docker.sock by default)
this.dockerClient = new plugins.docker.Docker({
socketPath: '/var/run/docker.sock',
});
logger.info('Docker client initialized');
// Ensure onebox network exists
await this.ensureNetwork();
} catch (error) {
logger.error(`Failed to initialize Docker client: ${error.message}`);
throw error;
}
}
/**
* Ensure onebox network exists
*/
private async ensureNetwork(): Promise<void> {
try {
const networks = await this.dockerClient!.listNetworks();
const existingNetwork = networks.find((n: any) => n.Name === this.networkName);
if (!existingNetwork) {
logger.info(`Creating Docker network: ${this.networkName}`);
await this.dockerClient!.createNetwork({
Name: this.networkName,
Driver: 'bridge',
Labels: {
'managed-by': 'onebox',
},
});
logger.success(`Docker network created: ${this.networkName}`);
} else {
logger.debug(`Docker network already exists: ${this.networkName}`);
}
} catch (error) {
logger.error(`Failed to create Docker network: ${error.message}`);
throw error;
}
}
/**
* Pull an image from a registry
*/
async pullImage(image: string, registry?: string): Promise<void> {
try {
logger.info(`Pulling Docker image: ${image}`);
const fullImage = registry ? `${registry}/${image}` : image;
await this.dockerClient!.pull(fullImage, (error: any, stream: any) => {
if (error) {
throw error;
}
// Follow progress
this.dockerClient!.modem.followProgress(stream, (err: any, output: any) => {
if (err) {
throw err;
}
logger.debug('Pull complete:', output);
});
});
logger.success(`Image pulled successfully: ${fullImage}`);
} catch (error) {
logger.error(`Failed to pull image ${image}: ${error.message}`);
throw error;
}
}
/**
* Create and start a container
*/
async createContainer(service: IService): Promise<string> {
try {
logger.info(`Creating container for service: ${service.name}`);
const fullImage = service.registry
? `${service.registry}/${service.image}`
: service.image;
// Prepare environment variables
const env: string[] = [];
for (const [key, value] of Object.entries(service.envVars)) {
env.push(`${key}=${value}`);
}
// Create container
const container = await this.dockerClient!.createContainer({
Image: fullImage,
name: `onebox-${service.name}`,
Env: env,
Labels: {
'managed-by': 'onebox',
'onebox-service': service.name,
},
ExposedPorts: {
[`${service.port}/tcp`]: {},
},
HostConfig: {
NetworkMode: this.networkName,
RestartPolicy: {
Name: 'unless-stopped',
},
PortBindings: {
// Don't bind to host ports - nginx will proxy
[`${service.port}/tcp`]: [],
},
},
});
const containerID = container.id;
logger.success(`Container created: ${containerID}`);
return containerID;
} catch (error) {
logger.error(`Failed to create container for ${service.name}: ${error.message}`);
throw error;
}
}
/**
* Start a container by ID
*/
async startContainer(containerID: string): Promise<void> {
try {
logger.info(`Starting container: ${containerID}`);
const container = this.dockerClient!.getContainer(containerID);
await container.start();
logger.success(`Container started: ${containerID}`);
} catch (error) {
// Ignore "already started" errors
if (error.message.includes('already started')) {
logger.debug(`Container already running: ${containerID}`);
return;
}
logger.error(`Failed to start container ${containerID}: ${error.message}`);
throw error;
}
}
/**
* Stop a container by ID
*/
async stopContainer(containerID: string): Promise<void> {
try {
logger.info(`Stopping container: ${containerID}`);
const container = this.dockerClient!.getContainer(containerID);
await container.stop();
logger.success(`Container stopped: ${containerID}`);
} catch (error) {
// Ignore "already stopped" errors
if (error.message.includes('already stopped') || error.statusCode === 304) {
logger.debug(`Container already stopped: ${containerID}`);
return;
}
logger.error(`Failed to stop container ${containerID}: ${error.message}`);
throw error;
}
}
/**
* Restart a container by ID
*/
async restartContainer(containerID: string): Promise<void> {
try {
logger.info(`Restarting container: ${containerID}`);
const container = this.dockerClient!.getContainer(containerID);
await container.restart();
logger.success(`Container restarted: ${containerID}`);
} catch (error) {
logger.error(`Failed to restart container ${containerID}: ${error.message}`);
throw error;
}
}
/**
* Remove a container by ID
*/
async removeContainer(containerID: string, force = false): Promise<void> {
try {
logger.info(`Removing container: ${containerID}`);
const container = this.dockerClient!.getContainer(containerID);
// Stop first if not forced
if (!force) {
try {
await this.stopContainer(containerID);
} catch (error) {
// Ignore stop errors
logger.debug(`Error stopping container before removal: ${error.message}`);
}
}
await container.remove({ force });
logger.success(`Container removed: ${containerID}`);
} catch (error) {
logger.error(`Failed to remove container ${containerID}: ${error.message}`);
throw error;
}
}
/**
* Get container status
*/
async getContainerStatus(containerID: string): Promise<string> {
try {
const container = this.dockerClient!.getContainer(containerID);
const info = await container.inspect();
return info.State.Status;
} catch (error) {
logger.error(`Failed to get container status ${containerID}: ${error.message}`);
return 'unknown';
}
}
/**
* Get container stats (CPU, memory, network)
*/
async getContainerStats(containerID: string): Promise<IContainerStats | null> {
try {
const container = this.dockerClient!.getContainer(containerID);
const stats = await container.stats({ stream: false });
// Calculate CPU percentage
const cpuDelta =
stats.cpu_stats.cpu_usage.total_usage - stats.precpu_stats.cpu_usage.total_usage;
const systemDelta = stats.cpu_stats.system_cpu_usage - stats.precpu_stats.system_cpu_usage;
const cpuPercent =
systemDelta > 0 ? (cpuDelta / systemDelta) * stats.cpu_stats.online_cpus * 100 : 0;
// Memory stats
const memoryUsed = stats.memory_stats.usage || 0;
const memoryLimit = stats.memory_stats.limit || 0;
const memoryPercent = memoryLimit > 0 ? (memoryUsed / memoryLimit) * 100 : 0;
// Network stats
let networkRx = 0;
let networkTx = 0;
if (stats.networks) {
for (const network of Object.values(stats.networks)) {
networkRx += (network as any).rx_bytes || 0;
networkTx += (network as any).tx_bytes || 0;
}
}
return {
cpuPercent,
memoryUsed,
memoryLimit,
memoryPercent,
networkRx,
networkTx,
};
} catch (error) {
logger.error(`Failed to get container stats ${containerID}: ${error.message}`);
return null;
}
}
/**
* Get container logs
*/
async getContainerLogs(
containerID: string,
tail = 100
): Promise<{ stdout: string; stderr: string }> {
try {
const container = this.dockerClient!.getContainer(containerID);
const logs = await container.logs({
stdout: true,
stderr: true,
tail,
timestamps: true,
});
// Parse logs (Docker returns them in a special format)
const stdout: string[] = [];
const stderr: string[] = [];
const lines = logs.toString().split('\n');
for (const line of lines) {
if (line.length === 0) continue;
// Docker log format: first byte indicates stream (1=stdout, 2=stderr)
const streamType = line.charCodeAt(0);
const content = line.slice(8); // Skip header (8 bytes)
if (streamType === 1) {
stdout.push(content);
} else if (streamType === 2) {
stderr.push(content);
}
}
return {
stdout: stdout.join('\n'),
stderr: stderr.join('\n'),
};
} catch (error) {
logger.error(`Failed to get container logs ${containerID}: ${error.message}`);
return { stdout: '', stderr: '' };
}
}
/**
* Stream container logs (real-time)
*/
async streamContainerLogs(
containerID: string,
callback: (line: string, isError: boolean) => void
): Promise<void> {
try {
const container = this.dockerClient!.getContainer(containerID);
const stream = await container.logs({
stdout: true,
stderr: true,
follow: true,
tail: 0,
timestamps: true,
});
stream.on('data', (chunk: Buffer) => {
const streamType = chunk[0];
const content = chunk.slice(8).toString();
callback(content, streamType === 2);
});
stream.on('error', (error: Error) => {
logger.error(`Log stream error for ${containerID}: ${error.message}`);
});
} catch (error) {
logger.error(`Failed to stream container logs ${containerID}: ${error.message}`);
throw error;
}
}
/**
* List all onebox-managed containers
*/
async listContainers(): Promise<any[]> {
try {
const containers = await this.dockerClient!.listContainers({
all: true,
filters: {
label: ['managed-by=onebox'],
},
});
return containers;
} catch (error) {
logger.error(`Failed to list containers: ${error.message}`);
return [];
}
}
/**
* Check if Docker is running
*/
async isDockerRunning(): Promise<boolean> {
try {
await this.dockerClient!.ping();
return true;
} catch (error) {
return false;
}
}
/**
* Get Docker version info
*/
async getDockerVersion(): Promise<any> {
try {
return await this.dockerClient!.version();
} catch (error) {
logger.error(`Failed to get Docker version: ${error.message}`);
return null;
}
}
/**
* Prune unused images
*/
async pruneImages(): Promise<void> {
try {
logger.info('Pruning unused Docker images...');
await this.dockerClient!.pruneImages();
logger.success('Unused images pruned successfully');
} catch (error) {
logger.error(`Failed to prune images: ${error.message}`);
throw error;
}
}
/**
* Get container IP address in onebox network
*/
async getContainerIP(containerID: string): Promise<string | null> {
try {
const container = this.dockerClient!.getContainer(containerID);
const info = await container.inspect();
const networks = info.NetworkSettings.Networks;
if (networks && networks[this.networkName]) {
return networks[this.networkName].IPAddress;
}
return null;
} catch (error) {
logger.error(`Failed to get container IP ${containerID}: ${error.message}`);
return null;
}
}
/**
* Execute a command in a running container
*/
async execInContainer(
containerID: string,
cmd: string[]
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
try {
const container = this.dockerClient!.getContainer(containerID);
const exec = await container.exec({
Cmd: cmd,
AttachStdout: true,
AttachStderr: true,
});
const stream = await exec.start({ Detach: false });
let stdout = '';
let stderr = '';
stream.on('data', (chunk: Buffer) => {
const streamType = chunk[0];
const content = chunk.slice(8).toString();
if (streamType === 1) {
stdout += content;
} else if (streamType === 2) {
stderr += content;
}
});
// Wait for completion
await new Promise((resolve) => stream.on('end', resolve));
const inspect = await exec.inspect();
const exitCode = inspect.ExitCode || 0;
return { stdout, stderr, exitCode };
} catch (error) {
logger.error(`Failed to exec in container ${containerID}: ${error.message}`);
throw error;
}
}
}

View File

@@ -0,0 +1,193 @@
/**
* HTTP Server for Onebox
*
* Serves REST API and Angular UI
*/
import * as plugins from './onebox.plugins.ts';
import { logger } from './onebox.logging.ts';
import type { Onebox } from './onebox.classes.onebox.ts';
import type { IApiResponse } from './onebox.types.ts';
export class OneboxHttpServer {
private oneboxRef: Onebox;
private server: Deno.HttpServer | null = null;
private port = 3000;
constructor(oneboxRef: Onebox) {
this.oneboxRef = oneboxRef;
}
/**
* Start HTTP server
*/
async start(port?: number): Promise<void> {
try {
if (this.server) {
logger.warn('HTTP server already running');
return;
}
this.port = port || 3000;
logger.info(`Starting HTTP server on port ${this.port}...`);
this.server = Deno.serve({ port: this.port }, (req) => this.handleRequest(req));
logger.success(`HTTP server started on http://localhost:${this.port}`);
} catch (error) {
logger.error(`Failed to start HTTP server: ${error.message}`);
throw error;
}
}
/**
* Stop HTTP server
*/
async stop(): Promise<void> {
try {
if (!this.server) {
return;
}
logger.info('Stopping HTTP server...');
await this.server.shutdown();
this.server = null;
logger.success('HTTP server stopped');
} catch (error) {
logger.error(`Failed to stop HTTP server: ${error.message}`);
throw error;
}
}
/**
* Handle HTTP request
*/
private async handleRequest(req: Request): Promise<Response> {
const url = new URL(req.url);
const path = url.pathname;
logger.debug(`${req.method} ${path}`);
try {
// API routes
if (path.startsWith('/api/')) {
return await this.handleApiRequest(req, path);
}
// Serve Angular UI (TODO: implement static file serving)
return new Response('Onebox API - UI coming soon', {
headers: { 'Content-Type': 'text/plain' },
});
} catch (error) {
logger.error(`Request error: ${error.message}`);
return this.jsonResponse({ success: false, error: error.message }, 500);
}
}
/**
* Handle API requests
*/
private async handleApiRequest(req: Request, path: string): Promise<Response> {
const method = req.method;
// Auth check (simplified - should use proper JWT middleware)
// Skip auth for login endpoint
if (path !== '/api/auth/login') {
const authHeader = req.headers.get('Authorization');
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return this.jsonResponse({ success: false, error: 'Unauthorized' }, 401);
}
}
// Route to appropriate handler
if (path === '/api/status' && method === 'GET') {
return await this.handleStatusRequest();
} else if (path === '/api/services' && method === 'GET') {
return await this.handleListServicesRequest();
} else if (path === '/api/services' && method === 'POST') {
return await this.handleDeployServiceRequest(req);
} else if (path.match(/^\/api\/services\/[^/]+$/) && method === 'GET') {
const name = path.split('/').pop()!;
return await this.handleGetServiceRequest(name);
} else if (path.match(/^\/api\/services\/[^/]+$/) && method === 'DELETE') {
const name = path.split('/').pop()!;
return await this.handleDeleteServiceRequest(name);
} else if (path.match(/^\/api\/services\/[^/]+\/start$/) && method === 'POST') {
const name = path.split('/')[3];
return await this.handleStartServiceRequest(name);
} else if (path.match(/^\/api\/services\/[^/]+\/stop$/) && method === 'POST') {
const name = path.split('/')[3];
return await this.handleStopServiceRequest(name);
} else if (path.match(/^\/api\/services\/[^/]+\/restart$/) && method === 'POST') {
const name = path.split('/')[3];
return await this.handleRestartServiceRequest(name);
} else if (path.match(/^\/api\/services\/[^/]+\/logs$/) && method === 'GET') {
const name = path.split('/')[3];
return await this.handleGetLogsRequest(name);
} else {
return this.jsonResponse({ success: false, error: 'Not found' }, 404);
}
}
// API Handlers
private async handleStatusRequest(): Promise<Response> {
const status = await this.oneboxRef.getSystemStatus();
return this.jsonResponse({ success: true, data: status });
}
private async handleListServicesRequest(): Promise<Response> {
const services = this.oneboxRef.services.listServices();
return this.jsonResponse({ success: true, data: services });
}
private async handleDeployServiceRequest(req: Request): Promise<Response> {
const body = await req.json();
const service = await this.oneboxRef.services.deployService(body);
return this.jsonResponse({ success: true, data: service });
}
private async handleGetServiceRequest(name: string): Promise<Response> {
const service = this.oneboxRef.services.getService(name);
if (!service) {
return this.jsonResponse({ success: false, error: 'Service not found' }, 404);
}
return this.jsonResponse({ success: true, data: service });
}
private async handleDeleteServiceRequest(name: string): Promise<Response> {
await this.oneboxRef.services.removeService(name);
return this.jsonResponse({ success: true, message: 'Service removed' });
}
private async handleStartServiceRequest(name: string): Promise<Response> {
await this.oneboxRef.services.startService(name);
return this.jsonResponse({ success: true, message: 'Service started' });
}
private async handleStopServiceRequest(name: string): Promise<Response> {
await this.oneboxRef.services.stopService(name);
return this.jsonResponse({ success: true, message: 'Service stopped' });
}
private async handleRestartServiceRequest(name: string): Promise<Response> {
await this.oneboxRef.services.restartService(name);
return this.jsonResponse({ success: true, message: 'Service restarted' });
}
private async handleGetLogsRequest(name: string): Promise<Response> {
const logs = await this.oneboxRef.services.getServiceLogs(name);
return this.jsonResponse({ success: true, data: logs });
}
/**
* Helper to create JSON response
*/
private jsonResponse(data: IApiResponse, status = 200): Response {
return new Response(JSON.stringify(data), {
status,
headers: { 'Content-Type': 'application/json' },
});
}
}

345
ts/onebox.classes.nginx.ts Normal file
View File

@@ -0,0 +1,345 @@
/**
* Nginx Manager for Onebox
*
* Manages Nginx reverse proxy configurations for services
*/
import * as plugins from './onebox.plugins.ts';
import { logger } from './onebox.logging.ts';
import { OneboxDatabase } from './onebox.classes.database.ts';
export class OneboxNginxManager {
private oneboxRef: any;
private database: OneboxDatabase;
private configDir = '/etc/nginx/sites-available';
private enabledDir = '/etc/nginx/sites-enabled';
constructor(oneboxRef: any) {
this.oneboxRef = oneboxRef;
this.database = oneboxRef.database;
// Allow custom nginx config directory
const customDir = this.database.getSetting('nginxConfigDir');
if (customDir) {
this.configDir = customDir;
}
}
/**
* Initialize nginx manager
*/
async init(): Promise<void> {
try {
// Ensure directories exist
await Deno.mkdir(this.configDir, { recursive: true });
await Deno.mkdir(this.enabledDir, { recursive: true });
logger.info('Nginx manager initialized');
} catch (error) {
logger.error(`Failed to initialize Nginx manager: ${error.message}`);
throw error;
}
}
/**
* Create nginx config for a service
*/
async createConfig(serviceId: number, domain: string, port: number): Promise<void> {
try {
logger.info(`Creating Nginx config for ${domain}`);
const service = this.database.getServiceByID(serviceId);
if (!service) {
throw new Error(`Service not found: ${serviceId}`);
}
// Get container IP (or use container name for DNS resolution within Docker network)
const containerName = `onebox-${service.name}`;
// Generate config
const config = this.generateConfig(domain, containerName, port);
// Write config file
const configPath = `${this.configDir}/onebox-${service.name}.conf`;
await Deno.writeTextFile(configPath, config);
// Create symlink in sites-enabled
const enabledPath = `${this.enabledDir}/onebox-${service.name}.conf`;
try {
await Deno.remove(enabledPath);
} catch {
// Ignore if doesn't exist
}
await Deno.symlink(configPath, enabledPath);
logger.success(`Nginx config created: ${domain}`);
} catch (error) {
logger.error(`Failed to create Nginx config: ${error.message}`);
throw error;
}
}
/**
* Generate nginx configuration
*/
private generateConfig(domain: string, upstream: string, port: number): string {
return `# Onebox-managed configuration for ${domain}
# Generated at ${new Date().toISOString()}
upstream onebox_${domain.replace(/\./g, '_')} {
server ${upstream}:${port};
}
# HTTP server (redirect to HTTPS or serve directly)
server {
listen 80;
listen [::]:80;
server_name ${domain};
# ACME challenge for Let's Encrypt
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
# Redirect to HTTPS (will be enabled after SSL is configured)
# location / {
# return 301 https://$server_name$request_uri;
# }
# Proxy to container (remove after SSL is configured)
location / {
proxy_pass http://onebox_${domain.replace(/\./g, '_')};
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket support
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
}
# HTTPS server (uncomment after SSL is configured)
# server {
# listen 443 ssl http2;
# listen [::]:443 ssl http2;
# server_name ${domain};
#
# ssl_certificate /etc/letsencrypt/live/${domain}/fullchain.pem;
# ssl_certificate_key /etc/letsencrypt/live/${domain}/privkey.pem;
#
# # SSL configuration
# ssl_protocols TLSv1.2 TLSv1.3;
# ssl_ciphers HIGH:!aNULL:!MD5;
# ssl_prefer_server_ciphers on;
#
# location / {
# proxy_pass http://onebox_${domain.replace(/\./g, '_')};
# proxy_set_header Host $host;
# proxy_set_header X-Real-IP $remote_addr;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# proxy_set_header X-Forwarded-Proto $scheme;
#
# # WebSocket support
# proxy_http_version 1.1;
# proxy_set_header Upgrade $http_upgrade;
# proxy_set_header Connection "upgrade";
#
# # Timeouts
# proxy_connect_timeout 60s;
# proxy_send_timeout 60s;
# proxy_read_timeout 60s;
# }
# }
`;
}
/**
* Update nginx config to enable SSL
*/
async enableSSL(domain: string): Promise<void> {
try {
logger.info(`Enabling SSL for ${domain}`);
// Find service by domain
const services = this.database.getAllServices();
const service = services.find((s) => s.domain === domain);
if (!service) {
throw new Error(`Service not found for domain: ${domain}`);
}
const configPath = `${this.configDir}/onebox-${service.name}.conf`;
let config = await Deno.readTextFile(configPath);
// Enable HTTPS redirect and HTTPS server block
config = config.replace('# location / {\n # return 301', 'location / {\n return 301');
config = config.replace('# }', '}');
config = config.replace(/# server \{[\s\S]*?# \}/m, (match) =>
match.replace(/# /g, '')
);
// Comment out HTTP proxy location
config = config.replace(
/# Proxy to container \(remove after SSL is configured\)[\s\S]*?location \/ \{[\s\S]*?\n \}/,
(match) => `# ${match.replace(/\n/g, '\n # ')}`
);
await Deno.writeTextFile(configPath, config);
logger.success(`SSL enabled for ${domain}`);
} catch (error) {
logger.error(`Failed to enable SSL for ${domain}: ${error.message}`);
throw error;
}
}
/**
* Remove nginx config for a service
*/
async removeConfig(serviceId: number): Promise<void> {
try {
const service = this.database.getServiceByID(serviceId);
if (!service) {
throw new Error(`Service not found: ${serviceId}`);
}
logger.info(`Removing Nginx config for ${service.name}`);
// Remove symlink
const enabledPath = `${this.enabledDir}/onebox-${service.name}.conf`;
try {
await Deno.remove(enabledPath);
} catch {
// Ignore if doesn't exist
}
// Remove config file
const configPath = `${this.configDir}/onebox-${service.name}.conf`;
try {
await Deno.remove(configPath);
} catch {
// Ignore if doesn't exist
}
logger.success(`Nginx config removed for ${service.name}`);
} catch (error) {
logger.error(`Failed to remove Nginx config: ${error.message}`);
throw error;
}
}
/**
* Test nginx configuration
*/
async test(): Promise<boolean> {
try {
const command = new Deno.Command('nginx', {
args: ['-t'],
stdout: 'piped',
stderr: 'piped',
});
const { code, stderr } = await command.output();
if (code !== 0) {
const errorMsg = new TextDecoder().decode(stderr);
logger.error(`Nginx config test failed: ${errorMsg}`);
return false;
}
logger.success('Nginx configuration is valid');
return true;
} catch (error) {
logger.error(`Failed to test Nginx config: ${error.message}`);
return false;
}
}
/**
* Reload nginx
*/
async reload(): Promise<void> {
try {
// Test config first
const isValid = await this.test();
if (!isValid) {
throw new Error('Nginx configuration is invalid');
}
logger.info('Reloading Nginx...');
const command = new Deno.Command('systemctl', {
args: ['reload', 'nginx'],
stdout: 'piped',
stderr: 'piped',
});
const { code, stderr } = await command.output();
if (code !== 0) {
const errorMsg = new TextDecoder().decode(stderr);
throw new Error(`Nginx reload failed: ${errorMsg}`);
}
logger.success('Nginx reloaded successfully');
} catch (error) {
logger.error(`Failed to reload Nginx: ${error.message}`);
throw error;
}
}
/**
* Get nginx status
*/
async getStatus(): Promise<string> {
try {
const command = new Deno.Command('systemctl', {
args: ['status', 'nginx'],
stdout: 'piped',
stderr: 'piped',
});
const { code, stdout } = await command.output();
const output = new TextDecoder().decode(stdout);
if (code === 0 || output.includes('active (running)')) {
return 'running';
} else if (output.includes('inactive') || output.includes('dead')) {
return 'stopped';
} else if (output.includes('failed')) {
return 'failed';
} else {
return 'unknown';
}
} catch (error) {
logger.error(`Failed to get Nginx status: ${error.message}`);
return 'unknown';
}
}
/**
* Check if nginx is installed
*/
async isInstalled(): Promise<boolean> {
try {
const command = new Deno.Command('which', {
args: ['nginx'],
stdout: 'piped',
stderr: 'piped',
});
const { code } = await command.output();
return code === 0;
} catch {
return false;
}
}
}

220
ts/onebox.classes.onebox.ts Normal file
View File

@@ -0,0 +1,220 @@
/**
* Main Onebox coordinator class
*
* Coordinates all components and provides the main API
*/
import { logger } from './onebox.logging.ts';
import { OneboxDatabase } from './onebox.classes.database.ts';
import { OneboxDockerManager } from './onebox.classes.docker.ts';
import { OneboxServicesManager } from './onebox.classes.services.ts';
import { OneboxRegistriesManager } from './onebox.classes.registries.ts';
import { OneboxNginxManager } from './onebox.classes.nginx.ts';
import { OneboxDnsManager } from './onebox.classes.dns.ts';
import { OneboxSslManager } from './onebox.classes.ssl.ts';
import { OneboxDaemon } from './onebox.classes.daemon.ts';
import { OneboxHttpServer } from './onebox.classes.httpserver.ts';
export class Onebox {
public database: OneboxDatabase;
public docker: OneboxDockerManager;
public services: OneboxServicesManager;
public registries: OneboxRegistriesManager;
public nginx: OneboxNginxManager;
public dns: OneboxDnsManager;
public ssl: OneboxSslManager;
public daemon: OneboxDaemon;
public httpServer: OneboxHttpServer;
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.nginx = new OneboxNginxManager(this);
this.dns = new OneboxDnsManager(this);
this.ssl = new OneboxSslManager(this);
this.daemon = new OneboxDaemon(this);
this.httpServer = new OneboxHttpServer(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();
// Initialize Nginx
await this.nginx.init();
// 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');
}
// Login to all registries
await this.registries.loginToAllRegistries();
this.initialized = true;
logger.success('Onebox initialized successfully');
} catch (error) {
logger.error(`Failed to initialize Onebox: ${error.message}`);
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...');
// Hash default password 'admin'
const passwordHash = await Deno.readTextFile('/dev/urandom').then((data) =>
// Simple hash for now - should use bcrypt
btoa('admin')
);
await this.database.createUser({
username: 'admin',
passwordHash: btoa('admin'), // Simple encoding for now
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: ${error.message}`);
}
}
/**
* Check if Onebox is initialized
*/
isInitialized(): boolean {
return this.initialized;
}
/**
* Get system status
*/
async getSystemStatus() {
try {
const dockerRunning = await this.docker.isDockerRunning();
const nginxStatus = await this.nginx.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;
return {
docker: {
running: dockerRunning,
version: dockerRunning ? await this.docker.getDockerVersion() : null,
},
nginx: {
status: nginxStatus,
installed: await this.nginx.isInstalled(),
},
dns: {
configured: dnsConfigured,
},
ssl: {
configured: sslConfigured,
certbotInstalled: await this.ssl.isCertbotInstalled(),
},
services: {
total: totalServices,
running: runningServices,
stopped: totalServices - runningServices,
},
};
} catch (error) {
logger.error(`Failed to get system status: ${error.message}`);
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();
// Close database
this.database.close();
logger.success('Onebox shutdown complete');
} catch (error) {
logger.error(`Error during shutdown: ${error.message}`);
}
}
}

View File

@@ -0,0 +1,195 @@
/**
* Registry Manager for Onebox
*
* Manages Docker registry credentials and authentication
*/
import * as plugins from './onebox.plugins.ts';
import type { IRegistry } from './onebox.types.ts';
import { logger } from './onebox.logging.ts';
import { OneboxDatabase } from './onebox.classes.database.ts';
export class OneboxRegistriesManager {
private oneboxRef: any; // Will be Onebox instance
private database: OneboxDatabase;
constructor(oneboxRef: any) {
this.oneboxRef = oneboxRef;
this.database = oneboxRef.database;
}
/**
* Encrypt a password (simple base64 for now, should use proper encryption)
*/
private encryptPassword(password: string): string {
// TODO: Use proper encryption with a secret key
// For now, using base64 encoding (NOT SECURE, just for structure)
return plugins.encoding.encodeBase64(new TextEncoder().encode(password));
}
/**
* Decrypt a password
*/
private decryptPassword(encrypted: string): string {
// TODO: Use proper decryption
return new TextDecoder().decode(plugins.encoding.decodeBase64(encrypted));
}
/**
* Add a registry
*/
async addRegistry(url: string, username: string, password: string): Promise<IRegistry> {
try {
// Check if registry already exists
const existing = this.database.getRegistryByURL(url);
if (existing) {
throw new Error(`Registry already exists: ${url}`);
}
// Encrypt password
const passwordEncrypted = this.encryptPassword(password);
// Create registry in database
const registry = await this.database.createRegistry({
url,
username,
passwordEncrypted,
createdAt: Date.now(),
});
logger.success(`Registry added: ${url}`);
// Perform Docker login
await this.loginToRegistry(registry);
return registry;
} catch (error) {
logger.error(`Failed to add registry ${url}: ${error.message}`);
throw error;
}
}
/**
* Remove a registry
*/
async removeRegistry(url: string): Promise<void> {
try {
const registry = this.database.getRegistryByURL(url);
if (!registry) {
throw new Error(`Registry not found: ${url}`);
}
this.database.deleteRegistry(url);
logger.success(`Registry removed: ${url}`);
// Note: We don't perform docker logout as it might affect other users
} catch (error) {
logger.error(`Failed to remove registry ${url}: ${error.message}`);
throw error;
}
}
/**
* List all registries
*/
listRegistries(): IRegistry[] {
return this.database.getAllRegistries();
}
/**
* Get registry by URL
*/
getRegistry(url: string): IRegistry | null {
return this.database.getRegistryByURL(url);
}
/**
* Perform Docker login for a registry
*/
async loginToRegistry(registry: IRegistry): Promise<void> {
try {
logger.info(`Logging into registry: ${registry.url}`);
const password = this.decryptPassword(registry.passwordEncrypted);
// Use docker login command
const command = [
'docker',
'login',
registry.url,
'--username',
registry.username,
'--password-stdin',
];
const process = new Deno.Command('docker', {
args: ['login', registry.url, '--username', registry.username, '--password-stdin'],
stdin: 'piped',
stdout: 'piped',
stderr: 'piped',
});
const child = process.spawn();
// Write password to stdin
const writer = child.stdin.getWriter();
await writer.write(new TextEncoder().encode(password));
await writer.close();
const { code, stdout, stderr } = await child.output();
if (code !== 0) {
const errorMsg = new TextDecoder().decode(stderr);
throw new Error(`Docker login failed: ${errorMsg}`);
}
logger.success(`Logged into registry: ${registry.url}`);
} catch (error) {
logger.error(`Failed to login to registry ${registry.url}: ${error.message}`);
throw error;
}
}
/**
* Login to all registries (useful on daemon start)
*/
async loginToAllRegistries(): Promise<void> {
const registries = this.listRegistries();
for (const registry of registries) {
try {
await this.loginToRegistry(registry);
} catch (error) {
logger.warn(`Failed to login to ${registry.url}: ${error.message}`);
// Continue with other registries
}
}
}
/**
* Test registry connection
*/
async testRegistry(url: string, username: string, password: string): Promise<boolean> {
try {
const command = new Deno.Command('docker', {
args: ['login', url, '--username', username, '--password-stdin'],
stdin: 'piped',
stdout: 'piped',
stderr: 'piped',
});
const child = command.spawn();
const writer = child.stdin.getWriter();
await writer.write(new TextEncoder().encode(password));
await writer.close();
const { code } = await child.output();
return code === 0;
} catch (error) {
logger.error(`Failed to test registry ${url}: ${error.message}`);
return false;
}
}
}

View File

@@ -0,0 +1,407 @@
/**
* Services Manager for Onebox
*
* Orchestrates service deployment: Docker + Nginx + DNS + SSL
*/
import type { IService, IServiceDeployOptions } from './onebox.types.ts';
import { logger } from './onebox.logging.ts';
import { OneboxDatabase } from './onebox.classes.database.ts';
import { OneboxDockerManager } from './onebox.classes.docker.ts';
export class OneboxServicesManager {
private oneboxRef: any; // Will be Onebox instance
private database: OneboxDatabase;
private docker: OneboxDockerManager;
constructor(oneboxRef: any) {
this.oneboxRef = oneboxRef;
this.database = oneboxRef.database;
this.docker = oneboxRef.docker;
}
/**
* Deploy a new service (full workflow)
*/
async deployService(options: IServiceDeployOptions): Promise<IService> {
try {
logger.info(`Deploying service: ${options.name}`);
// Check if service already exists
const existing = this.database.getServiceByName(options.name);
if (existing) {
throw new Error(`Service already exists: ${options.name}`);
}
// Create service record in database
const service = await this.database.createService({
name: options.name,
image: options.image,
registry: options.registry,
envVars: options.envVars || {},
port: options.port,
domain: options.domain,
status: 'stopped',
createdAt: Date.now(),
updatedAt: Date.now(),
});
// Pull image
await this.docker.pullImage(options.image, options.registry);
// Create container
const containerID = await this.docker.createContainer(service);
// Update service with container ID
this.database.updateService(service.id!, {
containerID,
status: 'starting',
});
// Start container
await this.docker.startContainer(containerID);
// Update status
this.database.updateService(service.id!, { status: 'running' });
// If domain is specified, configure nginx, DNS, and SSL
if (options.domain) {
logger.info(`Configuring domain: ${options.domain}`);
// Configure DNS (if autoDNS is enabled)
if (options.autoDNS !== false) {
try {
await this.oneboxRef.dns.addDNSRecord(options.domain);
} catch (error) {
logger.warn(`Failed to configure DNS for ${options.domain}: ${error.message}`);
}
}
// Configure nginx
try {
await this.oneboxRef.nginx.createConfig(service.id!, options.domain, options.port);
await this.oneboxRef.nginx.reload();
} catch (error) {
logger.warn(`Failed to configure Nginx for ${options.domain}: ${error.message}`);
}
// Configure SSL (if autoSSL is enabled)
if (options.autoSSL !== false) {
try {
await this.oneboxRef.ssl.obtainCertificate(options.domain);
await this.oneboxRef.nginx.reload();
} catch (error) {
logger.warn(`Failed to obtain SSL certificate for ${options.domain}: ${error.message}`);
}
}
}
logger.success(`Service deployed successfully: ${options.name}`);
return this.database.getServiceByName(options.name)!;
} catch (error) {
logger.error(`Failed to deploy service ${options.name}: ${error.message}`);
throw error;
}
}
/**
* Start a service
*/
async startService(name: string): Promise<void> {
try {
const service = this.database.getServiceByName(name);
if (!service) {
throw new Error(`Service not found: ${name}`);
}
if (!service.containerID) {
throw new Error(`Service ${name} has no container ID`);
}
logger.info(`Starting service: ${name}`);
this.database.updateService(service.id!, { status: 'starting' });
await this.docker.startContainer(service.containerID);
this.database.updateService(service.id!, { status: 'running' });
logger.success(`Service started: ${name}`);
} catch (error) {
logger.error(`Failed to start service ${name}: ${error.message}`);
this.database.updateService(
this.database.getServiceByName(name)?.id!,
{ status: 'failed' }
);
throw error;
}
}
/**
* Stop a service
*/
async stopService(name: string): Promise<void> {
try {
const service = this.database.getServiceByName(name);
if (!service) {
throw new Error(`Service not found: ${name}`);
}
if (!service.containerID) {
throw new Error(`Service ${name} has no container ID`);
}
logger.info(`Stopping service: ${name}`);
this.database.updateService(service.id!, { status: 'stopping' });
await this.docker.stopContainer(service.containerID);
this.database.updateService(service.id!, { status: 'stopped' });
logger.success(`Service stopped: ${name}`);
} catch (error) {
logger.error(`Failed to stop service ${name}: ${error.message}`);
throw error;
}
}
/**
* Restart a service
*/
async restartService(name: string): Promise<void> {
try {
const service = this.database.getServiceByName(name);
if (!service) {
throw new Error(`Service not found: ${name}`);
}
if (!service.containerID) {
throw new Error(`Service ${name} has no container ID`);
}
logger.info(`Restarting service: ${name}`);
await this.docker.restartContainer(service.containerID);
this.database.updateService(service.id!, { status: 'running' });
logger.success(`Service restarted: ${name}`);
} catch (error) {
logger.error(`Failed to restart service ${name}: ${error.message}`);
throw error;
}
}
/**
* Remove a service (full cleanup)
*/
async removeService(name: string): Promise<void> {
try {
const service = this.database.getServiceByName(name);
if (!service) {
throw new Error(`Service not found: ${name}`);
}
logger.info(`Removing service: ${name}`);
// Stop and remove container
if (service.containerID) {
try {
await this.docker.removeContainer(service.containerID, true);
} catch (error) {
logger.warn(`Failed to remove container: ${error.message}`);
}
}
// Remove nginx config
if (service.domain) {
try {
await this.oneboxRef.nginx.removeConfig(service.id!);
await this.oneboxRef.nginx.reload();
} catch (error) {
logger.warn(`Failed to remove Nginx config: ${error.message}`);
}
// Note: We don't remove DNS records or SSL certs automatically
// as they might be used by other services or need manual cleanup
}
// Remove from database
this.database.deleteService(service.id!);
logger.success(`Service removed: ${name}`);
} catch (error) {
logger.error(`Failed to remove service ${name}: ${error.message}`);
throw error;
}
}
/**
* List all services
*/
listServices(): IService[] {
return this.database.getAllServices();
}
/**
* Get service by name
*/
getService(name: string): IService | null {
return this.database.getServiceByName(name);
}
/**
* Get service logs
*/
async getServiceLogs(name: string, tail = 100): Promise<string> {
try {
const service = this.database.getServiceByName(name);
if (!service) {
throw new Error(`Service not found: ${name}`);
}
if (!service.containerID) {
throw new Error(`Service ${name} has no container ID`);
}
const logs = await this.docker.getContainerLogs(service.containerID, tail);
return `=== STDOUT ===\n${logs.stdout}\n\n=== STDERR ===\n${logs.stderr}`;
} catch (error) {
logger.error(`Failed to get logs for service ${name}: ${error.message}`);
throw error;
}
}
/**
* Stream service logs (real-time)
*/
async streamServiceLogs(
name: string,
callback: (line: string, isError: boolean) => void
): Promise<void> {
try {
const service = this.database.getServiceByName(name);
if (!service) {
throw new Error(`Service not found: ${name}`);
}
if (!service.containerID) {
throw new Error(`Service ${name} has no container ID`);
}
await this.docker.streamContainerLogs(service.containerID, callback);
} catch (error) {
logger.error(`Failed to stream logs for service ${name}: ${error.message}`);
throw error;
}
}
/**
* Get service metrics
*/
async getServiceMetrics(name: string) {
try {
const service = this.database.getServiceByName(name);
if (!service) {
throw new Error(`Service not found: ${name}`);
}
if (!service.containerID) {
throw new Error(`Service ${name} has no container ID`);
}
const stats = await this.docker.getContainerStats(service.containerID);
return stats;
} catch (error) {
logger.error(`Failed to get metrics for service ${name}: ${error.message}`);
return null;
}
}
/**
* Get service status
*/
async getServiceStatus(name: string): Promise<string> {
try {
const service = this.database.getServiceByName(name);
if (!service) {
return 'not-found';
}
if (!service.containerID) {
return service.status;
}
const status = await this.docker.getContainerStatus(service.containerID);
return status;
} catch (error) {
logger.error(`Failed to get status for service ${name}: ${error.message}`);
return 'unknown';
}
}
/**
* Update service environment variables
*/
async updateServiceEnv(name: string, envVars: Record<string, string>): Promise<void> {
try {
const service = this.database.getServiceByName(name);
if (!service) {
throw new Error(`Service not found: ${name}`);
}
// Update database
this.database.updateService(service.id!, { envVars });
// Note: Requires container restart to take effect
logger.info(`Environment variables updated for ${name}. Restart service to apply changes.`);
} catch (error) {
logger.error(`Failed to update env vars for service ${name}: ${error.message}`);
throw error;
}
}
/**
* Sync service status from Docker
*/
async syncServiceStatus(name: string): Promise<void> {
try {
const service = this.database.getServiceByName(name);
if (!service || !service.containerID) {
return;
}
const status = await this.docker.getContainerStatus(service.containerID);
// Map Docker status to our status
let ourStatus: IService['status'] = 'stopped';
if (status === 'running') {
ourStatus = 'running';
} else if (status === 'exited' || status === 'dead') {
ourStatus = 'stopped';
} else if (status === 'created') {
ourStatus = 'stopped';
} else if (status === 'restarting') {
ourStatus = 'starting';
}
this.database.updateService(service.id!, { status: ourStatus });
} catch (error) {
logger.debug(`Failed to sync status for service ${name}: ${error.message}`);
}
}
/**
* Sync all service statuses from Docker
*/
async syncAllServiceStatuses(): Promise<void> {
const services = this.listServices();
for (const service of services) {
await this.syncServiceStatus(service.name);
}
}
}

317
ts/onebox.classes.ssl.ts Normal file
View File

@@ -0,0 +1,317 @@
/**
* SSL Manager for Onebox
*
* Manages SSL certificates via Let's Encrypt (using smartacme)
*/
import * as plugins from './onebox.plugins.ts';
import { logger } from './onebox.logging.ts';
import { OneboxDatabase } from './onebox.classes.database.ts';
export class OneboxSslManager {
private oneboxRef: any;
private database: OneboxDatabase;
private smartacme: plugins.smartacme.SmartAcme | null = null;
private acmeEmail: string | null = null;
constructor(oneboxRef: any) {
this.oneboxRef = oneboxRef;
this.database = oneboxRef.database;
}
/**
* Initialize SSL manager
*/
async init(): Promise<void> {
try {
// Get ACME email from settings
const acmeEmail = this.database.getSetting('acmeEmail');
if (!acmeEmail) {
logger.warn('ACME email not configured. SSL certificate management will be limited.');
logger.info('Configure with: onebox config set acmeEmail <email>');
return;
}
this.acmeEmail = acmeEmail;
// Initialize SmartACME
this.smartacme = new plugins.smartacme.SmartAcme({
email: acmeEmail,
environment: 'production', // or 'staging' for testing
dns: 'cloudflare', // Use Cloudflare DNS challenge
});
logger.info('SSL manager initialized with SmartACME');
} catch (error) {
logger.error(`Failed to initialize SSL manager: ${error.message}`);
throw error;
}
}
/**
* Check if SSL manager is configured
*/
isConfigured(): boolean {
return this.smartacme !== null && this.acmeEmail !== null;
}
/**
* Obtain SSL certificate for a domain
*/
async obtainCertificate(domain: string): Promise<void> {
try {
if (!this.isConfigured()) {
throw new Error('SSL manager not configured');
}
logger.info(`Obtaining SSL certificate for ${domain}...`);
// Check if certificate already exists and is valid
const existing = this.database.getSSLCertificate(domain);
if (existing && existing.expiryDate > Date.now()) {
logger.info(`Valid certificate already exists for ${domain}`);
return;
}
// Use certbot for now (smartacme integration would be more complex)
// This is a simplified version - in production, use proper ACME client
await this.obtainCertificateWithCertbot(domain);
// Store in database
const certPath = `/etc/letsencrypt/live/${domain}/cert.pem`;
const keyPath = `/etc/letsencrypt/live/${domain}/privkey.pem`;
const fullChainPath = `/etc/letsencrypt/live/${domain}/fullchain.pem`;
// Get expiry date (90 days from now for Let's Encrypt)
const expiryDate = Date.now() + 90 * 24 * 60 * 60 * 1000;
if (existing) {
this.database.updateSSLCertificate(domain, {
certPath,
keyPath,
fullChainPath,
expiryDate,
});
} else {
await this.database.createSSLCertificate({
domain,
certPath,
keyPath,
fullChainPath,
expiryDate,
issuer: 'Let\'s Encrypt',
createdAt: Date.now(),
updatedAt: Date.now(),
});
}
// Enable SSL in nginx config
await this.oneboxRef.nginx.enableSSL(domain);
logger.success(`SSL certificate obtained for ${domain}`);
} catch (error) {
logger.error(`Failed to obtain certificate for ${domain}: ${error.message}`);
throw error;
}
}
/**
* Obtain certificate using certbot
*/
private async obtainCertificateWithCertbot(domain: string): Promise<void> {
try {
logger.info(`Running certbot for ${domain}...`);
// Use webroot method (nginx serves .well-known/acme-challenge)
const command = new Deno.Command('certbot', {
args: [
'certonly',
'--webroot',
'--webroot-path=/var/www/certbot',
'--email',
this.acmeEmail!,
'--agree-tos',
'--no-eff-email',
'--domain',
domain,
'--non-interactive',
],
stdout: 'piped',
stderr: 'piped',
});
const { code, stderr } = await command.output();
if (code !== 0) {
const errorMsg = new TextDecoder().decode(stderr);
throw new Error(`Certbot failed: ${errorMsg}`);
}
logger.success(`Certbot obtained certificate for ${domain}`);
} catch (error) {
throw new Error(`Failed to run certbot: ${error.message}`);
}
}
/**
* Renew certificate for a domain
*/
async renewCertificate(domain: string): Promise<void> {
try {
logger.info(`Renewing SSL certificate for ${domain}...`);
const command = new Deno.Command('certbot', {
args: ['renew', '--cert-name', domain, '--non-interactive'],
stdout: 'piped',
stderr: 'piped',
});
const { code, stderr } = await command.output();
if (code !== 0) {
const errorMsg = new TextDecoder().decode(stderr);
throw new Error(`Certbot renewal failed: ${errorMsg}`);
}
// Update database
const expiryDate = Date.now() + 90 * 24 * 60 * 60 * 1000;
this.database.updateSSLCertificate(domain, {
expiryDate,
});
logger.success(`Certificate renewed for ${domain}`);
// Reload nginx
await this.oneboxRef.nginx.reload();
} catch (error) {
logger.error(`Failed to renew certificate for ${domain}: ${error.message}`);
throw error;
}
}
/**
* List all certificates
*/
listCertificates() {
return this.database.getAllSSLCertificates();
}
/**
* Get certificate info for a domain
*/
getCertificate(domain: string) {
return this.database.getSSLCertificate(domain);
}
/**
* Check certificates that are expiring soon and renew them
*/
async renewExpiring(): Promise<void> {
try {
logger.info('Checking for expiring certificates...');
const certificates = this.listCertificates();
const thirtyDaysFromNow = Date.now() + 30 * 24 * 60 * 60 * 1000;
for (const cert of certificates) {
if (cert.expiryDate < thirtyDaysFromNow) {
logger.info(`Certificate for ${cert.domain} expires soon, renewing...`);
try {
await this.renewCertificate(cert.domain);
} catch (error) {
logger.error(`Failed to renew ${cert.domain}: ${error.message}`);
// Continue with other certificates
}
}
}
logger.success('Certificate renewal check complete');
} catch (error) {
logger.error(`Failed to check expiring certificates: ${error.message}`);
throw error;
}
}
/**
* Force renewal of all certificates
*/
async renewAll(): Promise<void> {
try {
logger.info('Renewing all certificates...');
const command = new Deno.Command('certbot', {
args: ['renew', '--force-renewal', '--non-interactive'],
stdout: 'piped',
stderr: 'piped',
});
const { code, stderr } = await command.output();
if (code !== 0) {
const errorMsg = new TextDecoder().decode(stderr);
throw new Error(`Certbot renewal failed: ${errorMsg}`);
}
logger.success('All certificates renewed');
// Reload nginx
await this.oneboxRef.nginx.reload();
} catch (error) {
logger.error(`Failed to renew all certificates: ${error.message}`);
throw error;
}
}
/**
* Check if certbot is installed
*/
async isCertbotInstalled(): Promise<boolean> {
try {
const command = new Deno.Command('which', {
args: ['certbot'],
stdout: 'piped',
stderr: 'piped',
});
const { code } = await command.output();
return code === 0;
} catch {
return false;
}
}
/**
* Get certificate expiry date from file
*/
async getCertificateExpiry(domain: string): Promise<Date | null> {
try {
const certPath = `/etc/letsencrypt/live/${domain}/cert.pem`;
const command = new Deno.Command('openssl', {
args: ['x509', '-enddate', '-noout', '-in', certPath],
stdout: 'piped',
stderr: 'piped',
});
const { code, stdout } = await command.output();
if (code !== 0) {
return null;
}
const output = new TextDecoder().decode(stdout);
const match = output.match(/notAfter=(.+)/);
if (match) {
return new Date(match[1]);
}
return null;
} catch (error) {
logger.error(`Failed to get certificate expiry for ${domain}: ${error.message}`);
return null;
}
}
}

366
ts/onebox.cli.ts Normal file
View File

@@ -0,0 +1,366 @@
/**
* CLI Router for Onebox
*/
import { logger } from './onebox.logging.ts';
import { projectInfo } from './onebox.info.ts';
import { Onebox } from './onebox.classes.onebox.ts';
export async function runCli(): Promise<void> {
const args = Deno.args;
if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
printHelp();
return;
}
if (args.includes('--version') || args.includes('-v')) {
console.log(`${projectInfo.name} v${projectInfo.version}`);
return;
}
const command = args[0];
const subcommand = args[1];
try {
// Initialize Onebox
const onebox = new Onebox();
await onebox.init();
// Route commands
switch (command) {
case 'service':
await handleServiceCommand(onebox, subcommand, args.slice(2));
break;
case 'registry':
await handleRegistryCommand(onebox, subcommand, args.slice(2));
break;
case 'dns':
await handleDnsCommand(onebox, subcommand, args.slice(2));
break;
case 'ssl':
await handleSslCommand(onebox, subcommand, args.slice(2));
break;
case 'nginx':
await handleNginxCommand(onebox, subcommand, args.slice(2));
break;
case 'daemon':
await handleDaemonCommand(onebox, subcommand, args.slice(2));
break;
case 'config':
await handleConfigCommand(onebox, subcommand, args.slice(2));
break;
case 'status':
await handleStatusCommand(onebox);
break;
default:
logger.error(`Unknown command: ${command}`);
printHelp();
Deno.exit(1);
}
// Cleanup
await onebox.shutdown();
} catch (error) {
logger.error(error.message);
Deno.exit(1);
}
}
// Service commands
async function handleServiceCommand(onebox: Onebox, subcommand: string, args: string[]) {
switch (subcommand) {
case 'add': {
const name = args[0];
const image = getArg(args, '--image');
const domain = getArg(args, '--domain');
const port = parseInt(getArg(args, '--port') || '80', 10);
const envArgs = args.filter((a) => a.startsWith('--env=')).map((a) => a.slice(6));
const envVars: Record<string, string> = {};
for (const env of envArgs) {
const [key, value] = env.split('=');
envVars[key] = value;
}
await onebox.services.deployService({ name, image, port, domain, envVars });
break;
}
case 'remove':
await onebox.services.removeService(args[0]);
break;
case 'start':
await onebox.services.startService(args[0]);
break;
case 'stop':
await onebox.services.stopService(args[0]);
break;
case 'restart':
await onebox.services.restartService(args[0]);
break;
case 'list': {
const services = onebox.services.listServices();
logger.table(
['Name', 'Image', 'Status', 'Domain', 'Port'],
services.map((s) => [s.name, s.image, s.status, s.domain || '-', s.port.toString()])
);
break;
}
case 'logs': {
const logs = await onebox.services.getServiceLogs(args[0]);
console.log(logs);
break;
}
default:
logger.error(`Unknown service subcommand: ${subcommand}`);
}
}
// Registry commands
async function handleRegistryCommand(onebox: Onebox, subcommand: string, args: string[]) {
switch (subcommand) {
case 'add': {
const url = getArg(args, '--url');
const username = getArg(args, '--username');
const password = getArg(args, '--password');
await onebox.registries.addRegistry(url, username, password);
break;
}
case 'remove':
await onebox.registries.removeRegistry(getArg(args, '--url'));
break;
case 'list': {
const registries = onebox.registries.listRegistries();
logger.table(
['URL', 'Username'],
registries.map((r) => [r.url, r.username])
);
break;
}
default:
logger.error(`Unknown registry subcommand: ${subcommand}`);
}
}
// DNS commands
async function handleDnsCommand(onebox: Onebox, subcommand: string, args: string[]) {
switch (subcommand) {
case 'add':
await onebox.dns.addDNSRecord(args[0]);
break;
case 'remove':
await onebox.dns.removeDNSRecord(args[0]);
break;
case 'list': {
const records = onebox.dns.listDNSRecords();
logger.table(
['Domain', 'Type', 'Value'],
records.map((r) => [r.domain, r.type, r.value])
);
break;
}
case 'sync':
await onebox.dns.syncFromCloudflare();
break;
default:
logger.error(`Unknown dns subcommand: ${subcommand}`);
}
}
// SSL commands
async function handleSslCommand(onebox: Onebox, subcommand: string, args: string[]) {
switch (subcommand) {
case 'renew':
if (args[0]) {
await onebox.ssl.renewCertificate(args[0]);
} else {
await onebox.ssl.renewExpiring();
}
break;
case 'list': {
const certs = onebox.ssl.listCertificates();
logger.table(
['Domain', 'Expiry', 'Issuer'],
certs.map((c) => [c.domain, new Date(c.expiryDate).toISOString(), c.issuer])
);
break;
}
case 'force-renew':
await onebox.ssl.renewCertificate(args[0]);
break;
default:
logger.error(`Unknown ssl subcommand: ${subcommand}`);
}
}
// Nginx commands
async function handleNginxCommand(onebox: Onebox, subcommand: string, _args: string[]) {
switch (subcommand) {
case 'reload':
await onebox.nginx.reload();
break;
case 'test':
await onebox.nginx.test();
break;
case 'status': {
const status = await onebox.nginx.getStatus();
logger.info(`Nginx status: ${status}`);
break;
}
default:
logger.error(`Unknown nginx subcommand: ${subcommand}`);
}
}
// Daemon commands
async function handleDaemonCommand(onebox: Onebox, subcommand: string, _args: string[]) {
switch (subcommand) {
case 'install':
await onebox.daemon.installService();
break;
case 'start':
await onebox.startDaemon();
break;
case 'stop':
await onebox.stopDaemon();
break;
case 'logs': {
const command = new Deno.Command('journalctl', {
args: ['-u', 'smartdaemon_onebox', '-f'],
stdout: 'inherit',
stderr: 'inherit',
});
await command.output();
break;
}
case 'status': {
const status = await onebox.daemon.getServiceStatus();
logger.info(`Daemon status: ${status}`);
break;
}
default:
logger.error(`Unknown daemon subcommand: ${subcommand}`);
}
}
// Config commands
async function handleConfigCommand(onebox: Onebox, subcommand: string, args: string[]) {
switch (subcommand) {
case 'show': {
const settings = onebox.database.getAllSettings();
logger.table(
['Key', 'Value'],
Object.entries(settings).map(([k, v]) => [k, v])
);
break;
}
case 'set':
onebox.database.setSetting(args[0], args[1]);
logger.success(`Setting ${args[0]} updated`);
break;
default:
logger.error(`Unknown config subcommand: ${subcommand}`);
}
}
// Status command
async function handleStatusCommand(onebox: Onebox) {
const status = await onebox.getSystemStatus();
console.log(JSON.stringify(status, null, 2));
}
// Helpers
function getArg(args: string[], flag: string): string {
const arg = args.find((a) => a.startsWith(`${flag}=`));
return arg ? arg.split('=')[1] : '';
}
function printHelp(): void {
console.log(`
Onebox v${projectInfo.version} - Self-hosted container platform
Usage: onebox <command> [options]
Commands:
service add <name> --image <image> [--domain <domain>] [--port <port>] [--env KEY=VALUE]
service remove <name>
service start <name>
service stop <name>
service restart <name>
service list
service logs <name>
registry add --url <url> --username <user> --password <pass>
registry remove --url <url>
registry list
dns add <domain>
dns remove <domain>
dns list
dns sync
ssl renew [domain]
ssl list
ssl force-renew <domain>
nginx reload
nginx test
nginx status
daemon install
daemon start
daemon stop
daemon logs
daemon status
config show
config set <key> <value>
status
Options:
--help, -h Show this help message
--version, -v Show version
--debug Enable debug logging
Examples:
onebox service add myapp --image nginx:latest --domain app.example.com --port 80
onebox registry add --url registry.example.com --username user --password pass
onebox daemon install
onebox daemon start
`);
}

12
ts/onebox.info.ts Normal file
View File

@@ -0,0 +1,12 @@
/**
* Project information and version
*/
import denoConfig from '../deno.json' with { type: 'json' };
export const projectInfo = {
name: denoConfig.name,
version: denoConfig.version,
description: 'Self-hosted container platform with automatic SSL and DNS',
repository: 'https://code.foss.global/serve.zone/onebox',
};

124
ts/onebox.logging.ts Normal file
View File

@@ -0,0 +1,124 @@
/**
* Logging utilities for Onebox
*/
type LogLevel = 'info' | 'success' | 'warn' | 'error' | 'debug';
class Logger {
private debugMode = false;
constructor() {
this.debugMode = Deno.args.includes('--debug') || Deno.env.get('DEBUG') === 'true';
}
/**
* Log a message with specified level
*/
log(level: LogLevel, message: string, ...args: unknown[]): void {
const timestamp = new Date().toISOString();
const prefix = this.getPrefix(level);
const formattedMessage = `${prefix} ${message}`;
switch (level) {
case 'error':
console.error(formattedMessage, ...args);
break;
case 'warn':
console.warn(formattedMessage, ...args);
break;
case 'debug':
if (this.debugMode) {
console.log(formattedMessage, ...args);
}
break;
default:
console.log(formattedMessage, ...args);
}
}
/**
* Info level logging
*/
info(message: string, ...args: unknown[]): void {
this.log('info', message, ...args);
}
/**
* Success level logging
*/
success(message: string, ...args: unknown[]): void {
this.log('success', message, ...args);
}
/**
* Warning level logging
*/
warn(message: string, ...args: unknown[]): void {
this.log('warn', message, ...args);
}
/**
* Error level logging
*/
error(message: string, ...args: unknown[]): void {
this.log('error', message, ...args);
}
/**
* Debug level logging (only when --debug flag is present)
*/
debug(message: string, ...args: unknown[]): void {
this.log('debug', message, ...args);
}
/**
* Get colored prefix for log level
*/
private getPrefix(level: LogLevel): string {
const colors = {
info: '\x1b[36m', // Cyan
success: '\x1b[32m', // Green
warn: '\x1b[33m', // Yellow
error: '\x1b[31m', // Red
debug: '\x1b[90m', // Gray
};
const reset = '\x1b[0m';
const icons = {
info: '',
success: '✓',
warn: '⚠',
error: '✖',
debug: '⚙',
};
return `${colors[level]}${icons[level]}${reset}`;
}
/**
* Print a table (simplified version)
*/
table(headers: string[], rows: string[][]): void {
// Calculate column widths
const widths = headers.map((header, i) => {
const maxContentWidth = Math.max(
...rows.map((row) => (row[i] || '').toString().length)
);
return Math.max(header.length, maxContentWidth);
});
// Print header
const headerRow = headers.map((h, i) => h.padEnd(widths[i])).join(' ');
console.log(headerRow);
console.log(headers.map((_, i) => '-'.repeat(widths[i])).join(' '));
// Print rows
for (const row of rows) {
const formattedRow = row.map((cell, i) => (cell || '').toString().padEnd(widths[i])).join(' ');
console.log(formattedRow);
}
}
}
export const logger = new Logger();

46
ts/onebox.plugins.ts Normal file
View File

@@ -0,0 +1,46 @@
/**
* Centralized dependency imports for Onebox
*
* This file serves as the single source of truth for all external dependencies.
* All modules should import from this file using: import * as plugins from './onebox.plugins.ts'
*/
// Deno Standard Library
import * as path from '@std/path';
import * as fs from '@std/fs';
import * as http from '@std/http';
import * as encoding from '@std/encoding';
export { path, fs, http, encoding };
// Database
import * as sqlite from '@db/sqlite';
export { sqlite };
// Systemd Daemon Integration
import * as smartdaemon from '@push.rocks/smartdaemon';
export { smartdaemon };
// Docker API Client
import * as docker from '@apiclient.xyz/docker';
export { docker };
// Cloudflare DNS Management
import * as cloudflare from '@apiclient.xyz/cloudflare';
export { cloudflare };
// Let's Encrypt / ACME
import * as smartacme from '@push.rocks/smartacme';
export { smartacme };
// Crypto utilities (for password hashing, encryption)
import * as bcrypt from 'https://deno.land/x/bcrypt@v0.4.1/mod.ts';
export { bcrypt };
// JWT for authentication
import { create as createJwt, verify as verifyJwt, decode as decodeJwt } from 'https://deno.land/x/djwt@v3.0.2/mod.ts';
export { createJwt, verifyJwt, decodeJwt };
// Crypto key management
import { crypto } from 'https://deno.land/std@0.208.0/crypto/mod.ts';
export { crypto };

165
ts/onebox.types.ts Normal file
View File

@@ -0,0 +1,165 @@
/**
* Type definitions for Onebox
*/
// Service types
export interface IService {
id?: number;
name: string;
image: string;
registry?: string;
envVars: Record<string, string>;
port: number;
domain?: string;
containerID?: string;
status: 'stopped' | 'starting' | 'running' | 'stopping' | 'failed';
createdAt: number;
updatedAt: number;
}
// Registry types
export interface IRegistry {
id?: number;
url: string;
username: string;
passwordEncrypted: string;
createdAt: number;
}
// Nginx configuration types
export interface INginxConfig {
id?: number;
serviceId: number;
domain: string;
port: number;
sslEnabled: boolean;
configTemplate: string;
createdAt: number;
updatedAt: number;
}
// SSL certificate types
export interface ISslCertificate {
id?: number;
domain: string;
certPath: string;
keyPath: string;
fullChainPath: string;
expiryDate: number;
issuer: string;
createdAt: number;
updatedAt: number;
}
// DNS record types
export interface IDnsRecord {
id?: number;
domain: string;
type: 'A' | 'AAAA' | 'CNAME';
value: string;
cloudflareID?: string;
createdAt: number;
updatedAt: number;
}
// Metrics types
export interface IMetric {
id?: number;
serviceId: number;
timestamp: number;
cpuPercent: number;
memoryUsed: number;
memoryLimit: number;
networkRxBytes: number;
networkTxBytes: number;
}
// Log entry types
export interface ILogEntry {
id?: number;
serviceId: number;
timestamp: number;
message: string;
level: 'info' | 'warn' | 'error' | 'debug';
source: 'stdout' | 'stderr';
}
// User types
export interface IUser {
id?: number;
username: string;
passwordHash: string;
role: 'admin' | 'user';
createdAt: number;
updatedAt: number;
}
// Settings types
export interface ISetting {
key: string;
value: string;
updatedAt: number;
}
// Application settings
export interface IAppSettings {
serverIP?: string;
cloudflareAPIKey?: string;
cloudflareEmail?: string;
cloudflareZoneID?: string;
acmeEmail?: string;
nginxConfigDir?: string;
dataDir?: string;
httpPort?: number;
metricsInterval?: number;
logRetentionDays?: number;
}
// Container stats from Docker
export interface IContainerStats {
cpuPercent: number;
memoryUsed: number;
memoryLimit: number;
memoryPercent: number;
networkRx: number;
networkTx: number;
}
// Service deployment options
export interface IServiceDeployOptions {
name: string;
image: string;
registry?: string;
envVars?: Record<string, string>;
port: number;
domain?: string;
autoSSL?: boolean;
autoDNS?: boolean;
}
// HTTP API request/response types
export interface IApiResponse<T = unknown> {
success: boolean;
data?: T;
error?: string;
message?: string;
}
export interface ILoginRequest {
username: string;
password: string;
}
export interface ILoginResponse {
token: string;
user: {
username: string;
role: string;
};
}
// CLI command types
export interface ICliArgs {
_: string[];
[key: string]: unknown;
}