/** * @ai-summary Controller for backup API endpoints * @ai-context Handles HTTP requests for backup operations */ import { FastifyRequest, FastifyReply } from 'fastify'; import { Pool } from 'pg'; import { logger } from '../../../core/logging/logger'; import { BackupService } from '../domain/backup.service'; import { BackupRestoreService } from '../domain/backup-restore.service'; import { ListBackupsQuery, CreateBackupBody, BackupIdParam, RestoreBody, CreateScheduleBody, UpdateScheduleBody, ScheduleIdParam, UpdateSettingsBody, } from './backup.validation'; import { auditLogService } from '../../audit-log'; export class BackupController { private backupService: BackupService; private restoreService: BackupRestoreService; constructor(pool: Pool) { this.backupService = new BackupService(pool); this.restoreService = new BackupRestoreService(pool); } // ============================================ // Backup Operations // ============================================ async listBackups( request: FastifyRequest<{ Querystring: ListBackupsQuery }>, reply: FastifyReply ): Promise { const result = await this.backupService.listBackups(request.query); reply.send(result); } async createBackup( request: FastifyRequest<{ Body: CreateBackupBody }>, reply: FastifyReply ): Promise { const adminSub = (request as any).userContext?.auth0Sub; const result = await this.backupService.createBackup({ name: request.body.name, backupType: 'manual', createdBy: adminSub, includeDocuments: request.body.includeDocuments, }); if (result.success) { // Log backup creation to unified audit log await auditLogService.info( 'system', adminSub || null, `Backup created: ${request.body.name || 'Manual backup'}`, 'backup', result.backupId, { name: request.body.name, includeDocuments: request.body.includeDocuments } ).catch(err => logger.error('Failed to log backup create audit event', { error: err })); reply.status(201).send({ backupId: result.backupId, status: 'completed', message: 'Backup created successfully', }); } else { // Log backup failure await auditLogService.error( 'system', adminSub || null, `Backup failed: ${request.body.name || 'Manual backup'}`, 'backup', result.backupId, { error: result.error } ).catch(err => logger.error('Failed to log backup failure audit event', { error: err })); reply.status(500).send({ backupId: result.backupId, status: 'failed', message: result.error, }); } } async getBackup( request: FastifyRequest<{ Params: BackupIdParam }>, reply: FastifyReply ): Promise { const backup = await this.backupService.getBackup(request.params.id); if (!backup) { reply.status(404).send({ error: 'Backup not found' }); return; } reply.send(backup); } async downloadBackup( request: FastifyRequest<{ Params: BackupIdParam }>, reply: FastifyReply ): Promise { const download = await this.backupService.createDownloadStream(request.params.id); if (!download) { reply.status(404).send({ error: 'Backup not found or file not available' }); return; } return reply .header('Content-Type', 'application/gzip') .header('Content-Disposition', `attachment; filename="${download.filename}"`) .header('Content-Length', download.size) .send(download.stream); } async deleteBackup( request: FastifyRequest<{ Params: BackupIdParam }>, reply: FastifyReply ): Promise { const deleted = await this.backupService.deleteBackup(request.params.id); if (!deleted) { reply.status(404).send({ error: 'Backup not found' }); return; } reply.status(204).send(); } async uploadBackup( request: FastifyRequest, reply: FastifyReply ): Promise { const adminSub = (request as any).userContext?.auth0Sub; // Handle multipart file upload const data = await request.file(); if (!data) { reply.status(400).send({ error: 'No file uploaded' }); return; } // Validate file type const filename = data.filename; if (!filename.endsWith('.tar.gz') && !filename.endsWith('.tgz')) { reply.status(400).send({ error: 'Invalid file type. Expected .tar.gz archive' }); return; } // Save file temporarily const fs = await import('fs'); const path = await import('path'); const os = await import('os'); const tempPath = path.join(os.tmpdir(), `upload-${Date.now()}-${filename}`); const writeStream = fs.createWriteStream(tempPath); await new Promise((resolve, reject) => { data.file.pipe(writeStream); data.file.on('end', resolve); data.file.on('error', reject); }); try { const backup = await this.backupService.importUploadedBackup( tempPath, filename, adminSub ); reply.status(201).send({ backupId: backup.id, filename: backup.filename, fileSizeBytes: backup.fileSizeBytes, message: 'Backup uploaded successfully', }); } catch (error) { // Cleanup temp file on error await fs.promises.unlink(tempPath).catch(() => {}); throw error; } } // ============================================ // Restore Operations // ============================================ async previewRestore( request: FastifyRequest<{ Params: BackupIdParam }>, reply: FastifyReply ): Promise { try { const preview = await this.restoreService.previewRestore(request.params.id); reply.send(preview); } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Failed to preview restore'; logger.error('Preview restore failed', { backupId: request.params.id, error: errorMessage, stack: error instanceof Error ? error.stack : undefined, }); reply.status(400).send({ error: errorMessage, }); } } async executeRestore( request: FastifyRequest<{ Params: BackupIdParam; Body: RestoreBody }>, reply: FastifyReply ): Promise { const adminSub = (request as any).userContext?.auth0Sub; try { const result = await this.restoreService.executeRestore({ backupId: request.params.id, createSafetyBackup: request.body?.createSafetyBackup ?? true, }); if (result.success) { // Log successful restore to unified audit log await auditLogService.info( 'system', adminSub || null, `Backup restored: ${request.params.id}`, 'backup', request.params.id, { safetyBackupId: result.safetyBackupId } ).catch(err => logger.error('Failed to log restore success audit event', { error: err })); reply.send({ success: true, safetyBackupId: result.safetyBackupId, restoredAt: new Date().toISOString(), message: 'Restore completed successfully', }); } else { // Log restore failure await auditLogService.error( 'system', adminSub || null, `Backup restore failed: ${request.params.id}`, 'backup', request.params.id, { error: result.error, safetyBackupId: result.safetyBackupId } ).catch(err => logger.error('Failed to log restore failure audit event', { error: err })); reply.status(500).send({ success: false, safetyBackupId: result.safetyBackupId, error: result.error, message: 'Restore failed', }); } } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Failed to execute restore'; logger.error('Restore execution failed', { backupId: request.params.id, error: errorMessage, stack: error instanceof Error ? error.stack : undefined, }); reply.status(400).send({ success: false, error: errorMessage, }); } } async getRestoreStatus( _request: FastifyRequest, reply: FastifyReply ): Promise { const status = this.restoreService.getRestoreStatus(); reply.send(status); } // ============================================ // Schedule Operations // ============================================ async listSchedules( _request: FastifyRequest, reply: FastifyReply ): Promise { const schedules = await this.backupService.listSchedules(); reply.send(schedules); } async createSchedule( request: FastifyRequest<{ Body: CreateScheduleBody }>, reply: FastifyReply ): Promise { const schedule = await this.backupService.createSchedule( request.body.name, request.body.frequency, request.body.retentionCount, request.body.isEnabled ); reply.status(201).send(schedule); } async updateSchedule( request: FastifyRequest<{ Params: ScheduleIdParam; Body: UpdateScheduleBody }>, reply: FastifyReply ): Promise { const schedule = await this.backupService.updateSchedule( request.params.id, request.body ); if (!schedule) { reply.status(404).send({ error: 'Schedule not found' }); return; } reply.send(schedule); } async deleteSchedule( request: FastifyRequest<{ Params: ScheduleIdParam }>, reply: FastifyReply ): Promise { const deleted = await this.backupService.deleteSchedule(request.params.id); if (!deleted) { reply.status(404).send({ error: 'Schedule not found' }); return; } reply.status(204).send(); } async toggleSchedule( request: FastifyRequest<{ Params: ScheduleIdParam }>, reply: FastifyReply ): Promise { const schedule = await this.backupService.toggleSchedule(request.params.id); if (!schedule) { reply.status(404).send({ error: 'Schedule not found' }); return; } reply.send(schedule); } // ============================================ // Settings Operations // ============================================ async getSettings( _request: FastifyRequest, reply: FastifyReply ): Promise { const settings = await this.backupService.getSettings(); reply.send(settings); } async updateSettings( request: FastifyRequest<{ Body: UpdateSettingsBody }>, reply: FastifyReply ): Promise { const settings = await this.backupService.updateSettings(request.body); reply.send(settings); } }