- Added base interface and abstract class for platform service providers. - Created MinIOProvider class for S3-compatible storage with deployment, provisioning, and deprovisioning functionalities. - Implemented MongoDBProvider class for MongoDB service with similar capabilities. - Introduced error handling utilities for better error management. - Developed TokensComponent for managing registry tokens in the UI, including creation, deletion, and display of tokens.
1842 lines
61 KiB
TypeScript
1842 lines
61 KiB
TypeScript
/**
|
|
* Database layer for Onebox using SQLite
|
|
*/
|
|
|
|
import * as plugins from '../plugins.ts';
|
|
import type {
|
|
IService,
|
|
IRegistry,
|
|
IRegistryToken,
|
|
INginxConfig,
|
|
ISslCertificate,
|
|
IDnsRecord,
|
|
IMetric,
|
|
ILogEntry,
|
|
IUser,
|
|
ISetting,
|
|
IPlatformService,
|
|
IPlatformResource,
|
|
IPlatformRequirements,
|
|
TPlatformServiceType,
|
|
IDomain,
|
|
ICertificate,
|
|
ICertRequirement,
|
|
} from '../types.ts';
|
|
|
|
// Type alias for sqlite bind parameters
|
|
type BindValue = string | number | bigint | boolean | null | undefined | Uint8Array;
|
|
import { logger } from '../logging.ts';
|
|
import { getErrorMessage } from '../utils/error.ts';
|
|
|
|
export class OneboxDatabase {
|
|
private db: InstanceType<typeof plugins.sqlite.DB> | null = null;
|
|
private dbPath: string;
|
|
|
|
constructor(dbPath = './.nogit/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: ${getErrorMessage(error)}`);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create all database tables
|
|
*/
|
|
private async createTables(): Promise<void> {
|
|
if (!this.db) throw new Error('Database not initialized');
|
|
|
|
// Services table
|
|
this.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.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.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.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.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,
|
|
zone_id TEXT,
|
|
created_at INTEGER NOT NULL,
|
|
updated_at INTEGER NOT NULL
|
|
)
|
|
`);
|
|
|
|
// Metrics table
|
|
this.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.query(`
|
|
CREATE INDEX IF NOT EXISTS idx_metrics_service_timestamp
|
|
ON metrics(service_id, timestamp DESC)
|
|
`);
|
|
|
|
// Logs table
|
|
this.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.query(`
|
|
CREATE INDEX IF NOT EXISTS idx_logs_service_timestamp
|
|
ON logs(service_id, timestamp DESC)
|
|
`);
|
|
|
|
// Users table
|
|
this.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.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.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');
|
|
|
|
try {
|
|
const currentVersion = this.getMigrationVersion();
|
|
logger.info(`Current database migration version: ${currentVersion}`);
|
|
|
|
// Migration 1: Initial schema
|
|
if (currentVersion === 0) {
|
|
logger.info('Setting initial migration version to 1');
|
|
this.setMigrationVersion(1);
|
|
}
|
|
|
|
// Migration 2: Convert timestamp columns from INTEGER to REAL
|
|
const updatedVersion = this.getMigrationVersion();
|
|
if (updatedVersion < 2) {
|
|
logger.info('Running migration 2: Converting timestamps to REAL...');
|
|
|
|
// For each table, we need to:
|
|
// 1. Create new table with REAL timestamps
|
|
// 2. Copy data
|
|
// 3. Drop old table
|
|
// 4. Rename new table
|
|
|
|
// SSL certificates
|
|
this.query(`
|
|
CREATE TABLE ssl_certificates_new (
|
|
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 REAL NOT NULL,
|
|
issuer TEXT NOT NULL,
|
|
created_at REAL NOT NULL,
|
|
updated_at REAL NOT NULL
|
|
)
|
|
`);
|
|
this.query(`INSERT INTO ssl_certificates_new SELECT * FROM ssl_certificates`);
|
|
this.query(`DROP TABLE ssl_certificates`);
|
|
this.query(`ALTER TABLE ssl_certificates_new RENAME TO ssl_certificates`);
|
|
|
|
// Services
|
|
this.query(`
|
|
CREATE TABLE services_new (
|
|
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 REAL NOT NULL,
|
|
updated_at REAL NOT NULL
|
|
)
|
|
`);
|
|
this.query(`INSERT INTO services_new SELECT * FROM services`);
|
|
this.query(`DROP TABLE services`);
|
|
this.query(`ALTER TABLE services_new RENAME TO services`);
|
|
|
|
// Registries
|
|
this.query(`
|
|
CREATE TABLE registries_new (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
url TEXT NOT NULL UNIQUE,
|
|
username TEXT NOT NULL,
|
|
password_encrypted TEXT NOT NULL,
|
|
created_at REAL NOT NULL
|
|
)
|
|
`);
|
|
this.query(`INSERT INTO registries_new SELECT * FROM registries`);
|
|
this.query(`DROP TABLE registries`);
|
|
this.query(`ALTER TABLE registries_new RENAME TO registries`);
|
|
|
|
// Nginx configs
|
|
this.query(`
|
|
CREATE TABLE nginx_configs_new (
|
|
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 REAL NOT NULL,
|
|
updated_at REAL NOT NULL,
|
|
FOREIGN KEY (service_id) REFERENCES services(id) ON DELETE CASCADE
|
|
)
|
|
`);
|
|
this.query(`INSERT INTO nginx_configs_new SELECT * FROM nginx_configs`);
|
|
this.query(`DROP TABLE nginx_configs`);
|
|
this.query(`ALTER TABLE nginx_configs_new RENAME TO nginx_configs`);
|
|
|
|
// DNS records
|
|
this.query(`
|
|
CREATE TABLE dns_records_new (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
domain TEXT NOT NULL UNIQUE,
|
|
type TEXT NOT NULL,
|
|
value TEXT NOT NULL,
|
|
cloudflare_id TEXT,
|
|
zone_id TEXT,
|
|
created_at REAL NOT NULL,
|
|
updated_at REAL NOT NULL
|
|
)
|
|
`);
|
|
this.query(`INSERT INTO dns_records_new SELECT * FROM dns_records`);
|
|
this.query(`DROP TABLE dns_records`);
|
|
this.query(`ALTER TABLE dns_records_new RENAME TO dns_records`);
|
|
|
|
// Metrics
|
|
this.query(`
|
|
CREATE TABLE metrics_new (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
service_id INTEGER NOT NULL,
|
|
timestamp REAL 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
|
|
)
|
|
`);
|
|
this.query(`INSERT INTO metrics_new SELECT * FROM metrics`);
|
|
this.query(`DROP TABLE metrics`);
|
|
this.query(`ALTER TABLE metrics_new RENAME TO metrics`);
|
|
this.query(`CREATE INDEX IF NOT EXISTS idx_metrics_service_timestamp ON metrics(service_id, timestamp DESC)`);
|
|
|
|
// Logs
|
|
this.query(`
|
|
CREATE TABLE logs_new (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
service_id INTEGER NOT NULL,
|
|
timestamp REAL NOT NULL,
|
|
message TEXT NOT NULL,
|
|
level TEXT NOT NULL,
|
|
source TEXT NOT NULL,
|
|
FOREIGN KEY (service_id) REFERENCES services(id) ON DELETE CASCADE
|
|
)
|
|
`);
|
|
this.query(`INSERT INTO logs_new SELECT * FROM logs`);
|
|
this.query(`DROP TABLE logs`);
|
|
this.query(`ALTER TABLE logs_new RENAME TO logs`);
|
|
this.query(`CREATE INDEX IF NOT EXISTS idx_logs_service_timestamp ON logs(service_id, timestamp DESC)`);
|
|
|
|
// Users
|
|
this.query(`
|
|
CREATE TABLE users_new (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
username TEXT NOT NULL UNIQUE,
|
|
password_hash TEXT NOT NULL,
|
|
role TEXT NOT NULL DEFAULT 'user',
|
|
created_at REAL NOT NULL,
|
|
updated_at REAL NOT NULL
|
|
)
|
|
`);
|
|
this.query(`INSERT INTO users_new SELECT * FROM users`);
|
|
this.query(`DROP TABLE users`);
|
|
this.query(`ALTER TABLE users_new RENAME TO users`);
|
|
|
|
// Settings
|
|
this.query(`
|
|
CREATE TABLE settings_new (
|
|
key TEXT PRIMARY KEY,
|
|
value TEXT NOT NULL,
|
|
updated_at REAL NOT NULL
|
|
)
|
|
`);
|
|
this.query(`INSERT INTO settings_new SELECT * FROM settings`);
|
|
this.query(`DROP TABLE settings`);
|
|
this.query(`ALTER TABLE settings_new RENAME TO settings`);
|
|
|
|
// Migrations table itself
|
|
this.query(`
|
|
CREATE TABLE migrations_new (
|
|
version INTEGER PRIMARY KEY,
|
|
applied_at REAL NOT NULL
|
|
)
|
|
`);
|
|
this.query(`INSERT INTO migrations_new SELECT * FROM migrations`);
|
|
this.query(`DROP TABLE migrations`);
|
|
this.query(`ALTER TABLE migrations_new RENAME TO migrations`);
|
|
|
|
this.setMigrationVersion(2);
|
|
logger.success('Migration 2 completed: All timestamps converted to REAL');
|
|
}
|
|
|
|
// Migration 3: Domain management tables
|
|
const version3 = this.getMigrationVersion();
|
|
if (version3 < 3) {
|
|
logger.info('Running migration 3: Creating domain management tables...');
|
|
|
|
// 1. Create domains table
|
|
this.query(`
|
|
CREATE TABLE domains (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
domain TEXT NOT NULL UNIQUE,
|
|
dns_provider TEXT,
|
|
cloudflare_zone_id TEXT,
|
|
is_obsolete INTEGER NOT NULL DEFAULT 0,
|
|
default_wildcard INTEGER NOT NULL DEFAULT 1,
|
|
created_at REAL NOT NULL,
|
|
updated_at REAL NOT NULL
|
|
)
|
|
`);
|
|
|
|
// 2. Create certificates table (renamed from ssl_certificates)
|
|
this.query(`
|
|
CREATE TABLE certificates (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
domain_id INTEGER NOT NULL,
|
|
cert_domain TEXT NOT NULL,
|
|
is_wildcard INTEGER NOT NULL DEFAULT 0,
|
|
cert_path TEXT NOT NULL,
|
|
key_path TEXT NOT NULL,
|
|
full_chain_path TEXT NOT NULL,
|
|
expiry_date REAL NOT NULL,
|
|
issuer TEXT NOT NULL,
|
|
is_valid INTEGER NOT NULL DEFAULT 1,
|
|
created_at REAL NOT NULL,
|
|
updated_at REAL NOT NULL,
|
|
FOREIGN KEY (domain_id) REFERENCES domains(id) ON DELETE CASCADE
|
|
)
|
|
`);
|
|
|
|
// 3. Create cert_requirements table
|
|
this.query(`
|
|
CREATE TABLE cert_requirements (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
service_id INTEGER NOT NULL,
|
|
domain_id INTEGER NOT NULL,
|
|
subdomain TEXT NOT NULL,
|
|
certificate_id INTEGER,
|
|
status TEXT NOT NULL DEFAULT 'pending',
|
|
created_at REAL NOT NULL,
|
|
updated_at REAL NOT NULL,
|
|
FOREIGN KEY (service_id) REFERENCES services(id) ON DELETE CASCADE,
|
|
FOREIGN KEY (domain_id) REFERENCES domains(id) ON DELETE CASCADE,
|
|
FOREIGN KEY (certificate_id) REFERENCES certificates(id) ON DELETE SET NULL
|
|
)
|
|
`);
|
|
|
|
// 4. Migrate existing ssl_certificates data
|
|
// Extract unique base domains from existing certificates
|
|
interface OldSslCert {
|
|
id?: number;
|
|
domain?: string;
|
|
cert_path?: string;
|
|
key_path?: string;
|
|
full_chain_path?: string;
|
|
expiry_date?: number;
|
|
issuer?: string;
|
|
created_at?: number;
|
|
updated_at?: number;
|
|
[key: number]: unknown; // Allow array-style access as fallback
|
|
}
|
|
const existingCerts = this.query<OldSslCert>('SELECT * FROM ssl_certificates');
|
|
|
|
const now = Date.now();
|
|
const domainMap = new Map<string, number>();
|
|
|
|
// Create domain entries for each unique base domain
|
|
for (const cert of existingCerts) {
|
|
const domain = String(cert.domain ?? (cert as Record<number, unknown>)[1]);
|
|
if (!domainMap.has(domain)) {
|
|
this.query(
|
|
'INSERT INTO domains (domain, dns_provider, is_obsolete, default_wildcard, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)',
|
|
[domain, null, 0, 1, now, now]
|
|
);
|
|
const result = this.query<{ id?: number; [key: number]: unknown }>('SELECT last_insert_rowid() as id');
|
|
const domainId = result[0].id ?? (result[0] as Record<number, unknown>)[0];
|
|
domainMap.set(domain, Number(domainId));
|
|
}
|
|
}
|
|
|
|
// Migrate certificates to new table
|
|
for (const cert of existingCerts) {
|
|
const domain = String(cert.domain ?? (cert as Record<number, unknown>)[1]);
|
|
const domainId = domainMap.get(domain);
|
|
|
|
this.query(
|
|
`INSERT INTO certificates (
|
|
domain_id, cert_domain, is_wildcard, cert_path, key_path, full_chain_path,
|
|
expiry_date, issuer, is_valid, created_at, updated_at
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
[
|
|
domainId,
|
|
domain,
|
|
0, // We don't know if it's wildcard, default to false
|
|
String(cert.cert_path ?? (cert as Record<number, unknown>)[2]),
|
|
String(cert.key_path ?? (cert as Record<number, unknown>)[3]),
|
|
String(cert.full_chain_path ?? (cert as Record<number, unknown>)[4]),
|
|
Number(cert.expiry_date ?? (cert as Record<number, unknown>)[5]),
|
|
String(cert.issuer ?? (cert as Record<number, unknown>)[6]),
|
|
1, // Assume valid
|
|
Number(cert.created_at ?? (cert as Record<number, unknown>)[7]),
|
|
Number(cert.updated_at ?? (cert as Record<number, unknown>)[8])
|
|
]
|
|
);
|
|
}
|
|
|
|
// 5. Drop old ssl_certificates table
|
|
this.query('DROP TABLE ssl_certificates');
|
|
|
|
// 6. Create indices for performance
|
|
this.query('CREATE INDEX IF NOT EXISTS idx_domains_cloudflare_zone ON domains(cloudflare_zone_id)');
|
|
this.query('CREATE INDEX IF NOT EXISTS idx_certificates_domain ON certificates(domain_id)');
|
|
this.query('CREATE INDEX IF NOT EXISTS idx_certificates_expiry ON certificates(expiry_date)');
|
|
this.query('CREATE INDEX IF NOT EXISTS idx_cert_requirements_service ON cert_requirements(service_id)');
|
|
this.query('CREATE INDEX IF NOT EXISTS idx_cert_requirements_domain ON cert_requirements(domain_id)');
|
|
|
|
this.setMigrationVersion(3);
|
|
logger.success('Migration 3 completed: Domain management tables created');
|
|
}
|
|
|
|
// Migration 4: Add Onebox Registry support columns to services table
|
|
const version4 = this.getMigrationVersion();
|
|
if (version4 < 4) {
|
|
logger.info('Running migration 4: Adding Onebox Registry columns to services table...');
|
|
|
|
// Add new columns for registry support
|
|
this.query(`
|
|
ALTER TABLE services ADD COLUMN use_onebox_registry INTEGER DEFAULT 0
|
|
`);
|
|
this.query(`
|
|
ALTER TABLE services ADD COLUMN registry_repository TEXT
|
|
`);
|
|
this.query(`
|
|
ALTER TABLE services ADD COLUMN registry_token TEXT
|
|
`);
|
|
this.query(`
|
|
ALTER TABLE services ADD COLUMN registry_image_tag TEXT DEFAULT 'latest'
|
|
`);
|
|
this.query(`
|
|
ALTER TABLE services ADD COLUMN auto_update_on_push INTEGER DEFAULT 0
|
|
`);
|
|
this.query(`
|
|
ALTER TABLE services ADD COLUMN image_digest TEXT
|
|
`);
|
|
|
|
this.setMigrationVersion(4);
|
|
logger.success('Migration 4 completed: Onebox Registry columns added to services table');
|
|
}
|
|
|
|
// Migration 5: Registry tokens table
|
|
const version5 = this.getMigrationVersion();
|
|
if (version5 < 5) {
|
|
logger.info('Running migration 5: Creating registry_tokens table...');
|
|
|
|
this.query(`
|
|
CREATE TABLE registry_tokens (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL,
|
|
token_hash TEXT NOT NULL UNIQUE,
|
|
token_type TEXT NOT NULL,
|
|
scope TEXT NOT NULL,
|
|
expires_at REAL,
|
|
created_at REAL NOT NULL,
|
|
last_used_at REAL,
|
|
created_by TEXT NOT NULL
|
|
)
|
|
`);
|
|
|
|
// Create indices for performance
|
|
this.query('CREATE INDEX IF NOT EXISTS idx_registry_tokens_type ON registry_tokens(token_type)');
|
|
this.query('CREATE INDEX IF NOT EXISTS idx_registry_tokens_hash ON registry_tokens(token_hash)');
|
|
|
|
this.setMigrationVersion(5);
|
|
logger.success('Migration 5 completed: Registry tokens table created');
|
|
}
|
|
|
|
// Migration 6: Drop registry_token column from services table (replaced by registry_tokens table)
|
|
const version6 = this.getMigrationVersion();
|
|
if (version6 < 6) {
|
|
logger.info('Running migration 6: Dropping registry_token column from services table...');
|
|
|
|
// SQLite doesn't support DROP COLUMN directly, so we need to recreate the table
|
|
// Create new table without registry_token
|
|
this.query(`
|
|
CREATE TABLE services_new (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL UNIQUE,
|
|
image TEXT NOT NULL,
|
|
registry TEXT,
|
|
env_vars TEXT,
|
|
port INTEGER NOT NULL,
|
|
domain TEXT,
|
|
container_id TEXT,
|
|
status TEXT NOT NULL,
|
|
created_at REAL NOT NULL,
|
|
updated_at REAL NOT NULL,
|
|
use_onebox_registry INTEGER DEFAULT 0,
|
|
registry_repository TEXT,
|
|
registry_image_tag TEXT DEFAULT 'latest',
|
|
auto_update_on_push INTEGER DEFAULT 0,
|
|
image_digest TEXT
|
|
)
|
|
`);
|
|
|
|
// Copy data (excluding registry_token)
|
|
this.query(`
|
|
INSERT INTO services_new (
|
|
id, name, image, registry, env_vars, port, domain, container_id, status,
|
|
created_at, updated_at, use_onebox_registry, registry_repository,
|
|
registry_image_tag, auto_update_on_push, image_digest
|
|
)
|
|
SELECT
|
|
id, name, image, registry, env_vars, port, domain, container_id, status,
|
|
created_at, updated_at, use_onebox_registry, registry_repository,
|
|
registry_image_tag, auto_update_on_push, image_digest
|
|
FROM services
|
|
`);
|
|
|
|
// Drop old table
|
|
this.query('DROP TABLE services');
|
|
|
|
// Rename new table
|
|
this.query('ALTER TABLE services_new RENAME TO services');
|
|
|
|
// Recreate indices
|
|
this.query('CREATE INDEX IF NOT EXISTS idx_services_name ON services(name)');
|
|
this.query('CREATE INDEX IF NOT EXISTS idx_services_status ON services(status)');
|
|
|
|
this.setMigrationVersion(6);
|
|
logger.success('Migration 6 completed: registry_token column dropped from services table');
|
|
}
|
|
|
|
// Migration 7: Platform services tables
|
|
const version7 = this.getMigrationVersion();
|
|
if (version7 < 7) {
|
|
logger.info('Running migration 7: Creating platform services tables...');
|
|
|
|
// Create platform_services table
|
|
this.query(`
|
|
CREATE TABLE platform_services (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL UNIQUE,
|
|
type TEXT NOT NULL,
|
|
status TEXT NOT NULL DEFAULT 'stopped',
|
|
container_id TEXT,
|
|
config TEXT NOT NULL DEFAULT '{}',
|
|
admin_credentials_encrypted TEXT,
|
|
created_at REAL NOT NULL,
|
|
updated_at REAL NOT NULL
|
|
)
|
|
`);
|
|
|
|
// Create platform_resources table
|
|
this.query(`
|
|
CREATE TABLE platform_resources (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
platform_service_id INTEGER NOT NULL,
|
|
service_id INTEGER NOT NULL,
|
|
resource_type TEXT NOT NULL,
|
|
resource_name TEXT NOT NULL,
|
|
credentials_encrypted TEXT NOT NULL,
|
|
created_at REAL NOT NULL,
|
|
FOREIGN KEY (platform_service_id) REFERENCES platform_services(id) ON DELETE CASCADE,
|
|
FOREIGN KEY (service_id) REFERENCES services(id) ON DELETE CASCADE
|
|
)
|
|
`);
|
|
|
|
// Add platform_requirements column to services table
|
|
this.query(`
|
|
ALTER TABLE services ADD COLUMN platform_requirements TEXT DEFAULT '{}'
|
|
`);
|
|
|
|
// Create indices
|
|
this.query('CREATE INDEX IF NOT EXISTS idx_platform_services_type ON platform_services(type)');
|
|
this.query('CREATE INDEX IF NOT EXISTS idx_platform_resources_service ON platform_resources(service_id)');
|
|
this.query('CREATE INDEX IF NOT EXISTS idx_platform_resources_platform ON platform_resources(platform_service_id)');
|
|
|
|
this.setMigrationVersion(7);
|
|
logger.success('Migration 7 completed: Platform services tables created');
|
|
}
|
|
} catch (error) {
|
|
logger.error(`Migration failed: ${getErrorMessage(error)}`);
|
|
if (error instanceof Error && error.stack) {
|
|
logger.error(`Stack: ${error.stack}`);
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get current migration version
|
|
*/
|
|
private getMigrationVersion(): number {
|
|
if (!this.db) throw new Error('Database not initialized');
|
|
|
|
try {
|
|
const result = this.query<{ version?: number | null; [key: number]: unknown }>('SELECT MAX(version) as version FROM migrations');
|
|
if (result.length === 0) return 0;
|
|
|
|
// Handle both array and object access patterns
|
|
const versionValue = result[0].version ?? (result[0] as Record<number, unknown>)[0];
|
|
return versionValue !== null && versionValue !== undefined ? Number(versionValue) : 0;
|
|
} catch (error) {
|
|
logger.warn(`Error getting migration version: ${getErrorMessage(error)}, defaulting to 0`);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set migration version
|
|
*/
|
|
private setMigrationVersion(version: number): void {
|
|
if (!this.db) throw new Error('Database not initialized');
|
|
|
|
this.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 = Record<string, unknown>>(sql: string, params: BindValue[] = []): T[] {
|
|
if (!this.db) {
|
|
const error = new Error('Database not initialized');
|
|
console.error('Database access before initialization!');
|
|
console.error('Stack trace:', error.stack);
|
|
throw error;
|
|
}
|
|
|
|
// For queries without parameters, use exec for better performance
|
|
if (params.length === 0 && !sql.trim().toUpperCase().startsWith('SELECT')) {
|
|
this.db.exec(sql);
|
|
return [] as T[];
|
|
}
|
|
|
|
// For SELECT queries or statements with parameters, use prepare().all()
|
|
const stmt = this.db.prepare(sql);
|
|
try {
|
|
const result = stmt.all(...params);
|
|
return result as T[];
|
|
} finally {
|
|
stmt.finalize();
|
|
}
|
|
}
|
|
|
|
// ============ Services CRUD ============
|
|
|
|
async createService(service: Omit<IService, 'id'>): Promise<IService> {
|
|
if (!this.db) throw new Error('Database not initialized');
|
|
|
|
const now = Date.now();
|
|
this.query(
|
|
`INSERT INTO services (
|
|
name, image, registry, env_vars, port, domain, container_id, status,
|
|
created_at, updated_at,
|
|
use_onebox_registry, registry_repository, registry_image_tag,
|
|
auto_update_on_push, image_digest, platform_requirements
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
[
|
|
service.name,
|
|
service.image,
|
|
service.registry || null,
|
|
JSON.stringify(service.envVars),
|
|
service.port,
|
|
service.domain || null,
|
|
service.containerID || null,
|
|
service.status,
|
|
now,
|
|
now,
|
|
service.useOneboxRegistry ? 1 : 0,
|
|
service.registryRepository || null,
|
|
service.registryImageTag || 'latest',
|
|
service.autoUpdateOnPush ? 1 : 0,
|
|
service.imageDigest || null,
|
|
JSON.stringify(service.platformRequirements || {}),
|
|
]
|
|
);
|
|
|
|
return this.getServiceByName(service.name)!;
|
|
}
|
|
|
|
getServiceByName(name: string): IService | null {
|
|
if (!this.db) throw new Error('Database not initialized');
|
|
|
|
const rows = this.query('SELECT * FROM services WHERE name = ?', [name]);
|
|
if (rows.length > 0) {
|
|
logger.info(`getServiceByName: raw row data: ${JSON.stringify(rows[0])}`);
|
|
const service = this.rowToService(rows[0]);
|
|
logger.info(`getServiceByName: service object containerID: ${service.containerID}`);
|
|
return service;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
getServiceByID(id: number): IService | null {
|
|
if (!this.db) throw new Error('Database not initialized');
|
|
|
|
const rows = this.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.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);
|
|
}
|
|
// Onebox Registry fields
|
|
if (updates.useOneboxRegistry !== undefined) {
|
|
fields.push('use_onebox_registry = ?');
|
|
values.push(updates.useOneboxRegistry ? 1 : 0);
|
|
}
|
|
if (updates.registryRepository !== undefined) {
|
|
fields.push('registry_repository = ?');
|
|
values.push(updates.registryRepository);
|
|
}
|
|
if (updates.registryImageTag !== undefined) {
|
|
fields.push('registry_image_tag = ?');
|
|
values.push(updates.registryImageTag);
|
|
}
|
|
if (updates.autoUpdateOnPush !== undefined) {
|
|
fields.push('auto_update_on_push = ?');
|
|
values.push(updates.autoUpdateOnPush ? 1 : 0);
|
|
}
|
|
if (updates.imageDigest !== undefined) {
|
|
fields.push('image_digest = ?');
|
|
values.push(updates.imageDigest);
|
|
}
|
|
if (updates.platformRequirements !== undefined) {
|
|
fields.push('platform_requirements = ?');
|
|
values.push(JSON.stringify(updates.platformRequirements));
|
|
}
|
|
|
|
fields.push('updated_at = ?');
|
|
values.push(Date.now());
|
|
values.push(id);
|
|
|
|
this.query(`UPDATE services SET ${fields.join(', ')} WHERE id = ?`, values);
|
|
}
|
|
|
|
deleteService(id: number): void {
|
|
if (!this.db) throw new Error('Database not initialized');
|
|
this.query('DELETE FROM services WHERE id = ?', [id]);
|
|
}
|
|
|
|
private rowToService(row: any): IService {
|
|
// Handle env_vars JSON parsing safely
|
|
let envVars = {};
|
|
const envVarsRaw = row.env_vars || row[4];
|
|
if (envVarsRaw && envVarsRaw !== 'undefined' && envVarsRaw !== 'null') {
|
|
try {
|
|
envVars = JSON.parse(String(envVarsRaw));
|
|
} catch (e) {
|
|
logger.warn(`Failed to parse env_vars for service: ${getErrorMessage(e)}`);
|
|
envVars = {};
|
|
}
|
|
}
|
|
|
|
// Handle platform_requirements JSON parsing safely
|
|
let platformRequirements: IPlatformRequirements | undefined;
|
|
const platformReqRaw = row.platform_requirements;
|
|
if (platformReqRaw && platformReqRaw !== 'undefined' && platformReqRaw !== 'null' && platformReqRaw !== '{}') {
|
|
try {
|
|
platformRequirements = JSON.parse(String(platformReqRaw));
|
|
} catch (e) {
|
|
logger.warn(`Failed to parse platform_requirements for service: ${getErrorMessage(e)}`);
|
|
platformRequirements = undefined;
|
|
}
|
|
}
|
|
|
|
return {
|
|
id: Number(row.id || row[0]),
|
|
name: String(row.name || row[1]),
|
|
image: String(row.image || row[2]),
|
|
registry: (row.registry || row[3]) ? String(row.registry || row[3]) : undefined,
|
|
envVars,
|
|
port: Number(row.port || row[5]),
|
|
domain: (row.domain || row[6]) ? String(row.domain || row[6]) : undefined,
|
|
containerID: (row.container_id || row[7]) ? String(row.container_id || row[7]) : undefined,
|
|
status: String(row.status || row[8]) as IService['status'],
|
|
createdAt: Number(row.created_at || row[9]),
|
|
updatedAt: Number(row.updated_at || row[10]),
|
|
// Onebox Registry fields
|
|
useOneboxRegistry: row.use_onebox_registry ? Boolean(row.use_onebox_registry) : undefined,
|
|
registryRepository: row.registry_repository ? String(row.registry_repository) : undefined,
|
|
registryImageTag: row.registry_image_tag ? String(row.registry_image_tag) : undefined,
|
|
autoUpdateOnPush: row.auto_update_on_push ? Boolean(row.auto_update_on_push) : undefined,
|
|
imageDigest: row.image_digest ? String(row.image_digest) : undefined,
|
|
// Platform service requirements
|
|
platformRequirements,
|
|
};
|
|
}
|
|
|
|
// ============ Registries CRUD ============
|
|
|
|
async createRegistry(registry: Omit<IRegistry, 'id'>): Promise<IRegistry> {
|
|
if (!this.db) throw new Error('Database not initialized');
|
|
|
|
const now = Date.now();
|
|
this.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.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.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.query('DELETE FROM registries WHERE url = ?', [url]);
|
|
}
|
|
|
|
private rowToRegistry(row: any): IRegistry {
|
|
return {
|
|
id: Number(row.id || row[0]),
|
|
url: String(row.url || row[1]),
|
|
username: String(row.username || row[2]),
|
|
passwordEncrypted: String(row.password_encrypted || row[3]),
|
|
createdAt: Number(row.created_at || row[4]),
|
|
};
|
|
}
|
|
|
|
// ============ Settings CRUD ============
|
|
|
|
getSetting(key: string): string | null {
|
|
if (!this.db) throw new Error('Database not initialized');
|
|
|
|
const rows = this.query('SELECT value FROM settings WHERE key = ?', [key]);
|
|
if (rows.length === 0) return null;
|
|
|
|
// @db/sqlite returns rows as objects with column names as keys
|
|
const value = (rows[0] as any).value || rows[0][0];
|
|
return value ? String(value) : null;
|
|
}
|
|
|
|
setSetting(key: string, value: string): void {
|
|
if (!this.db) throw new Error('Database not initialized');
|
|
|
|
const now = Date.now();
|
|
this.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.query('SELECT key, value FROM settings');
|
|
const settings: Record<string, string> = {};
|
|
for (const row of rows) {
|
|
// @db/sqlite returns rows as objects with column names as keys
|
|
const key = (row as any).key || row[0];
|
|
const value = (row as any).value || row[1];
|
|
settings[String(key)] = String(value);
|
|
}
|
|
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.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.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.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.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.query('DELETE FROM users WHERE username = ?', [username]);
|
|
}
|
|
|
|
private rowToUser(row: any): IUser {
|
|
return {
|
|
id: Number(row.id || row[0]),
|
|
username: String(row.username || row[1]),
|
|
passwordHash: String(row.password_hash || row[2]),
|
|
role: String(row.role || row[3]) as IUser['role'],
|
|
createdAt: Number(row.created_at || row[4]),
|
|
updatedAt: Number(row.updated_at || row[5]),
|
|
};
|
|
}
|
|
|
|
// ============ Metrics ============
|
|
|
|
addMetric(metric: Omit<IMetric, 'id'>): void {
|
|
if (!this.db) throw new Error('Database not initialized');
|
|
|
|
this.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.query(
|
|
'SELECT * FROM metrics WHERE service_id = ? ORDER BY timestamp DESC LIMIT ?',
|
|
[serviceId, limit]
|
|
);
|
|
return rows.map((row) => this.rowToMetric(row));
|
|
}
|
|
|
|
private rowToMetric(row: any): IMetric {
|
|
return {
|
|
id: Number(row.id || row[0]),
|
|
serviceId: Number(row.service_id || row[1]),
|
|
timestamp: Number(row.timestamp || row[2]),
|
|
cpuPercent: Number(row.cpu_percent || row[3]),
|
|
memoryUsed: Number(row.memory_used || row[4]),
|
|
memoryLimit: Number(row.memory_limit || row[5]),
|
|
networkRxBytes: Number(row.network_rx_bytes || row[6]),
|
|
networkTxBytes: Number(row.network_tx_bytes || row[7]),
|
|
};
|
|
}
|
|
|
|
// ============ Logs ============
|
|
|
|
addLog(log: Omit<ILogEntry, 'id'>): void {
|
|
if (!this.db) throw new Error('Database not initialized');
|
|
|
|
this.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.query(
|
|
'SELECT * FROM logs WHERE service_id = ? ORDER BY timestamp DESC LIMIT ?',
|
|
[serviceId, limit]
|
|
);
|
|
return rows.map((row) => this.rowToLog(row));
|
|
}
|
|
|
|
private rowToLog(row: any): ILogEntry {
|
|
return {
|
|
id: Number(row.id || row[0]),
|
|
serviceId: Number(row.service_id || row[1]),
|
|
timestamp: Number(row.timestamp || row[2]),
|
|
message: String(row.message || row[3]),
|
|
level: String(row.level || row[4]) as ILogEntry['level'],
|
|
source: String(row.source || 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.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.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.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.query(`UPDATE ssl_certificates SET ${fields.join(', ')} WHERE domain = ?`, values);
|
|
}
|
|
|
|
deleteSSLCertificate(domain: string): void {
|
|
if (!this.db) throw new Error('Database not initialized');
|
|
this.query('DELETE FROM ssl_certificates WHERE domain = ?', [domain]);
|
|
}
|
|
|
|
private rowToSSLCert(row: any): ISslCertificate {
|
|
return {
|
|
id: Number(row.id || row[0]),
|
|
domain: String(row.domain || row[1]),
|
|
certPath: String(row.cert_path || row[2]),
|
|
keyPath: String(row.key_path || row[3]),
|
|
fullChainPath: String(row.full_chain_path || row[4]),
|
|
expiryDate: Number(row.expiry_date || row[5]),
|
|
issuer: String(row.issuer || row[6]),
|
|
createdAt: Number(row.created_at || row[7]),
|
|
updatedAt: Number(row.updated_at || row[8]),
|
|
};
|
|
}
|
|
|
|
// ============ Domains ============
|
|
|
|
createDomain(domain: Omit<IDomain, 'id'>): IDomain {
|
|
if (!this.db) throw new Error('Database not initialized');
|
|
|
|
this.query(
|
|
`INSERT INTO domains (domain, dns_provider, cloudflare_zone_id, is_obsolete, default_wildcard, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
[
|
|
domain.domain,
|
|
domain.dnsProvider,
|
|
domain.cloudflareZoneId,
|
|
domain.isObsolete ? 1 : 0,
|
|
domain.defaultWildcard ? 1 : 0,
|
|
domain.createdAt,
|
|
domain.updatedAt,
|
|
]
|
|
);
|
|
|
|
return this.getDomainByName(domain.domain)!;
|
|
}
|
|
|
|
getDomainByName(domain: string): IDomain | null {
|
|
if (!this.db) throw new Error('Database not initialized');
|
|
|
|
const rows = this.query('SELECT * FROM domains WHERE domain = ?', [domain]);
|
|
return rows.length > 0 ? this.rowToDomain(rows[0]) : null;
|
|
}
|
|
|
|
getDomainById(id: number): IDomain | null {
|
|
if (!this.db) throw new Error('Database not initialized');
|
|
|
|
const rows = this.query('SELECT * FROM domains WHERE id = ?', [id]);
|
|
return rows.length > 0 ? this.rowToDomain(rows[0]) : null;
|
|
}
|
|
|
|
getAllDomains(): IDomain[] {
|
|
if (!this.db) throw new Error('Database not initialized');
|
|
|
|
const rows = this.query('SELECT * FROM domains ORDER BY domain ASC');
|
|
return rows.map((row) => this.rowToDomain(row));
|
|
}
|
|
|
|
getDomainsByProvider(provider: 'cloudflare' | 'manual'): IDomain[] {
|
|
if (!this.db) throw new Error('Database not initialized');
|
|
|
|
const rows = this.query('SELECT * FROM domains WHERE dns_provider = ? ORDER BY domain ASC', [
|
|
provider,
|
|
]);
|
|
return rows.map((row) => this.rowToDomain(row));
|
|
}
|
|
|
|
updateDomain(id: number, updates: Partial<IDomain>): void {
|
|
if (!this.db) throw new Error('Database not initialized');
|
|
|
|
const fields: string[] = [];
|
|
const values: unknown[] = [];
|
|
|
|
if (updates.domain !== undefined) {
|
|
fields.push('domain = ?');
|
|
values.push(updates.domain);
|
|
}
|
|
if (updates.dnsProvider !== undefined) {
|
|
fields.push('dns_provider = ?');
|
|
values.push(updates.dnsProvider);
|
|
}
|
|
if (updates.cloudflareZoneId !== undefined) {
|
|
fields.push('cloudflare_zone_id = ?');
|
|
values.push(updates.cloudflareZoneId);
|
|
}
|
|
if (updates.isObsolete !== undefined) {
|
|
fields.push('is_obsolete = ?');
|
|
values.push(updates.isObsolete ? 1 : 0);
|
|
}
|
|
if (updates.defaultWildcard !== undefined) {
|
|
fields.push('default_wildcard = ?');
|
|
values.push(updates.defaultWildcard ? 1 : 0);
|
|
}
|
|
|
|
fields.push('updated_at = ?');
|
|
values.push(Date.now());
|
|
values.push(id);
|
|
|
|
this.query(`UPDATE domains SET ${fields.join(', ')} WHERE id = ?`, values);
|
|
}
|
|
|
|
deleteDomain(id: number): void {
|
|
if (!this.db) throw new Error('Database not initialized');
|
|
this.query('DELETE FROM domains WHERE id = ?', [id]);
|
|
}
|
|
|
|
private rowToDomain(row: any): IDomain {
|
|
return {
|
|
id: Number(row.id || row[0]),
|
|
domain: String(row.domain || row[1]),
|
|
dnsProvider: (row.dns_provider || row[2]) as IDomain['dnsProvider'],
|
|
cloudflareZoneId: row.cloudflare_zone_id || row[3] || undefined,
|
|
isObsolete: Boolean(row.is_obsolete || row[4]),
|
|
defaultWildcard: Boolean(row.default_wildcard || row[5]),
|
|
createdAt: Number(row.created_at || row[6]),
|
|
updatedAt: Number(row.updated_at || row[7]),
|
|
};
|
|
}
|
|
|
|
// ============ Certificates ============
|
|
|
|
createCertificate(cert: Omit<ICertificate, 'id'>): ICertificate {
|
|
if (!this.db) throw new Error('Database not initialized');
|
|
|
|
this.query(
|
|
`INSERT INTO certificates (domain_id, cert_domain, is_wildcard, cert_path, key_path, full_chain_path, expiry_date, issuer, is_valid, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
[
|
|
cert.domainId,
|
|
cert.certDomain,
|
|
cert.isWildcard ? 1 : 0,
|
|
cert.certPath,
|
|
cert.keyPath,
|
|
cert.fullChainPath,
|
|
cert.expiryDate,
|
|
cert.issuer,
|
|
cert.isValid ? 1 : 0,
|
|
cert.createdAt,
|
|
cert.updatedAt,
|
|
]
|
|
);
|
|
|
|
const rows = this.query('SELECT * FROM certificates WHERE id = last_insert_rowid()');
|
|
return this.rowToCertificate(rows[0]);
|
|
}
|
|
|
|
getCertificateById(id: number): ICertificate | null {
|
|
if (!this.db) throw new Error('Database not initialized');
|
|
|
|
const rows = this.query('SELECT * FROM certificates WHERE id = ?', [id]);
|
|
return rows.length > 0 ? this.rowToCertificate(rows[0]) : null;
|
|
}
|
|
|
|
getCertificatesByDomain(domainId: number): ICertificate[] {
|
|
if (!this.db) throw new Error('Database not initialized');
|
|
|
|
const rows = this.query('SELECT * FROM certificates WHERE domain_id = ? ORDER BY expiry_date DESC', [
|
|
domainId,
|
|
]);
|
|
return rows.map((row) => this.rowToCertificate(row));
|
|
}
|
|
|
|
getAllCertificates(): ICertificate[] {
|
|
if (!this.db) throw new Error('Database not initialized');
|
|
|
|
const rows = this.query('SELECT * FROM certificates ORDER BY expiry_date DESC');
|
|
return rows.map((row) => this.rowToCertificate(row));
|
|
}
|
|
|
|
updateCertificate(id: number, updates: Partial<ICertificate>): void {
|
|
if (!this.db) throw new Error('Database not initialized');
|
|
|
|
const fields: string[] = [];
|
|
const values: unknown[] = [];
|
|
|
|
if (updates.certDomain !== undefined) {
|
|
fields.push('cert_domain = ?');
|
|
values.push(updates.certDomain);
|
|
}
|
|
if (updates.isWildcard !== undefined) {
|
|
fields.push('is_wildcard = ?');
|
|
values.push(updates.isWildcard ? 1 : 0);
|
|
}
|
|
if (updates.certPath !== undefined) {
|
|
fields.push('cert_path = ?');
|
|
values.push(updates.certPath);
|
|
}
|
|
if (updates.keyPath !== undefined) {
|
|
fields.push('key_path = ?');
|
|
values.push(updates.keyPath);
|
|
}
|
|
if (updates.fullChainPath !== undefined) {
|
|
fields.push('full_chain_path = ?');
|
|
values.push(updates.fullChainPath);
|
|
}
|
|
if (updates.expiryDate !== undefined) {
|
|
fields.push('expiry_date = ?');
|
|
values.push(updates.expiryDate);
|
|
}
|
|
if (updates.issuer !== undefined) {
|
|
fields.push('issuer = ?');
|
|
values.push(updates.issuer);
|
|
}
|
|
if (updates.isValid !== undefined) {
|
|
fields.push('is_valid = ?');
|
|
values.push(updates.isValid ? 1 : 0);
|
|
}
|
|
|
|
fields.push('updated_at = ?');
|
|
values.push(Date.now());
|
|
values.push(id);
|
|
|
|
this.query(`UPDATE certificates SET ${fields.join(', ')} WHERE id = ?`, values);
|
|
}
|
|
|
|
deleteCertificate(id: number): void {
|
|
if (!this.db) throw new Error('Database not initialized');
|
|
this.query('DELETE FROM certificates WHERE id = ?', [id]);
|
|
}
|
|
|
|
private rowToCertificate(row: any): ICertificate {
|
|
return {
|
|
id: Number(row.id || row[0]),
|
|
domainId: Number(row.domain_id || row[1]),
|
|
certDomain: String(row.cert_domain || row[2]),
|
|
isWildcard: Boolean(row.is_wildcard || row[3]),
|
|
certPath: String(row.cert_path || row[4]),
|
|
keyPath: String(row.key_path || row[5]),
|
|
fullChainPath: String(row.full_chain_path || row[6]),
|
|
expiryDate: Number(row.expiry_date || row[7]),
|
|
issuer: String(row.issuer || row[8]),
|
|
isValid: Boolean(row.is_valid || row[9]),
|
|
createdAt: Number(row.created_at || row[10]),
|
|
updatedAt: Number(row.updated_at || row[11]),
|
|
};
|
|
}
|
|
|
|
// ============ Certificate Requirements ============
|
|
|
|
createCertRequirement(req: Omit<ICertRequirement, 'id'>): ICertRequirement {
|
|
if (!this.db) throw new Error('Database not initialized');
|
|
|
|
this.query(
|
|
`INSERT INTO cert_requirements (service_id, domain_id, subdomain, certificate_id, status, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
[
|
|
req.serviceId,
|
|
req.domainId,
|
|
req.subdomain,
|
|
req.certificateId,
|
|
req.status,
|
|
req.createdAt,
|
|
req.updatedAt,
|
|
]
|
|
);
|
|
|
|
const rows = this.query('SELECT * FROM cert_requirements WHERE id = last_insert_rowid()');
|
|
return this.rowToCertRequirement(rows[0]);
|
|
}
|
|
|
|
getCertRequirementById(id: number): ICertRequirement | null {
|
|
if (!this.db) throw new Error('Database not initialized');
|
|
|
|
const rows = this.query('SELECT * FROM cert_requirements WHERE id = ?', [id]);
|
|
return rows.length > 0 ? this.rowToCertRequirement(rows[0]) : null;
|
|
}
|
|
|
|
getCertRequirementsByService(serviceId: number): ICertRequirement[] {
|
|
if (!this.db) throw new Error('Database not initialized');
|
|
|
|
const rows = this.query('SELECT * FROM cert_requirements WHERE service_id = ?', [serviceId]);
|
|
return rows.map((row) => this.rowToCertRequirement(row));
|
|
}
|
|
|
|
getCertRequirementsByDomain(domainId: number): ICertRequirement[] {
|
|
if (!this.db) throw new Error('Database not initialized');
|
|
|
|
const rows = this.query('SELECT * FROM cert_requirements WHERE domain_id = ?', [domainId]);
|
|
return rows.map((row) => this.rowToCertRequirement(row));
|
|
}
|
|
|
|
getAllCertRequirements(): ICertRequirement[] {
|
|
if (!this.db) throw new Error('Database not initialized');
|
|
|
|
const rows = this.query('SELECT * FROM cert_requirements ORDER BY created_at DESC');
|
|
return rows.map((row) => this.rowToCertRequirement(row));
|
|
}
|
|
|
|
updateCertRequirement(id: number, updates: Partial<ICertRequirement>): void {
|
|
if (!this.db) throw new Error('Database not initialized');
|
|
|
|
const fields: string[] = [];
|
|
const values: unknown[] = [];
|
|
|
|
if (updates.subdomain !== undefined) {
|
|
fields.push('subdomain = ?');
|
|
values.push(updates.subdomain);
|
|
}
|
|
if (updates.certificateId !== undefined) {
|
|
fields.push('certificate_id = ?');
|
|
values.push(updates.certificateId);
|
|
}
|
|
if (updates.status !== undefined) {
|
|
fields.push('status = ?');
|
|
values.push(updates.status);
|
|
}
|
|
|
|
fields.push('updated_at = ?');
|
|
values.push(Date.now());
|
|
values.push(id);
|
|
|
|
this.query(`UPDATE cert_requirements SET ${fields.join(', ')} WHERE id = ?`, values);
|
|
}
|
|
|
|
deleteCertRequirement(id: number): void {
|
|
if (!this.db) throw new Error('Database not initialized');
|
|
this.query('DELETE FROM cert_requirements WHERE id = ?', [id]);
|
|
}
|
|
|
|
private rowToCertRequirement(row: any): ICertRequirement {
|
|
return {
|
|
id: Number(row.id || row[0]),
|
|
serviceId: Number(row.service_id || row[1]),
|
|
domainId: Number(row.domain_id || row[2]),
|
|
subdomain: String(row.subdomain || row[3]),
|
|
certificateId: row.certificate_id || row[4] || undefined,
|
|
status: String(row.status || row[5]) as ICertRequirement['status'],
|
|
createdAt: Number(row.created_at || row[6]),
|
|
updatedAt: Number(row.updated_at || row[7]),
|
|
};
|
|
}
|
|
|
|
// ============ Registry Tokens ============
|
|
|
|
createRegistryToken(token: Omit<IRegistryToken, 'id'>): IRegistryToken {
|
|
if (!this.db) throw new Error('Database not initialized');
|
|
|
|
const scopeJson = Array.isArray(token.scope) ? JSON.stringify(token.scope) : token.scope;
|
|
|
|
this.query(
|
|
`INSERT INTO registry_tokens (name, token_hash, token_type, scope, expires_at, created_at, last_used_at, created_by)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
[
|
|
token.name,
|
|
token.tokenHash,
|
|
token.type,
|
|
scopeJson,
|
|
token.expiresAt,
|
|
token.createdAt,
|
|
token.lastUsedAt,
|
|
token.createdBy,
|
|
]
|
|
);
|
|
|
|
const rows = this.query('SELECT * FROM registry_tokens WHERE id = last_insert_rowid()');
|
|
return this.rowToRegistryToken(rows[0]);
|
|
}
|
|
|
|
getRegistryTokenById(id: number): IRegistryToken | null {
|
|
if (!this.db) throw new Error('Database not initialized');
|
|
|
|
const rows = this.query('SELECT * FROM registry_tokens WHERE id = ?', [id]);
|
|
return rows.length > 0 ? this.rowToRegistryToken(rows[0]) : null;
|
|
}
|
|
|
|
getRegistryTokenByHash(tokenHash: string): IRegistryToken | null {
|
|
if (!this.db) throw new Error('Database not initialized');
|
|
|
|
const rows = this.query('SELECT * FROM registry_tokens WHERE token_hash = ?', [tokenHash]);
|
|
return rows.length > 0 ? this.rowToRegistryToken(rows[0]) : null;
|
|
}
|
|
|
|
getAllRegistryTokens(): IRegistryToken[] {
|
|
if (!this.db) throw new Error('Database not initialized');
|
|
|
|
const rows = this.query('SELECT * FROM registry_tokens ORDER BY created_at DESC');
|
|
return rows.map((row) => this.rowToRegistryToken(row));
|
|
}
|
|
|
|
getRegistryTokensByType(type: 'global' | 'ci'): IRegistryToken[] {
|
|
if (!this.db) throw new Error('Database not initialized');
|
|
|
|
const rows = this.query('SELECT * FROM registry_tokens WHERE token_type = ? ORDER BY created_at DESC', [type]);
|
|
return rows.map((row) => this.rowToRegistryToken(row));
|
|
}
|
|
|
|
updateRegistryTokenLastUsed(id: number): void {
|
|
if (!this.db) throw new Error('Database not initialized');
|
|
this.query('UPDATE registry_tokens SET last_used_at = ? WHERE id = ?', [Date.now(), id]);
|
|
}
|
|
|
|
deleteRegistryToken(id: number): void {
|
|
if (!this.db) throw new Error('Database not initialized');
|
|
this.query('DELETE FROM registry_tokens WHERE id = ?', [id]);
|
|
}
|
|
|
|
private rowToRegistryToken(row: any): IRegistryToken {
|
|
// Parse scope - it's either 'all' or a JSON array
|
|
let scope: 'all' | string[];
|
|
const scopeRaw = row.scope || row[4];
|
|
if (scopeRaw === 'all') {
|
|
scope = 'all';
|
|
} else {
|
|
try {
|
|
scope = JSON.parse(String(scopeRaw));
|
|
} catch {
|
|
scope = 'all';
|
|
}
|
|
}
|
|
|
|
return {
|
|
id: Number(row.id || row[0]),
|
|
name: String(row.name || row[1]),
|
|
tokenHash: String(row.token_hash || row[2]),
|
|
type: String(row.token_type || row[3]) as IRegistryToken['type'],
|
|
scope,
|
|
expiresAt: row.expires_at || row[5] ? Number(row.expires_at || row[5]) : null,
|
|
createdAt: Number(row.created_at || row[6]),
|
|
lastUsedAt: row.last_used_at || row[7] ? Number(row.last_used_at || row[7]) : null,
|
|
createdBy: String(row.created_by || row[8]),
|
|
};
|
|
}
|
|
|
|
// ============ Platform Services CRUD ============
|
|
|
|
createPlatformService(service: Omit<IPlatformService, 'id'>): IPlatformService {
|
|
if (!this.db) throw new Error('Database not initialized');
|
|
|
|
const now = Date.now();
|
|
this.query(
|
|
`INSERT INTO platform_services (name, type, status, container_id, config, admin_credentials_encrypted, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
[
|
|
service.name,
|
|
service.type,
|
|
service.status,
|
|
service.containerId || null,
|
|
JSON.stringify(service.config),
|
|
service.adminCredentialsEncrypted || null,
|
|
now,
|
|
now,
|
|
]
|
|
);
|
|
|
|
return this.getPlatformServiceByName(service.name)!;
|
|
}
|
|
|
|
getPlatformServiceByName(name: string): IPlatformService | null {
|
|
if (!this.db) throw new Error('Database not initialized');
|
|
|
|
const rows = this.query('SELECT * FROM platform_services WHERE name = ?', [name]);
|
|
return rows.length > 0 ? this.rowToPlatformService(rows[0]) : null;
|
|
}
|
|
|
|
getPlatformServiceById(id: number): IPlatformService | null {
|
|
if (!this.db) throw new Error('Database not initialized');
|
|
|
|
const rows = this.query('SELECT * FROM platform_services WHERE id = ?', [id]);
|
|
return rows.length > 0 ? this.rowToPlatformService(rows[0]) : null;
|
|
}
|
|
|
|
getPlatformServiceByType(type: TPlatformServiceType): IPlatformService | null {
|
|
if (!this.db) throw new Error('Database not initialized');
|
|
|
|
const rows = this.query('SELECT * FROM platform_services WHERE type = ?', [type]);
|
|
return rows.length > 0 ? this.rowToPlatformService(rows[0]) : null;
|
|
}
|
|
|
|
getAllPlatformServices(): IPlatformService[] {
|
|
if (!this.db) throw new Error('Database not initialized');
|
|
|
|
const rows = this.query('SELECT * FROM platform_services ORDER BY created_at DESC');
|
|
return rows.map((row) => this.rowToPlatformService(row));
|
|
}
|
|
|
|
updatePlatformService(id: number, updates: Partial<IPlatformService>): void {
|
|
if (!this.db) throw new Error('Database not initialized');
|
|
|
|
const fields: string[] = [];
|
|
const values: unknown[] = [];
|
|
|
|
if (updates.status !== undefined) {
|
|
fields.push('status = ?');
|
|
values.push(updates.status);
|
|
}
|
|
if (updates.containerId !== undefined) {
|
|
fields.push('container_id = ?');
|
|
values.push(updates.containerId);
|
|
}
|
|
if (updates.config !== undefined) {
|
|
fields.push('config = ?');
|
|
values.push(JSON.stringify(updates.config));
|
|
}
|
|
if (updates.adminCredentialsEncrypted !== undefined) {
|
|
fields.push('admin_credentials_encrypted = ?');
|
|
values.push(updates.adminCredentialsEncrypted);
|
|
}
|
|
|
|
fields.push('updated_at = ?');
|
|
values.push(Date.now());
|
|
values.push(id);
|
|
|
|
this.query(`UPDATE platform_services SET ${fields.join(', ')} WHERE id = ?`, values);
|
|
}
|
|
|
|
deletePlatformService(id: number): void {
|
|
if (!this.db) throw new Error('Database not initialized');
|
|
this.query('DELETE FROM platform_services WHERE id = ?', [id]);
|
|
}
|
|
|
|
private rowToPlatformService(row: any): IPlatformService {
|
|
let config = { image: '', port: 0 };
|
|
const configRaw = row.config;
|
|
if (configRaw) {
|
|
try {
|
|
config = JSON.parse(String(configRaw));
|
|
} catch (e) {
|
|
logger.warn(`Failed to parse platform service config: ${getErrorMessage(e)}`);
|
|
}
|
|
}
|
|
|
|
return {
|
|
id: Number(row.id),
|
|
name: String(row.name),
|
|
type: String(row.type) as TPlatformServiceType,
|
|
status: String(row.status) as IPlatformService['status'],
|
|
containerId: row.container_id ? String(row.container_id) : undefined,
|
|
config,
|
|
adminCredentialsEncrypted: row.admin_credentials_encrypted ? String(row.admin_credentials_encrypted) : undefined,
|
|
createdAt: Number(row.created_at),
|
|
updatedAt: Number(row.updated_at),
|
|
};
|
|
}
|
|
|
|
// ============ Platform Resources CRUD ============
|
|
|
|
createPlatformResource(resource: Omit<IPlatformResource, 'id'>): IPlatformResource {
|
|
if (!this.db) throw new Error('Database not initialized');
|
|
|
|
const now = Date.now();
|
|
this.query(
|
|
`INSERT INTO platform_resources (platform_service_id, service_id, resource_type, resource_name, credentials_encrypted, created_at)
|
|
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
[
|
|
resource.platformServiceId,
|
|
resource.serviceId,
|
|
resource.resourceType,
|
|
resource.resourceName,
|
|
resource.credentialsEncrypted,
|
|
now,
|
|
]
|
|
);
|
|
|
|
const rows = this.query('SELECT * FROM platform_resources WHERE id = last_insert_rowid()');
|
|
return this.rowToPlatformResource(rows[0]);
|
|
}
|
|
|
|
getPlatformResourceById(id: number): IPlatformResource | null {
|
|
if (!this.db) throw new Error('Database not initialized');
|
|
|
|
const rows = this.query('SELECT * FROM platform_resources WHERE id = ?', [id]);
|
|
return rows.length > 0 ? this.rowToPlatformResource(rows[0]) : null;
|
|
}
|
|
|
|
getPlatformResourcesByService(serviceId: number): IPlatformResource[] {
|
|
if (!this.db) throw new Error('Database not initialized');
|
|
|
|
const rows = this.query('SELECT * FROM platform_resources WHERE service_id = ?', [serviceId]);
|
|
return rows.map((row) => this.rowToPlatformResource(row));
|
|
}
|
|
|
|
getPlatformResourcesByPlatformService(platformServiceId: number): IPlatformResource[] {
|
|
if (!this.db) throw new Error('Database not initialized');
|
|
|
|
const rows = this.query('SELECT * FROM platform_resources WHERE platform_service_id = ?', [platformServiceId]);
|
|
return rows.map((row) => this.rowToPlatformResource(row));
|
|
}
|
|
|
|
getAllPlatformResources(): IPlatformResource[] {
|
|
if (!this.db) throw new Error('Database not initialized');
|
|
|
|
const rows = this.query('SELECT * FROM platform_resources ORDER BY created_at DESC');
|
|
return rows.map((row) => this.rowToPlatformResource(row));
|
|
}
|
|
|
|
deletePlatformResource(id: number): void {
|
|
if (!this.db) throw new Error('Database not initialized');
|
|
this.query('DELETE FROM platform_resources WHERE id = ?', [id]);
|
|
}
|
|
|
|
deletePlatformResourcesByService(serviceId: number): void {
|
|
if (!this.db) throw new Error('Database not initialized');
|
|
this.query('DELETE FROM platform_resources WHERE service_id = ?', [serviceId]);
|
|
}
|
|
|
|
private rowToPlatformResource(row: any): IPlatformResource {
|
|
return {
|
|
id: Number(row.id),
|
|
platformServiceId: Number(row.platform_service_id),
|
|
serviceId: Number(row.service_id),
|
|
resourceType: String(row.resource_type) as IPlatformResource['resourceType'],
|
|
resourceName: String(row.resource_name),
|
|
credentialsEncrypted: String(row.credentials_encrypted),
|
|
createdAt: Number(row.created_at),
|
|
};
|
|
}
|
|
}
|