feat(api): implement GDPR-compliant user data deletion
- Add deletedAt/deletionScheduledAt fields to User model with indexes - Implement 5 CQRS command handlers: - RequestUserDeletion: 30-day soft-delete grace period - CancelUserDeletion: restore within grace period - ForceDeleteUser: admin immediate deletion with PII anonymization - ProcessScheduledDeletions: cron-ready batch processor - ExportUserData: GDPR Article 20 data portability - Cascade strategy: anonymize PII, expire listings, cancel subscriptions, delete reviews/inquiries/searches/notifications, preserve payments for audit - Add UserDataController with DELETE /users/me, POST /users/me/cancel-deletion, GET /users/me/export, DELETE /users/:id/force (admin) - 22 unit tests covering all handlers (160 files, 853 tests passing) - Migration: 20260410000000_add_user_soft_delete_fields Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
Post,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { type CommandBus } from '@nestjs/cqrs';
|
||||
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
|
||||
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 { 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';
|
||||
import { CurrentUser } from '../decorators/current-user.decorator';
|
||||
import { Roles } from '../decorators/roles.decorator';
|
||||
import { type ForceDeleteUserDto } from '../dto/force-delete-user.dto';
|
||||
import { type RequestDeletionDto } from '../dto/request-deletion.dto';
|
||||
import { JwtAuthGuard } from '../guards/jwt-auth.guard';
|
||||
import { RolesGuard } from '../guards/roles.guard';
|
||||
|
||||
@ApiTags('users')
|
||||
@Controller('users')
|
||||
export class UserDataController {
|
||||
constructor(private readonly commandBus: CommandBus) {}
|
||||
|
||||
@Delete('me')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth('JWT')
|
||||
@ApiOperation({ summary: 'Request account deletion (30-day grace period)' })
|
||||
@ApiResponse({ status: 200, description: 'Deletion scheduled' })
|
||||
@ApiResponse({ status: 401, description: 'Unauthorized' })
|
||||
async requestDeletion(
|
||||
@CurrentUser() user: JwtPayload,
|
||||
@Body() dto: RequestDeletionDto,
|
||||
): Promise<{ scheduledAt: Date; message: string }> {
|
||||
const result = await this.commandBus.execute(
|
||||
new RequestUserDeletionCommand(user.sub, dto.reason),
|
||||
);
|
||||
return { ...result, message: 'Tài khoản sẽ bị xóa sau 30 ngày' };
|
||||
}
|
||||
|
||||
@Post('me/cancel-deletion')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth('JWT')
|
||||
@ApiOperation({ summary: 'Cancel pending account deletion' })
|
||||
@ApiResponse({ status: 201, description: 'Deletion cancelled' })
|
||||
@ApiResponse({ status: 401, description: 'Unauthorized' })
|
||||
async cancelDeletion(
|
||||
@CurrentUser() user: JwtPayload,
|
||||
): Promise<{ message: string }> {
|
||||
return this.commandBus.execute(new CancelUserDeletionCommand(user.sub));
|
||||
}
|
||||
|
||||
@Get('me/export')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth('JWT')
|
||||
@ApiOperation({ summary: 'Export user data (GDPR Article 20)' })
|
||||
@ApiResponse({ status: 200, description: 'User data exported as JSON' })
|
||||
@ApiResponse({ status: 401, description: 'Unauthorized' })
|
||||
async exportData(
|
||||
@CurrentUser() user: JwtPayload,
|
||||
): Promise<UserDataExport> {
|
||||
return this.commandBus.execute(new ExportUserDataCommand(user.sub));
|
||||
}
|
||||
|
||||
@Delete(':id/force')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles('ADMIN')
|
||||
@ApiBearerAuth('JWT')
|
||||
@ApiOperation({ summary: 'Force-delete user immediately (admin only)' })
|
||||
@ApiResponse({ status: 200, description: 'User force-deleted' })
|
||||
@ApiResponse({ status: 401, description: 'Unauthorized' })
|
||||
@ApiResponse({ status: 403, description: 'Forbidden — admin only' })
|
||||
async forceDelete(
|
||||
@Param('id') id: string,
|
||||
@CurrentUser() admin: JwtPayload,
|
||||
@Body() dto: ForceDeleteUserDto,
|
||||
): Promise<{ message: string }> {
|
||||
return this.commandBus.execute(
|
||||
new ForceDeleteUserCommand(id, admin.sub, dto.reason),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user