644 lines
18 KiB
TypeScript
644 lines
18 KiB
TypeScript
/**
|
|
* Database layer for Onebox using SQLite
|
|
* Refactored into repository pattern
|
|
*/
|
|
|
|
import * as plugins from '../plugins.ts';
|
|
import type {
|
|
IService,
|
|
IRegistry,
|
|
IRegistryToken,
|
|
ISslCertificate,
|
|
IMetric,
|
|
ILogEntry,
|
|
IUser,
|
|
IPlatformService,
|
|
IPlatformResource,
|
|
TPlatformServiceType,
|
|
IDomain,
|
|
ICertificate,
|
|
ICertRequirement,
|
|
IBackup,
|
|
IBackupSchedule,
|
|
IBackupScheduleUpdate,
|
|
} from '../types.ts';
|
|
import type { TBindValue } from './types.ts';
|
|
import { logger } from '../logging.ts';
|
|
import { getErrorMessage } from '../utils/error.ts';
|
|
import { MigrationRunner } from './migrations/index.ts';
|
|
|
|
// Import repositories
|
|
import {
|
|
ServiceRepository,
|
|
RegistryRepository,
|
|
CertificateRepository,
|
|
AuthRepository,
|
|
MetricsRepository,
|
|
PlatformRepository,
|
|
BackupRepository,
|
|
} from './repositories/index.ts';
|
|
|
|
export class OneboxDatabase {
|
|
private db: InstanceType<typeof plugins.sqlite.DB> | null = null;
|
|
private dbPath: string;
|
|
|
|
// Repositories
|
|
private serviceRepo!: ServiceRepository;
|
|
private registryRepo!: RegistryRepository;
|
|
private certificateRepo!: CertificateRepository;
|
|
private authRepo!: AuthRepository;
|
|
private metricsRepo!: MetricsRepository;
|
|
private platformRepo!: PlatformRepository;
|
|
private backupRepo!: BackupRepository;
|
|
|
|
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
|
|
const runner = new MigrationRunner(this.query.bind(this));
|
|
runner.run();
|
|
|
|
// Initialize repositories with bound query function
|
|
const queryFn = this.query.bind(this);
|
|
this.serviceRepo = new ServiceRepository(queryFn);
|
|
this.registryRepo = new RegistryRepository(queryFn);
|
|
this.certificateRepo = new CertificateRepository(queryFn);
|
|
this.authRepo = new AuthRepository(queryFn);
|
|
this.metricsRepo = new MetricsRepository(queryFn);
|
|
this.platformRepo = new PlatformRepository(queryFn);
|
|
this.backupRepo = new BackupRepository(queryFn);
|
|
} 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
|
|
*/
|
|
/**
|
|
* 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: TBindValue[] = []): 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;
|
|
}
|
|
|
|
if (params.length === 0 && !sql.trim().toUpperCase().startsWith('SELECT')) {
|
|
this.db.exec(sql);
|
|
return [] as T[];
|
|
}
|
|
|
|
const stmt = this.db.prepare(sql);
|
|
try {
|
|
const result = stmt.all(...params);
|
|
return result as T[];
|
|
} finally {
|
|
stmt.finalize();
|
|
}
|
|
}
|
|
|
|
// ============ Services CRUD (delegated to repository) ============
|
|
|
|
async createService(service: Omit<IService, 'id'>): Promise<IService> {
|
|
return this.serviceRepo.create(service);
|
|
}
|
|
|
|
getServiceByName(name: string): IService | null {
|
|
return this.serviceRepo.getByName(name);
|
|
}
|
|
|
|
getServiceByID(id: number): IService | null {
|
|
return this.serviceRepo.getById(id);
|
|
}
|
|
|
|
getAllServices(): IService[] {
|
|
return this.serviceRepo.getAll();
|
|
}
|
|
|
|
updateService(id: number, updates: Partial<IService>): void {
|
|
this.serviceRepo.update(id, updates);
|
|
}
|
|
|
|
deleteService(id: number): void {
|
|
this.serviceRepo.delete(id);
|
|
}
|
|
|
|
// ============ Registries CRUD (delegated to repository) ============
|
|
|
|
async createRegistry(registry: Omit<IRegistry, 'id'>): Promise<IRegistry> {
|
|
return this.registryRepo.createRegistry(registry);
|
|
}
|
|
|
|
getRegistryByURL(url: string): IRegistry | null {
|
|
return this.registryRepo.getRegistryByURL(url);
|
|
}
|
|
|
|
getAllRegistries(): IRegistry[] {
|
|
return this.registryRepo.getAllRegistries();
|
|
}
|
|
|
|
deleteRegistry(url: string): void {
|
|
this.registryRepo.deleteRegistry(url);
|
|
}
|
|
|
|
// ============ Settings CRUD (delegated to repository) ============
|
|
|
|
getSetting(key: string): string | null {
|
|
return this.authRepo.getSetting(key);
|
|
}
|
|
|
|
setSetting(key: string, value: string): void {
|
|
this.authRepo.setSetting(key, value);
|
|
}
|
|
|
|
getAllSettings(): Record<string, string> {
|
|
return this.authRepo.getAllSettings();
|
|
}
|
|
|
|
// ============ Users CRUD (delegated to repository) ============
|
|
|
|
async createUser(user: Omit<IUser, 'id'>): Promise<IUser> {
|
|
return this.authRepo.createUser(user);
|
|
}
|
|
|
|
getUserByUsername(username: string): IUser | null {
|
|
return this.authRepo.getUserByUsername(username);
|
|
}
|
|
|
|
getAllUsers(): IUser[] {
|
|
return this.authRepo.getAllUsers();
|
|
}
|
|
|
|
updateUserPassword(username: string, passwordHash: string): void {
|
|
this.authRepo.updateUserPassword(username, passwordHash);
|
|
}
|
|
|
|
deleteUser(username: string): void {
|
|
this.authRepo.deleteUser(username);
|
|
}
|
|
|
|
// ============ Metrics (delegated to repository) ============
|
|
|
|
addMetric(metric: Omit<IMetric, 'id'>): void {
|
|
this.metricsRepo.addMetric(metric);
|
|
}
|
|
|
|
getMetrics(serviceId: number, limit = 100): IMetric[] {
|
|
return this.metricsRepo.getMetrics(serviceId, limit);
|
|
}
|
|
|
|
// ============ Logs (delegated to repository) ============
|
|
|
|
addLog(log: Omit<ILogEntry, 'id'>): void {
|
|
this.metricsRepo.addLog(log);
|
|
}
|
|
|
|
getLogs(serviceId: number, limit = 1000): ILogEntry[] {
|
|
return this.metricsRepo.getLogs(serviceId, limit);
|
|
}
|
|
|
|
// ============ SSL Certificates Legacy API (delegated to repository) ============
|
|
|
|
async createSSLCertificate(cert: Omit<ISslCertificate, 'id'>): Promise<ISslCertificate> {
|
|
return this.certificateRepo.createSSLCertificate(cert);
|
|
}
|
|
|
|
getSSLCertificate(domain: string): ISslCertificate | null {
|
|
return this.certificateRepo.getSSLCertificate(domain);
|
|
}
|
|
|
|
getAllSSLCertificates(): ISslCertificate[] {
|
|
return this.certificateRepo.getAllSSLCertificates();
|
|
}
|
|
|
|
updateSSLCertificate(domain: string, updates: Partial<ISslCertificate>): void {
|
|
this.certificateRepo.updateSSLCertificate(domain, updates);
|
|
}
|
|
|
|
deleteSSLCertificate(domain: string): void {
|
|
this.certificateRepo.deleteSSLCertificate(domain);
|
|
}
|
|
|
|
// ============ Domains (delegated to repository) ============
|
|
|
|
createDomain(domain: Omit<IDomain, 'id'>): IDomain {
|
|
return this.certificateRepo.createDomain(domain);
|
|
}
|
|
|
|
getDomainByName(domain: string): IDomain | null {
|
|
return this.certificateRepo.getDomainByName(domain);
|
|
}
|
|
|
|
getDomainById(id: number): IDomain | null {
|
|
return this.certificateRepo.getDomainById(id);
|
|
}
|
|
|
|
getAllDomains(): IDomain[] {
|
|
return this.certificateRepo.getAllDomains();
|
|
}
|
|
|
|
getDomainsByProvider(provider: 'cloudflare' | 'manual'): IDomain[] {
|
|
return this.certificateRepo.getDomainsByProvider(provider);
|
|
}
|
|
|
|
updateDomain(id: number, updates: Partial<IDomain>): void {
|
|
this.certificateRepo.updateDomain(id, updates);
|
|
}
|
|
|
|
deleteDomain(id: number): void {
|
|
this.certificateRepo.deleteDomain(id);
|
|
}
|
|
|
|
// ============ Certificates (delegated to repository) ============
|
|
|
|
createCertificate(cert: Omit<ICertificate, 'id'>): ICertificate {
|
|
return this.certificateRepo.createCertificate(cert);
|
|
}
|
|
|
|
getCertificateById(id: number): ICertificate | null {
|
|
return this.certificateRepo.getCertificateById(id);
|
|
}
|
|
|
|
getCertificatesByDomain(domainId: number): ICertificate[] {
|
|
return this.certificateRepo.getCertificatesByDomain(domainId);
|
|
}
|
|
|
|
getAllCertificates(): ICertificate[] {
|
|
return this.certificateRepo.getAllCertificates();
|
|
}
|
|
|
|
updateCertificate(id: number, updates: Partial<ICertificate>): void {
|
|
this.certificateRepo.updateCertificate(id, updates);
|
|
}
|
|
|
|
deleteCertificate(id: number): void {
|
|
this.certificateRepo.deleteCertificate(id);
|
|
}
|
|
|
|
// ============ Certificate Requirements (delegated to repository) ============
|
|
|
|
createCertRequirement(req: Omit<ICertRequirement, 'id'>): ICertRequirement {
|
|
return this.certificateRepo.createCertRequirement(req);
|
|
}
|
|
|
|
getCertRequirementById(id: number): ICertRequirement | null {
|
|
return this.certificateRepo.getCertRequirementById(id);
|
|
}
|
|
|
|
getCertRequirementsByService(serviceId: number): ICertRequirement[] {
|
|
return this.certificateRepo.getCertRequirementsByService(serviceId);
|
|
}
|
|
|
|
getCertRequirementsByDomain(domainId: number): ICertRequirement[] {
|
|
return this.certificateRepo.getCertRequirementsByDomain(domainId);
|
|
}
|
|
|
|
getAllCertRequirements(): ICertRequirement[] {
|
|
return this.certificateRepo.getAllCertRequirements();
|
|
}
|
|
|
|
updateCertRequirement(id: number, updates: Partial<ICertRequirement>): void {
|
|
this.certificateRepo.updateCertRequirement(id, updates);
|
|
}
|
|
|
|
deleteCertRequirement(id: number): void {
|
|
this.certificateRepo.deleteCertRequirement(id);
|
|
}
|
|
|
|
// ============ Registry Tokens (delegated to repository) ============
|
|
|
|
createRegistryToken(token: Omit<IRegistryToken, 'id'>): IRegistryToken {
|
|
return this.registryRepo.createToken(token);
|
|
}
|
|
|
|
getRegistryTokenById(id: number): IRegistryToken | null {
|
|
return this.registryRepo.getTokenById(id);
|
|
}
|
|
|
|
getRegistryTokenByHash(tokenHash: string): IRegistryToken | null {
|
|
return this.registryRepo.getTokenByHash(tokenHash);
|
|
}
|
|
|
|
getAllRegistryTokens(): IRegistryToken[] {
|
|
return this.registryRepo.getAllTokens();
|
|
}
|
|
|
|
getRegistryTokensByType(type: 'global' | 'ci'): IRegistryToken[] {
|
|
return this.registryRepo.getTokensByType(type);
|
|
}
|
|
|
|
updateRegistryTokenLastUsed(id: number): void {
|
|
this.registryRepo.updateTokenLastUsed(id);
|
|
}
|
|
|
|
deleteRegistryToken(id: number): void {
|
|
this.registryRepo.deleteToken(id);
|
|
}
|
|
|
|
// ============ Platform Services (delegated to repository) ============
|
|
|
|
createPlatformService(service: Omit<IPlatformService, 'id'>): IPlatformService {
|
|
return this.platformRepo.createPlatformService(service);
|
|
}
|
|
|
|
getPlatformServiceByName(name: string): IPlatformService | null {
|
|
return this.platformRepo.getPlatformServiceByName(name);
|
|
}
|
|
|
|
getPlatformServiceById(id: number): IPlatformService | null {
|
|
return this.platformRepo.getPlatformServiceById(id);
|
|
}
|
|
|
|
getPlatformServiceByType(type: TPlatformServiceType): IPlatformService | null {
|
|
return this.platformRepo.getPlatformServiceByType(type);
|
|
}
|
|
|
|
getAllPlatformServices(): IPlatformService[] {
|
|
return this.platformRepo.getAllPlatformServices();
|
|
}
|
|
|
|
updatePlatformService(id: number, updates: Partial<IPlatformService>): void {
|
|
this.platformRepo.updatePlatformService(id, updates);
|
|
}
|
|
|
|
deletePlatformService(id: number): void {
|
|
this.platformRepo.deletePlatformService(id);
|
|
}
|
|
|
|
// ============ Platform Resources (delegated to repository) ============
|
|
|
|
createPlatformResource(resource: Omit<IPlatformResource, 'id'>): IPlatformResource {
|
|
return this.platformRepo.createPlatformResource(resource);
|
|
}
|
|
|
|
getPlatformResourceById(id: number): IPlatformResource | null {
|
|
return this.platformRepo.getPlatformResourceById(id);
|
|
}
|
|
|
|
getPlatformResourcesByService(serviceId: number): IPlatformResource[] {
|
|
return this.platformRepo.getPlatformResourcesByService(serviceId);
|
|
}
|
|
|
|
getPlatformResourcesByPlatformService(platformServiceId: number): IPlatformResource[] {
|
|
return this.platformRepo.getPlatformResourcesByPlatformService(platformServiceId);
|
|
}
|
|
|
|
getAllPlatformResources(): IPlatformResource[] {
|
|
return this.platformRepo.getAllPlatformResources();
|
|
}
|
|
|
|
deletePlatformResource(id: number): void {
|
|
this.platformRepo.deletePlatformResource(id);
|
|
}
|
|
|
|
deletePlatformResourcesByService(serviceId: number): void {
|
|
this.platformRepo.deletePlatformResourcesByService(serviceId);
|
|
}
|
|
|
|
// ============ Backups (delegated to repository) ============
|
|
|
|
createBackup(backup: Omit<IBackup, 'id'>): IBackup {
|
|
return this.backupRepo.create(backup);
|
|
}
|
|
|
|
getBackupById(id: number): IBackup | null {
|
|
return this.backupRepo.getById(id);
|
|
}
|
|
|
|
getBackupsByService(serviceId: number): IBackup[] {
|
|
return this.backupRepo.getByService(serviceId);
|
|
}
|
|
|
|
getAllBackups(): IBackup[] {
|
|
return this.backupRepo.getAll();
|
|
}
|
|
|
|
deleteBackup(id: number): void {
|
|
this.backupRepo.delete(id);
|
|
}
|
|
|
|
deleteBackupsByService(serviceId: number): void {
|
|
this.backupRepo.deleteByService(serviceId);
|
|
}
|
|
|
|
getBackupsBySchedule(scheduleId: number): IBackup[] {
|
|
return this.backupRepo.getBySchedule(scheduleId);
|
|
}
|
|
|
|
// ============ Backup Schedules (delegated to repository) ============
|
|
|
|
createBackupSchedule(schedule: Omit<IBackupSchedule, 'id'>): IBackupSchedule {
|
|
return this.backupRepo.createSchedule(schedule);
|
|
}
|
|
|
|
getBackupScheduleById(id: number): IBackupSchedule | null {
|
|
return this.backupRepo.getScheduleById(id);
|
|
}
|
|
|
|
getBackupSchedulesByService(serviceId: number): IBackupSchedule[] {
|
|
return this.backupRepo.getSchedulesByService(serviceId);
|
|
}
|
|
|
|
getEnabledBackupSchedules(): IBackupSchedule[] {
|
|
return this.backupRepo.getEnabledSchedules();
|
|
}
|
|
|
|
getAllBackupSchedules(): IBackupSchedule[] {
|
|
return this.backupRepo.getAllSchedules();
|
|
}
|
|
|
|
updateBackupSchedule(id: number, updates: IBackupScheduleUpdate & { lastRunAt?: number; nextRunAt?: number; lastStatus?: 'success' | 'failed' | null; lastError?: string | null }): void {
|
|
this.backupRepo.updateSchedule(id, updates);
|
|
}
|
|
|
|
deleteBackupSchedule(id: number): void {
|
|
this.backupRepo.deleteSchedule(id);
|
|
}
|
|
|
|
deleteBackupSchedulesByService(serviceId: number): void {
|
|
this.backupRepo.deleteSchedulesByService(serviceId);
|
|
}
|
|
}
|