Files
onebox/ts/onebox.classes.database.ts
Juergen Kunz 246a6073e0 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.
2025-10-28 13:05:42 +00:00

660 lines
19 KiB
TypeScript

/**
* 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]),
};
}
}