diff --git a/backend/src/dashboard/dashboard.service.ts b/backend/src/dashboard/dashboard.service.ts index de9e8cef..c13b13d2 100644 --- a/backend/src/dashboard/dashboard.service.ts +++ b/backend/src/dashboard/dashboard.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, BadRequestException } from '@nestjs/common'; @Injectable() export class DashboardService { @@ -9,4 +9,20 @@ export class DashboardService { users: { total: 0, active: 0 } }; } + + normalizeTimezone(timezone?: string): string | undefined { + if (timezone === undefined || timezone === null) { + return undefined; + } + const trimmed = String(timezone).trim(); + if (trimmed.length === 0) { + throw new BadRequestException('Invalid timezone'); + } + try { + new Intl.DateTimeFormat('en-US', { timeZone: trimmed }); + } catch { + throw new BadRequestException(`Invalid timezone: ${trimmed}`); + } + return trimmed; + } } diff --git a/backend/src/dashboard/dto/analytics-query.dto.ts b/backend/src/dashboard/dto/analytics-query.dto.ts index a4b7f229..74c13349 100644 --- a/backend/src/dashboard/dto/analytics-query.dto.ts +++ b/backend/src/dashboard/dto/analytics-query.dto.ts @@ -1,4 +1,7 @@ -import { IsOptional, IsDateString } from 'class-validator'; +import { IsOptional, IsDateString, IsString, Matches } from 'class-validator'; +import { Transform } from 'class-transformer'; + +const IANA_TIMEZONE_PATTERN = /^[A-Za-z_]+(?:\/[A-Za-z0-9_+\-]+){0,2}$/; export class AnalyticsQueryDto { @IsOptional() @@ -8,4 +11,14 @@ export class AnalyticsQueryDto { @IsOptional() @IsDateString() to?: string; + + @IsOptional() + @Transform(({ value }) => + typeof value === 'string' ? value.trim() : value, + ) + @IsString() + @Matches(IANA_TIMEZONE_PATTERN, { + message: 'timezone must be a valid IANA timezone name', + }) + timezone?: string; }