diff --git a/src/modules/gdpr/dto/gdpr-export.dto.ts b/src/modules/gdpr/dto/gdpr-export.dto.ts new file mode 100644 index 00000000..257f19bf --- /dev/null +++ b/src/modules/gdpr/dto/gdpr-export.dto.ts @@ -0,0 +1,22 @@ +import { Exclude } from 'class-transformer'; + +export class GdprExportDto { + @Exclude() + password?: string; + + @Exclude() + refreshToken?: string; + + @Exclude() + passwordHistory?: string[]; + + @Exclude() + totpSecret?: string; + + @Exclude() + token?: string; + + constructor(partial: Partial) { + Object.assign(this, partial); + } +} diff --git a/src/modules/gdpr/gdpr.service.ts b/src/modules/gdpr/gdpr.service.ts index 574cfcda..57085613 100644 --- a/src/modules/gdpr/gdpr.service.ts +++ b/src/modules/gdpr/gdpr.service.ts @@ -1,8 +1,10 @@ import { Inject, Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; +import { plainToInstance, instanceToPlain } from 'class-transformer'; import { UserConsent } from './entities/user-consent.entity'; import { ConsentDto } from './dto/consent.dto'; +import { GdprExportDto } from './dto/gdpr-export.dto'; @Injectable() export class GdprService { @@ -32,8 +34,11 @@ export class GdprService { await this.auditService.log('GDPR_EXPORT', userId); + const gdprExportUserInstance = plainToInstance(GdprExportDto, user); + const cleanProfile = instanceToPlain(gdprExportUserInstance); + return { - profile: user, + profile: cleanProfile, consents, }; } diff --git a/src/modules/gdpr/tests/gdpr.service.spec.ts b/src/modules/gdpr/tests/gdpr.service.spec.ts index 4305206f..c5bd101d 100644 --- a/src/modules/gdpr/tests/gdpr.service.spec.ts +++ b/src/modules/gdpr/tests/gdpr.service.spec.ts @@ -4,7 +4,17 @@ import { GdprService } from '../gdpr.service'; import { UserConsent } from '../entities/user-consent.entity'; const mockUsersService = { - findById: jest.fn().mockResolvedValue({ id: 'user-1', email: 'test@test.com' }), + findById: jest.fn().mockResolvedValue({ + id: 'user-1', + email: 'test@test.com', + firstName: 'John', + lastName: 'Doe', + password: '$2a$10$bcryptencryptedhashplaceholder', + refreshToken: 'some-refresh-token-value', + passwordHistory: ['$2a$10$oldhash1', '$2a$10$oldhash2'], + totpSecret: 'supersecretotpvalue', + token: 'active-session-token-or-verification-token', + }), update: jest.fn().mockResolvedValue(undefined), }; @@ -34,9 +44,22 @@ describe('GdprService', () => { service = module.get(GdprService); }); - it('exports user data', async () => { + it('exports user data and excludes sensitive credential fields', async () => { const result = await service.exportUserData('user-1'); expect(result.profile).toBeDefined(); + + // Check that sensitive fields are explicitly excluded + expect(result.profile.password).toBeUndefined(); + expect(result.profile.refreshToken).toBeUndefined(); + expect(result.profile.passwordHistory).toBeUndefined(); + expect(result.profile.totpSecret).toBeUndefined(); + expect(result.profile.token).toBeUndefined(); + + // Check that PII fields are preserved + expect(result.profile.id).toBe('user-1'); + expect(result.profile.email).toBe('test@test.com'); + expect(result.profile.firstName).toBe('John'); + expect(result.profile.lastName).toBe('Doe'); }); it('erases user data', async () => {