feat(auth): add row/size caps + streaming to export-user-data
- Add per-collection row cap (default 10k, env EXPORT_ROW_CAP) via Prisma take on all findMany calls - Add total size cap (default 100MB, env EXPORT_SIZE_CAP_MB); throws PayloadTooLargeException (413) when exceeded - Convert response to Node.js Readable stream piped via NestJS StreamableFile to avoid large in-memory buffers - Export ExportUserDataResult interface (stream + truncated flag) from handler - Update controller to set Content-Type/Content-Disposition headers and return StreamableFile - Document EXPORT_ROW_CAP and EXPORT_SIZE_CAP_MB env vars in Swagger - Extend tests: row-cap assertion (take arg), size-cap 413 path, stream assertions Fixes GOO-223 (M-1 from GOO-200 audit). Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -5,13 +5,16 @@ import {
|
||||
Get,
|
||||
Param,
|
||||
Post,
|
||||
Res,
|
||||
StreamableFile,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { CommandBus } from '@nestjs/cqrs';
|
||||
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiProduces } from '@nestjs/swagger';
|
||||
import { Response } from 'express';
|
||||
import { CancelUserDeletionCommand } from '../../application/commands/cancel-user-deletion/cancel-user-deletion.command';
|
||||
import { ExportUserDataCommand } from '../../application/commands/export-user-data/export-user-data.command';
|
||||
import { type UserDataExport } from '../../application/commands/export-user-data/export-user-data.handler';
|
||||
import { type ExportUserDataResult } from '../../application/commands/export-user-data/export-user-data.handler';
|
||||
import { ForceDeleteUserCommand } from '../../application/commands/force-delete-user/force-delete-user.command';
|
||||
import { RequestUserDeletionCommand } from '../../application/commands/request-user-deletion/request-user-deletion.command';
|
||||
import { type JwtPayload } from '../../infrastructure/services/token.service';
|
||||
@@ -58,13 +61,33 @@ export class UserDataController {
|
||||
@Get('me/export')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth('JWT')
|
||||
@ApiOperation({ summary: 'Export user data (GDPR Article 20)' })
|
||||
@ApiResponse({ status: 200, description: 'User data exported as JSON' })
|
||||
@ApiProduces('application/json')
|
||||
@ApiOperation({
|
||||
summary: 'Export user data (GDPR Article 20)',
|
||||
description:
|
||||
'Streams the full user data export as JSON. ' +
|
||||
'Row cap (per collection) defaults to 10 000 rows; size cap defaults to 100 MB. ' +
|
||||
'Both are configurable via EXPORT_ROW_CAP and EXPORT_SIZE_CAP_MB env vars.',
|
||||
})
|
||||
@ApiResponse({ status: 200, description: 'User data exported as streaming JSON' })
|
||||
@ApiResponse({ status: 401, description: 'Unauthorized' })
|
||||
@ApiResponse({
|
||||
status: 413,
|
||||
description: 'Export exceeds size cap — contact support for chunked export',
|
||||
})
|
||||
async exportData(
|
||||
@CurrentUser() user: JwtPayload,
|
||||
): Promise<UserDataExport> {
|
||||
return this.commandBus.execute(new ExportUserDataCommand(user.sub));
|
||||
@Res({ passthrough: true }) res: Response,
|
||||
): Promise<StreamableFile> {
|
||||
const result: ExportUserDataResult = await this.commandBus.execute(
|
||||
new ExportUserDataCommand(user.sub),
|
||||
);
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.setHeader(
|
||||
'Content-Disposition',
|
||||
`attachment; filename="user-data-${user.sub}.json"`,
|
||||
);
|
||||
return new StreamableFile(result.stream);
|
||||
}
|
||||
|
||||
@Delete(':id/force')
|
||||
|
||||
Reference in New Issue
Block a user