Spaces:
Sleeping
Sleeping
File size: 6,552 Bytes
57a889c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 | 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.
*/
@Controller('api/backup')
@UseGuards(JwtAuthGuard, AdminGuard)
export class BackupController {
constructor(private readonly backup: BackupService) {}
@Get('list')
list() {
try {
return { backups: this.backup.listBackups() };
} catch {
throw new HttpException({ error: 'Error loading backups' }, 500);
}
}
@Post('create')
@HttpCode(200) // Express answers create with res.json (200), not the POST-default 201.
async create(@CurrentUser() user: User, @Req() 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);
}
}
@Get('download/:filename')
download(@Param('filename') filename: string, @Res() 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);
}
@Post('restore/:filename')
@HttpCode(200) // Express answers restore with res.json (200).
async restore(@CurrentUser() user: User, @Param('filename') filename: string, @Req() 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);
}
}
@Post('upload-restore')
@HttpCode(200) // Express answers upload-restore with res.json (200).
@UseInterceptors(FileInterceptor('backup', UPLOAD))
async uploadRestore(@CurrentUser() user: User, @UploadedFile() file: Express.Multer.File | undefined, @Req() 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);
}
}
@Get('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);
}
}
@Put('auto-settings')
updateAutoSettings(@CurrentUser() user: User, @Body() body: Record<string, unknown>, @Req() 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);
}
}
@Delete(':filename')
remove(@CurrentUser() user: User, @Param('filename') filename: string, @Req() 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 };
}
}
|