From 221d26650ac8bff33d5ab3593982c3406e5ffa9c Mon Sep 17 00:00:00 2001 From: Monique-7arch Date: Mon, 29 Jun 2026 15:03:49 +0100 Subject: [PATCH 1/2] feat: implement #723, #725, #726, #727 - IP block, audit-log validation --- src/auth/auth.controller.ts | 5 +- src/auth/auth.service.ts | 46 +++++++++++++- .../dto/audit-log-query.dto.spec.ts | 63 +++++++++++++++++++ 3 files changed, 111 insertions(+), 3 deletions(-) create mode 100644 src/transactions/dto/audit-log-query.dto.spec.ts diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index bb607980..10f22ddb 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -32,8 +32,9 @@ export class AuthController { constructor(private readonly authService: AuthService) {} @Post('register') - register(@Body() registerDto: RegisterDto) { - return this.authService.register(registerDto); + register(@Body() registerDto: RegisterDto, @Req() request: Request) { + const ipAddress = request.ip || request.socket.remoteAddress; + return this.authService.register(registerDto, ipAddress); } @UseGuards(GoogleAuthGuard) diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index eb1ae0fd..624b0e41 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -63,6 +63,8 @@ export class AuthService { private readonly logger = new Logger(AuthService.name); private readonly issuer = 'PropChain'; + private readonly registrationIpMap = new Map(); + private hashEmail(email: string): string { return createSha256(email).slice(0, 12); } @@ -109,7 +111,17 @@ export class AuthService { })); } - async register(data: RegisterDto) { + async register(data: RegisterDto, ipAddress?: string) { + // Block re-registration from same IP until prior email is verified + if (ipAddress) { + const allowed = this.canRegisterFromIp(ipAddress); + if (!allowed) { + throw new BadRequestException( + 'A registration from this IP is already pending email verification. Please verify your email before registering a new account.', + ); + } + } + const existingUser = await this.usersService.findByEmail(data.email); if (existingUser) { throw new BadRequestException('A user with that email already exists'); @@ -164,12 +176,44 @@ export class AuthService { this.logger.error('Failed to queue verification email:', err?.message || err); }); + // Track IP for re-registration prevention + if (ipAddress) { + const expiryMs = + parseDuration( + this.configService.get('EMAIL_VERIFICATION_EXPIRES_IN') ?? '24h', + 24 * 60 * 60, + ) * 1000; + this.registrationIpMap.set(ipAddress, { + email: user.email, + expiresAt: new Date(Date.now() + expiryMs), + }); + } + return { user: sanitizeUser(user), message: 'Registration successful. Please check your email to verify your account.', }; } + private canRegisterFromIp(ipAddress: string): boolean { + const entry = this.registrationIpMap.get(ipAddress); + if (!entry) return true; + if (Date.now() > entry.expiresAt.getTime()) { + this.registrationIpMap.delete(ipAddress); + return true; + } + return false; + } + + private cleanupIpForEmail(email: string): void { + for (const [ip, entry] of this.registrationIpMap.entries()) { + if (entry.email === email) { + this.registrationIpMap.delete(ip); + return; + } + } + } + /** * Performs mandatory security checks before validating credentials. * diff --git a/src/transactions/dto/audit-log-query.dto.spec.ts b/src/transactions/dto/audit-log-query.dto.spec.ts new file mode 100644 index 00000000..232af49e --- /dev/null +++ b/src/transactions/dto/audit-log-query.dto.spec.ts @@ -0,0 +1,63 @@ +import 'reflect-metadata'; +import { validate } from 'class-validator'; +import { plainToInstance } from 'class-transformer'; +import { AuditLogQueryDto } from './audit-log-query.dto'; + +describe('AuditLogQueryDto', () => { + it('accepts valid ISO 8601 dates', async () => { + const dto = plainToInstance(AuditLogQueryDto, { + dateFrom: '2024-01-01T00:00:00Z', + dateTo: '2024-12-31T23:59:59Z', + }); + const errors = await validate(dto); + expect(errors.length).toBe(0); + }); + + it('rejects malformed date strings for dateFrom', async () => { + const dto = plainToInstance(AuditLogQueryDto, { + dateFrom: 'not-a-date', + }); + const errors = await validate(dto); + expect(errors.length).toBeGreaterThan(0); + expect(errors[0].property).toBe('dateFrom'); + }); + + it('rejects malformed date strings for dateTo', async () => { + const dto = plainToInstance(AuditLogQueryDto, { + dateTo: 'bad-date-value', + }); + const errors = await validate(dto); + expect(errors.length).toBeGreaterThan(0); + expect(errors[0].property).toBe('dateTo'); + }); + + it('accepts missing optional dates', async () => { + const dto = plainToInstance(AuditLogQueryDto, {}); + const errors = await validate(dto); + expect(errors.length).toBe(0); + }); + + it('accepts dateFrom without dateTo', async () => { + const dto = plainToInstance(AuditLogQueryDto, { + dateFrom: '2024-06-01T00:00:00Z', + }); + const errors = await validate(dto); + expect(errors.length).toBe(0); + }); + + it('accepts dateTo without dateFrom', async () => { + const dto = plainToInstance(AuditLogQueryDto, { + dateTo: '2024-06-30T23:59:59Z', + }); + const errors = await validate(dto); + expect(errors.length).toBe(0); + }); + + it('rejects numeric strings for dates', async () => { + const dto = plainToInstance(AuditLogQueryDto, { + dateFrom: 12345, + }); + const errors = await validate(dto); + expect(errors.length).toBeGreaterThan(0); + }); +}); From e1dbd5ac1c66f619e753d3577a9625a24d74338c Mon Sep 17 00:00:00 2001 From: Monique-7arch Date: Mon, 29 Jun 2026 15:28:44 +0100 Subject: [PATCH 2/2] fix: use IsDateString instead of IsISO8601 for date validation IsISO8601 + Type(() => Date) fails validation because class-transformer converts the string to a Date before class-validator runs. IsDateString validates the raw string input correctly. --- src/transactions/dto/audit-log-query.dto.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/transactions/dto/audit-log-query.dto.ts b/src/transactions/dto/audit-log-query.dto.ts index 0d9403af..f4cd3b21 100644 --- a/src/transactions/dto/audit-log-query.dto.ts +++ b/src/transactions/dto/audit-log-query.dto.ts @@ -1,5 +1,5 @@ import { Type } from 'class-transformer'; -import { IsISO8601, IsOptional, IsString, IsInt, Min, Max } from 'class-validator'; +import { IsOptional, IsString, IsDateString, IsInt, Min, Max } from 'class-validator'; export class AuditLogQueryDto { @IsOptional() @@ -7,14 +7,12 @@ export class AuditLogQueryDto { actorId?: string; @IsOptional() - @IsISO8601() - @Type(() => Date) - dateFrom?: Date; + @IsDateString() + dateFrom?: string; @IsOptional() - @IsISO8601() - @Type(() => Date) - dateTo?: Date; + @IsDateString() + dateTo?: string; @IsOptional() @Type(() => Number)