import db from '../config/database'; import { logger } from '../utils/logger'; import * as fs from 'fs'; import * as path from 'path'; export interface BackupConfig { tenantId: string; shopId: string; taskId: string; traceId: string; businessType: 'TOC' | 'TOB'; backupType: 'FULL' | 'INCREMENTAL' | 'DIFFERENTIAL'; tables?: string[]; excludeTables?: string[]; compression: boolean; encryption: boolean; retentionDays: number; schedule?: { enabled: boolean; cron: string; }; } export interface BackupResult { success: boolean; backupId: string; backupType: string; filePath: string; fileSize: number; tablesBackedUp: number; recordsCount: number; checksum: string; startedAt: Date; completedAt: Date; duration: number; } export interface RestoreConfig { tenantId: string; shopId: string; taskId: string; traceId: string; businessType: 'TOC' | 'TOB'; backupId: string; targetTables?: string[]; verifyAfterRestore: boolean; } export interface RestoreResult { success: boolean; backupId: string; tablesRestored: number; recordsRestored: number; startedAt: Date; completedAt: Date; duration: number; verificationResult?: { success: boolean; checksumMatch: boolean; recordCountMatch: boolean; }; } export interface MonitoringConfig { tenantId: string; shopId: string; taskId: string; traceId: string; businessType: 'TOC' | 'TOB'; metrics: MetricConfig[]; alertChannels: AlertChannelConfig[]; } export interface MetricConfig { metricId: string; metricName: string; metricType: 'CPU' | 'MEMORY' | 'DISK' | 'NETWORK' | 'DATABASE' | 'API_LATENCY' | 'ERROR_RATE' | 'CUSTOM'; threshold: { warning: number; critical: number; }; evaluationWindow: number; enabled: boolean; } export interface AlertChannelConfig { channelId: string; channelType: 'EMAIL' | 'SLACK' | 'WEBHOOK' | 'SMS'; config: Record; enabled: boolean; } export interface AlertRule { ruleId: string; ruleName: string; metricId: string; condition: 'GT' | 'LT' | 'EQ' | 'GTE' | 'LTE'; threshold: number; severity: 'WARNING' | 'CRITICAL' | 'INFO'; channels: string[]; cooldownPeriod: number; enabled: boolean; } export interface MonitoringResult { success: boolean; configId: string; metricsConfigured: number; alertRulesConfigured: number; channelsConfigured: number; createdAt: Date; } export interface BackupVerificationConfig { tenantId: string; shopId: string; taskId: string; traceId: string; businessType: 'TOC' | 'TOB'; backupId: string; verifyChecksum: boolean; verifyRecordCount: boolean; sampleVerification: boolean; sampleSize?: number; } export interface BackupVerificationResult { success: boolean; backupId: string; checksumValid: boolean; recordCountValid: boolean; sampleVerificationResult?: { samplesChecked: number; samplesValid: number; accuracy: number; }; startedAt: Date; completedAt: Date; duration: number; } export class DevOpsService { private static readonly BACKUP_DIR = path.join(process.cwd(), 'backups'); static async createBackup(config: BackupConfig): Promise { const { tenantId, shopId, taskId, traceId, businessType, backupType, tables, compression, encryption } = config; logger.info(`[DevOpsService] Creating backup - type: ${backupType}, tenantId: ${tenantId}, traceId: ${traceId}`); const backupId = `BK-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; const startedAt = new Date(); try { if (!fs.existsSync(this.BACKUP_DIR)) { fs.mkdirSync(this.BACKUP_DIR, { recursive: true }); } const tablesToBackup = tables || await this.getAllTables(); let recordsCount = 0; const backupData: Record = {}; for (const table of tablesToBackup) { if (config.excludeTables?.includes(table)) continue; const data = await db(table).select('*'); backupData[table] = data; recordsCount += data.length; } const fileName = `${backupId}.json`; const filePath = path.join(this.BACKUP_DIR, fileName); let content = JSON.stringify(backupData, null, 2); if (compression) { const zlib = require('zlib'); content = zlib.gzipSync(content).toString('base64'); } if (encryption) { const crypto = require('crypto'); const key = process.env.BACKUP_ENCRYPTION_KEY || 'default-key-32-chars-long!!!!!'; // 32 chars for AES-256 const iv = crypto.randomBytes(16); const cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(key), iv); let encrypted = cipher.update(content, 'utf8', 'hex'); encrypted += cipher.final('hex'); content = iv.toString('hex') + ':' + encrypted; } fs.writeFileSync(filePath, content); const checksum = this.calculateChecksum(content); const fileSize = fs.statSync(filePath).size; const completedAt = new Date(); const duration = completedAt.getTime() - startedAt.getTime(); await db('cf_backups').insert({ id: backupId, tenant_id: tenantId, shop_id: shopId, task_id: taskId, trace_id: traceId, business_type: businessType, backup_type: backupType, file_path: filePath, file_size: fileSize, tables_backed_up: Object.keys(backupData).length, records_count: recordsCount, checksum, compression, encryption, retention_days: config.retentionDays, started_at: startedAt, completed_at: completedAt, duration, status: 'COMPLETED', }); logger.info(`[DevOpsService] Backup completed - backupId: ${backupId}, tables: ${Object.keys(backupData).length}, records: ${recordsCount}`); return { success: true, backupId, backupType, filePath, fileSize, tablesBackedUp: Object.keys(backupData).length, recordsCount, checksum, startedAt, completedAt, duration, }; } catch (error: any) { logger.error(`[DevOpsService] Backup failed - backupId: ${backupId}, error: ${error.message}`); await db('cf_backups').insert({ id: backupId, tenant_id: tenantId, shop_id: shopId, task_id: taskId, trace_id: traceId, business_type: businessType, backup_type: backupType, started_at: startedAt, status: 'FAILED', error_message: error.message, }); throw error; } } static async restoreBackup(config: RestoreConfig): Promise { const { tenantId, shopId, taskId, traceId, businessType, backupId, targetTables, verifyAfterRestore } = config; logger.info(`[DevOpsService] Restoring backup - backupId: ${backupId}, tenantId: ${tenantId}, traceId: ${traceId}`); const startedAt = new Date(); try { const backup = await db('cf_backups') .where({ id: backupId, tenant_id: tenantId }) .first(); if (!backup) { throw new Error(`Backup not found: ${backupId}`); } if (!fs.existsSync(backup.file_path)) { throw new Error(`Backup file not found: ${backup.file_path}`); } let content = fs.readFileSync(backup.file_path, 'utf8'); if (backup.encryption) { const crypto = require('crypto'); const key = process.env.BACKUP_ENCRYPTION_KEY || 'default-key-32-chars-long!!!!!'; const parts = content.split(':'); const iv = Buffer.from(parts[0], 'hex'); const encrypted = parts[1]; const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(key), iv); let decrypted = decipher.update(encrypted, 'hex', 'utf8'); decrypted += decipher.final('utf8'); content = decrypted; } if (backup.compression) { const zlib = require('zlib'); content = zlib.gunzipSync(Buffer.from(content, 'base64')).toString(); } const backupData: Record = JSON.parse(content); const tablesToRestore = targetTables || Object.keys(backupData); let recordsRestored = 0; for (const table of tablesToRestore) { if (!backupData[table]) continue; await db(table).del(); const batchSize = 1000; const data = backupData[table]; for (let i = 0; i < data.length; i += batchSize) { const batch = data.slice(i, i + batchSize); await db(table).insert(batch); } recordsRestored += data.length; } const completedAt = new Date(); const duration = completedAt.getTime() - startedAt.getTime(); let verificationResult; if (verifyAfterRestore) { verificationResult = await this.verifyBackup({ tenantId, shopId, taskId, traceId, businessType, backupId, verifyChecksum: true, verifyRecordCount: true, sampleVerification: false, }); } await db('cf_restore_logs').insert({ id: `RS-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, tenant_id: tenantId, shop_id: shopId, task_id: taskId, trace_id: traceId, business_type: businessType, backup_id: backupId, tables_restored: tablesToRestore.length, records_restored: recordsRestored, started_at: startedAt, completed_at: completedAt, duration, status: 'COMPLETED', }); logger.info(`[DevOpsService] Restore completed - backupId: ${backupId}, tables: ${tablesToRestore.length}, records: ${recordsRestored}`); return { success: true, backupId, tablesRestored: tablesToRestore.length, recordsRestored, startedAt, completedAt, duration, verificationResult: verificationResult ? { success: verificationResult.success, checksumMatch: verificationResult.checksumValid, recordCountMatch: verificationResult.recordCountValid, } : undefined, }; } catch (error: any) { logger.error(`[DevOpsService] Restore failed - backupId: ${backupId}, error: ${error.message}`); throw error; } } static async configureMonitoring(config: MonitoringConfig): Promise { const { tenantId, shopId, taskId, traceId, businessType, metrics, alertChannels } = config; logger.info(`[DevOpsService] Configuring monitoring - tenantId: ${tenantId}, metrics: ${metrics.length}, channels: ${alertChannels.length}, traceId: ${traceId}`); const configId = `MON-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; try { await db('cf_monitoring_configs').insert({ id: configId, tenant_id: tenantId, shop_id: shopId, task_id: taskId, trace_id: traceId, business_type: businessType, created_at: new Date(), }); for (const metric of metrics) { await db('cf_monitoring_metrics').insert({ id: metric.metricId, config_id: configId, metric_name: metric.metricName, metric_type: metric.metricType, threshold_warning: metric.threshold.warning, threshold_critical: metric.threshold.critical, evaluation_window: metric.evaluationWindow, enabled: metric.enabled, }); } for (const channel of alertChannels) { await db('cf_alert_channels').insert({ id: channel.channelId, config_id: configId, channel_type: channel.channelType, config: JSON.stringify(channel.config), enabled: channel.enabled, }); } logger.info(`[DevOpsService] Monitoring configured - configId: ${configId}, metrics: ${metrics.length}, channels: ${alertChannels.length}`); return { success: true, configId, metricsConfigured: metrics.length, alertRulesConfigured: metrics.filter(m => m.enabled).length, channelsConfigured: alertChannels.filter(c => c.enabled).length, createdAt: new Date(), }; } catch (error: any) { logger.error(`[DevOpsService] Monitoring configuration failed - error: ${error.message}`); throw error; } } static async verifyBackup(config: BackupVerificationConfig): Promise { const { tenantId, shopId, taskId, traceId, businessType, backupId, verifyChecksum, verifyRecordCount, sampleVerification, sampleSize } = config; logger.info(`[DevOpsService] Verifying backup - backupId: ${backupId}, tenantId: ${tenantId}, traceId: ${traceId}`); const startedAt = new Date(); try { const backup = await db('cf_backups') .where({ id: backupId, tenant_id: tenantId }) .first(); if (!backup) { throw new Error(`Backup not found: ${backupId}`); } if (!fs.existsSync(backup.file_path)) { throw new Error(`Backup file not found: ${backup.file_path}`); } let content = fs.readFileSync(backup.file_path, 'utf8'); if (backup.encryption) { const crypto = require('crypto'); const key = process.env.BACKUP_ENCRYPTION_KEY || 'default-key-32-chars-long!!!!!'; const parts = content.split(':'); const iv = Buffer.from(parts[0], 'hex'); const encrypted = parts[1]; const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(key), iv); let decrypted = decipher.update(encrypted, 'hex', 'utf8'); decrypted += decipher.final('utf8'); content = decrypted; } if (backup.compression) { const zlib = require('zlib'); content = zlib.gunzipSync(Buffer.from(content, 'base64')).toString(); } const checksumValid = verifyChecksum ? this.calculateChecksum(content) === backup.checksum : true; const backupData: Record = JSON.parse(content); let totalRecords = 0; for (const table of Object.keys(backupData)) { totalRecords += backupData[table].length; } const recordCountValid = verifyRecordCount ? totalRecords === backup.records_count : true; let sampleVerificationResult; if (sampleVerification && sampleSize) { let samplesChecked = 0; let samplesValid = 0; for (const table of Object.keys(backupData)) { const data = backupData[table]; const sampleIndices = this.getRandomIndices(data.length, Math.min(sampleSize, data.length)); for (const index of sampleIndices) { samplesChecked++; const record = data[index]; if (record && typeof record === 'object') { samplesValid++; } } } sampleVerificationResult = { samplesChecked, samplesValid, accuracy: samplesChecked > 0 ? (samplesValid / samplesChecked) * 100 : 0, }; } const completedAt = new Date(); const duration = completedAt.getTime() - startedAt.getTime(); await db('cf_backup_verifications').insert({ id: `BV-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, tenant_id: tenantId, shop_id: shopId, task_id: taskId, trace_id: traceId, business_type: businessType, backup_id: backupId, checksum_valid: checksumValid, record_count_valid: recordCountValid, sample_verification_result: sampleVerificationResult ? JSON.stringify(sampleVerificationResult) : null, started_at: startedAt, completed_at: completedAt, duration, }); logger.info(`[DevOpsService] Backup verification completed - backupId: ${backupId}, checksumValid: ${checksumValid}, recordCountValid: ${recordCountValid}`); return { success: checksumValid && recordCountValid, backupId, checksumValid, recordCountValid, sampleVerificationResult, startedAt, completedAt, duration, }; } catch (error: any) { logger.error(`[DevOpsService] Backup verification failed - backupId: ${backupId}, error: ${error.message}`); throw error; } } private static async getAllTables(): Promise { const tables = await db.raw("SHOW TABLES"); const dbName = Object.keys(tables[0])[0]; return tables.map((row: any) => row[dbName]).filter((name: string) => name.startsWith('cf_')); } private static calculateChecksum(content: string): string { const crypto = require('crypto'); return crypto.createHash('sha256').update(content).digest('hex'); } private static getRandomIndices(max: number, count: number): number[] { const indices = new Set(); while (indices.size < count && indices.size < max) { indices.add(Math.floor(Math.random() * max)); } return Array.from(indices); } static async initializeTables(): Promise { if (!(await db.schema.hasTable('cf_backups'))) { await db.schema.createTable('cf_backups', (table) => { table.string('id').primary(); table.string('tenant_id').notNullable(); table.string('shop_id').notNullable(); table.string('task_id').notNullable(); table.string('trace_id').notNullable(); table.enum('business_type', ['TOC', 'TOB']).notNullable(); table.enum('backup_type', ['FULL', 'INCREMENTAL', 'DIFFERENTIAL']).notNullable(); table.string('file_path').notNullable(); table.bigInteger('file_size'); table.integer('tables_backed_up'); table.integer('records_count'); table.string('checksum'); table.boolean('compression').defaultTo(false); table.boolean('encryption').defaultTo(false); table.integer('retention_days').defaultTo(30); table.timestamp('started_at').notNullable(); table.timestamp('completed_at'); table.integer('duration'); table.enum('status', ['PENDING', 'RUNNING', 'COMPLETED', 'FAILED']).defaultTo('PENDING'); table.text('error_message'); table.index(['tenant_id', 'shop_id']); table.index(['status']); table.index(['created_at']); }); logger.info('[DevOpsService] Created cf_backups table'); } if (!(await db.schema.hasTable('cf_restore_logs'))) { await db.schema.createTable('cf_restore_logs', (table) => { table.string('id').primary(); table.string('tenant_id').notNullable(); table.string('shop_id').notNullable(); table.string('task_id').notNullable(); table.string('trace_id').notNullable(); table.enum('business_type', ['TOC', 'TOB']).notNullable(); table.string('backup_id').notNullable(); table.integer('tables_restored'); table.integer('records_restored'); table.timestamp('started_at').notNullable(); table.timestamp('completed_at'); table.integer('duration'); table.enum('status', ['PENDING', 'RUNNING', 'COMPLETED', 'FAILED']).defaultTo('PENDING'); table.text('error_message'); table.index(['tenant_id', 'shop_id']); table.index(['backup_id']); }); logger.info('[DevOpsService] Created cf_restore_logs table'); } if (!(await db.schema.hasTable('cf_monitoring_configs'))) { await db.schema.createTable('cf_monitoring_configs', (table) => { table.string('id').primary(); table.string('tenant_id').notNullable(); table.string('shop_id').notNullable(); table.string('task_id').notNullable(); table.string('trace_id').notNullable(); table.enum('business_type', ['TOC', 'TOB']).notNullable(); table.timestamp('created_at').defaultTo(db.fn.now()); table.index(['tenant_id', 'shop_id']); }); logger.info('[DevOpsService] Created cf_monitoring_configs table'); } if (!(await db.schema.hasTable('cf_monitoring_metrics'))) { await db.schema.createTable('cf_monitoring_metrics', (table) => { table.string('id').primary(); table.string('config_id').notNullable(); table.string('metric_name').notNullable(); table.enum('metric_type', ['CPU', 'MEMORY', 'DISK', 'NETWORK', 'DATABASE', 'API_LATENCY', 'ERROR_RATE', 'CUSTOM']).notNullable(); table.decimal('threshold_warning', 10, 2); table.decimal('threshold_critical', 10, 2); table.integer('evaluation_window').defaultTo(300); table.boolean('enabled').defaultTo(true); table.foreign('config_id').references('cf_monitoring_configs.id'); table.index(['config_id']); }); logger.info('[DevOpsService] Created cf_monitoring_metrics table'); } if (!(await db.schema.hasTable('cf_alert_channels'))) { await db.schema.createTable('cf_alert_channels', (table) => { table.string('id').primary(); table.string('config_id').notNullable(); table.enum('channel_type', ['EMAIL', 'SLACK', 'WEBHOOK', 'SMS']).notNullable(); table.json('config').notNullable(); table.boolean('enabled').defaultTo(true); table.foreign('config_id').references('cf_monitoring_configs.id'); table.index(['config_id']); }); logger.info('[DevOpsService] Created cf_alert_channels table'); } if (!(await db.schema.hasTable('cf_backup_verifications'))) { await db.schema.createTable('cf_backup_verifications', (table) => { table.string('id').primary(); table.string('tenant_id').notNullable(); table.string('shop_id').notNullable(); table.string('task_id').notNullable(); table.string('trace_id').notNullable(); table.enum('business_type', ['TOC', 'TOB']).notNullable(); table.string('backup_id').notNullable(); table.boolean('checksum_valid'); table.boolean('record_count_valid'); table.json('sample_verification_result'); table.timestamp('started_at').notNullable(); table.timestamp('completed_at'); table.integer('duration'); table.index(['tenant_id', 'shop_id']); table.index(['backup_id']); }); logger.info('[DevOpsService] Created cf_backup_verifications table'); } } }