Backend-agnostic disaster-recovery backup engine that runs on the current storage backend (no storage cutover required): - Full-bundle export: db.dump (pg_dump custom) + every storage blob + manifest.json with per-object SHA-256, streamed as a tar. Entry points: admin UI download, GET /api/v1/admin/backup/export, scripts/create-full-backup.ts. - Admin-configurable push destinations (backup_destinations table, migration 0091): SFTP/SSH, S3-compatible (reuses the minio client), and mounted path/NAS behind one transport interface (test/push/prune). Secrets AES-GCM at rest; API returns only *IsSet markers. - Opt-in per-destination AES-256 bundle encryption (scrypt KDF, streamed) + scripts/decrypt-backup.ts for restore. - Wired the previously-dead database-backup cron to runScheduledBackupPush (push to enabled destinations, prune to retention, alert super-admins on failure). Tests: 1608 unit/integration pass; tsc + lint clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
63 lines
2.0 KiB
TypeScript
63 lines
2.0 KiB
TypeScript
import { NextResponse } from 'next/server';
|
|
|
|
import { requireSuperAdmin, withAuth } from '@/lib/api/helpers';
|
|
import { parseBody } from '@/lib/api/route-helpers';
|
|
import { createAuditLog } from '@/lib/audit';
|
|
import { errorResponse, NotFoundError } from '@/lib/errors';
|
|
import {
|
|
deleteDestination,
|
|
updateDestination,
|
|
type DestinationInput,
|
|
} from '@/lib/services/backup-destinations.service';
|
|
import { backupDestinationSchema } from '@/lib/validators/backup-destinations';
|
|
|
|
export const runtime = 'nodejs';
|
|
|
|
/** Update a backup destination. Super-admin only. */
|
|
export const PUT = withAuth(async (req, ctx, params) => {
|
|
try {
|
|
requireSuperAdmin(ctx, 'admin.backup.destinations.update');
|
|
const id = params.id;
|
|
if (!id) throw new NotFoundError('Backup destination');
|
|
const body = await parseBody(req, backupDestinationSchema);
|
|
const updated = await updateDestination(id, body as DestinationInput);
|
|
await createAuditLog({
|
|
userId: ctx.userId,
|
|
portId: ctx.portId,
|
|
action: 'update',
|
|
entityType: 'backup_destination',
|
|
entityId: id,
|
|
severity: 'warning',
|
|
metadata: { name: updated.name, type: updated.type, enabled: updated.enabled },
|
|
ipAddress: ctx.ipAddress,
|
|
userAgent: ctx.userAgent,
|
|
});
|
|
return NextResponse.json({ data: updated });
|
|
} catch (error) {
|
|
return errorResponse(error);
|
|
}
|
|
});
|
|
|
|
/** Delete a backup destination. Super-admin only. */
|
|
export const DELETE = withAuth(async (_req, ctx, params) => {
|
|
try {
|
|
requireSuperAdmin(ctx, 'admin.backup.destinations.delete');
|
|
const id = params.id;
|
|
if (!id) throw new NotFoundError('Backup destination');
|
|
await deleteDestination(id);
|
|
await createAuditLog({
|
|
userId: ctx.userId,
|
|
portId: ctx.portId,
|
|
action: 'delete',
|
|
entityType: 'backup_destination',
|
|
entityId: id,
|
|
severity: 'warning',
|
|
ipAddress: ctx.ipAddress,
|
|
userAgent: ctx.userAgent,
|
|
});
|
|
return new NextResponse(null, { status: 204 });
|
|
} catch (error) {
|
|
return errorResponse(error);
|
|
}
|
|
});
|