Spaces:
Running
Running
| import { | |
| Body, | |
| Controller, | |
| Delete, | |
| Get, | |
| HttpCode, | |
| HttpException, | |
| Param, | |
| Post, | |
| Put, | |
| Req, | |
| Res, | |
| UploadedFile, | |
| UseGuards, | |
| UseInterceptors, | |
| } from '@nestjs/common'; | |
| import { FileInterceptor } from '@nestjs/platform-express'; | |
| import type { Request, Response } from 'express'; | |
| import fs from 'fs'; | |
| import type { User } from '../../types'; | |
| import { BackupService } from './backup.service'; | |
| import { JwtAuthGuard } from '../auth/jwt-auth.guard'; | |
| import { AdminGuard } from '../auth/admin.guard'; | |
| import { CurrentUser } from '../auth/current-user.decorator'; | |
| import { writeAudit, getClientIp } from '../../services/auditLog'; | |
| import { getUploadTmpDir, MAX_BACKUP_UPLOAD_SIZE } from '../../services/backupService'; | |
| const UPLOAD = { | |
| dest: getUploadTmpDir(), | |
| fileFilter: (_req: unknown, file: Express.Multer.File, cb: (err: Error | null, accept: boolean) => void) => { | |
| if (file.originalname.endsWith('.zip')) return cb(null, true); | |
| cb(new Error('Only ZIP files allowed'), false); | |
| }, | |
| limits: { fileSize: MAX_BACKUP_UPLOAD_SIZE }, | |
| }; | |
| /** | |
| * /api/backup — admin-only database backup management (list, create, download, | |
| * restore from a stored or uploaded zip, auto-backup settings, delete). | |
| * | |
| * Byte-identical to the legacy Express route (server/src/routes/backup.ts): | |
| * admin-gated, the create rate-limit (429), the filename validation (400/404), | |
| * the audit-log writes, res.download for downloads and the tmp-file cleanup for | |
| * uploads. All JSON responses answer 200. | |
| */ | |
| ('api/backup') | |
| (JwtAuthGuard, AdminGuard) | |
| export class BackupController { | |
| constructor(private readonly backup: BackupService) {} | |
| ('list') | |
| list() { | |
| try { | |
| return { backups: this.backup.listBackups() }; | |
| } catch { | |
| throw new HttpException({ error: 'Error loading backups' }, 500); | |
| } | |
| } | |
| ('create') | |
| (200) // Express answers create with res.json (200), not the POST-default 201. | |
| async create(() user: User, () req: Request) { | |
| if (!this.backup.checkRateLimit(req.ip || 'unknown', 3, this.backup.rateWindow)) { | |
| throw new HttpException({ error: 'Too many backup requests. Please try again later.' }, 429); | |
| } | |
| try { | |
| const backup = await this.backup.createBackup(); | |
| writeAudit({ userId: user.id, action: 'backup.create', resource: backup.filename, ip: getClientIp(req), details: { size: backup.size } }); | |
| return { success: true, backup }; | |
| } catch { | |
| throw new HttpException({ error: 'Error creating backup' }, 500); | |
| } | |
| } | |
| ('download/:filename') | |
| download(('filename') filename: string, () res: Response): void { | |
| if (!this.backup.isValidBackupFilename(filename)) { | |
| throw new HttpException({ error: 'Invalid filename' }, 400); | |
| } | |
| if (!this.backup.backupFileExists(filename)) { | |
| throw new HttpException({ error: 'Backup not found' }, 404); | |
| } | |
| res.download(this.backup.backupFilePath(filename), filename); | |
| } | |
| ('restore/:filename') | |
| (200) // Express answers restore with res.json (200). | |
| async restore(() user: User, ('filename') filename: string, () req: Request) { | |
| if (!this.backup.isValidBackupFilename(filename)) { | |
| throw new HttpException({ error: 'Invalid filename' }, 400); | |
| } | |
| if (!this.backup.backupFileExists(filename)) { | |
| throw new HttpException({ error: 'Backup not found' }, 404); | |
| } | |
| try { | |
| const result = await this.backup.restoreFromZip(this.backup.backupFilePath(filename)); | |
| if (!result.success) { | |
| throw new HttpException({ error: result.error }, result.status || 400); | |
| } | |
| writeAudit({ userId: user.id, action: 'backup.restore', resource: filename, ip: getClientIp(req) }); | |
| return { success: true }; | |
| } catch (err) { | |
| if (err instanceof HttpException) throw err; | |
| throw new HttpException({ error: 'Error restoring backup' }, 500); | |
| } | |
| } | |
| ('upload-restore') | |
| (200) // Express answers upload-restore with res.json (200). | |
| (FileInterceptor('backup', UPLOAD)) | |
| async uploadRestore(() user: User, () file: Express.Multer.File | undefined, () req: Request) { | |
| if (!file) { | |
| throw new HttpException({ error: 'No file uploaded' }, 400); | |
| } | |
| const zipPath = file.path; | |
| const origName = file.originalname || 'upload.zip'; | |
| try { | |
| const result = await this.backup.restoreFromZip(zipPath); | |
| if (!result.success) { | |
| throw new HttpException({ error: result.error }, result.status || 400); | |
| } | |
| writeAudit({ userId: user.id, action: 'backup.upload_restore', resource: origName, ip: getClientIp(req) }); | |
| return { success: true }; | |
| } catch (err) { | |
| if (err instanceof HttpException) throw err; | |
| throw new HttpException({ error: 'Error restoring backup' }, 500); | |
| } finally { | |
| if (fs.existsSync(zipPath)) fs.unlinkSync(zipPath); | |
| } | |
| } | |
| ('auto-settings') | |
| autoSettings() { | |
| try { | |
| return this.backup.getAutoSettings(); | |
| } catch (err) { | |
| console.error('[backup] GET auto-settings:', err); | |
| throw new HttpException({ error: 'Could not load backup settings' }, 500); | |
| } | |
| } | |
| ('auto-settings') | |
| updateAutoSettings(() user: User, () body: Record<string, unknown>, () req: Request) { | |
| try { | |
| const settings = this.backup.updateAutoSettings(body || {}); | |
| writeAudit({ userId: user.id, action: 'backup.auto_settings', ip: getClientIp(req), details: { enabled: settings.enabled, interval: settings.interval, keep_days: settings.keep_days } }); | |
| return { settings }; | |
| } catch (err) { | |
| console.error('[backup] PUT auto-settings:', err); | |
| const msg = err instanceof Error ? err.message : String(err); | |
| throw new HttpException({ error: 'Could not save auto-backup settings', detail: process.env.NODE_ENV?.toLowerCase() !== 'production' ? msg : undefined }, 500); | |
| } | |
| } | |
| (':filename') | |
| remove(() user: User, ('filename') filename: string, () req: Request) { | |
| if (!this.backup.isValidBackupFilename(filename)) { | |
| throw new HttpException({ error: 'Invalid filename' }, 400); | |
| } | |
| if (!this.backup.backupFileExists(filename)) { | |
| throw new HttpException({ error: 'Backup not found' }, 404); | |
| } | |
| this.backup.deleteBackup(filename); | |
| writeAudit({ userId: user.id, action: 'backup.delete', resource: filename, ip: getClientIp(req) }); | |
| return { success: true }; | |
| } | |
| } | |