660 lines
19 KiB
TypeScript
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]),
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|