Features - Payment reminders via Resend (candidates, send, history, test email) - Bulk import of the customer list from Excel/CSV in the dashboard - Dashboard i18n (en/nl) with language selector Security - Enforce venue isolation on all /venues/:venueId routes - Require admin for planned-tab changes and pairing codes - Rate-limit login and device registration; opt-in TRUST_PROXY - Require JWT_SECRET in production; stop leaking internal errors - Escape customer names in reminder emails Sync and commands - Remove tabs the terminal no longer reports; validate nested sync payload - Return all pending commands on every sync (fixes missed-command race) - First command ack wins; atomic registration code claim - Reject commands for revoked devices, stale versions or duplicates - Remind only unpaid balances, with a 7-day cooldown Dashboard - Share a single token refresh across concurrent 401s (random logouts) - Keep the session alive for the refresh token lifetime Deployment - Replace `db push --accept-data-loss` with migrations (baselines legacy DBs) - Set NODE_ENV=production; pass Resend and TRUST_PROXY env in prod compose
34 lines
1.1 KiB
TypeScript
34 lines
1.1 KiB
TypeScript
import { IsString, IsOptional, IsNotEmpty, IsEmail, MaxLength, IsArray, ArrayMaxSize, ValidateNested, IsIn } from 'class-validator';
|
|
import { Transform, Type } from 'class-transformer';
|
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
|
|
|
export class ImportPlannedTabEntryDto {
|
|
@ApiProperty({ example: 'Jan Peeters' })
|
|
@IsString()
|
|
@IsNotEmpty()
|
|
@MaxLength(100)
|
|
customerName: string;
|
|
|
|
@ApiPropertyOptional({ example: 'jan@example.com' })
|
|
@IsOptional()
|
|
@Transform(({ value }) => (typeof value === 'string' ? value.trim() || undefined : value))
|
|
@IsEmail()
|
|
email?: string;
|
|
}
|
|
|
|
export class ImportPlannedTabsDto {
|
|
@ApiProperty({ type: [ImportPlannedTabEntryDto] })
|
|
@IsArray()
|
|
@ArrayMaxSize(5000)
|
|
@ValidateNested({ each: true })
|
|
@Type(() => ImportPlannedTabEntryDto)
|
|
entries: ImportPlannedTabEntryDto[];
|
|
|
|
@ApiProperty({
|
|
enum: ['merge', 'replace'],
|
|
description: 'merge adds new names and updates emails; replace also removes names that are not in the import',
|
|
})
|
|
@IsIn(['merge', 'replace'])
|
|
mode: 'merge' | 'replace';
|
|
}
|