import { OmitType, PartialType } from '@nestjs/mapped-types'; import { CUSTOM_MODULE_CATEGORIES } from '@tessera/shared'; import { Transform } from 'class-transformer'; import { IsBoolean, IsIn, IsNotEmpty, IsOptional, IsString, MaxLength, Validate, ValidatorConstraint, type ValidatorConstraintInterface, } from 'class-validator'; /** * Adresse eines eigenen Moduls (T-9WC-03, T-9WC-06): gueltig nur, wenn der * URL-Parser sie annimmt, das Schema `https:` ist, ein Rechnername da ist und * weder Benutzername noch Kennwort in der Adresse stehen — sonst saehe jeder * Benutzer die Zugangsdaten. `javascript:`, `data:`, `http:` und `ftp:` fallen * damit heraus. */ @ValidatorConstraint({ name: 'nurHttpsOhneZugangsdaten', async: false }) class NurHttpsOhneZugangsdatenConstraint implements ValidatorConstraintInterface { validate(value: unknown): boolean { if (typeof value !== 'string') return false; let parsed: URL; try { parsed = new URL(value); } catch { return false; } return ( parsed.protocol === 'https:' && parsed.hostname !== '' && parsed.username === '' && parsed.password === '' ); } defaultMessage(): string { return 'Nur https-Adressen ohne Zugangsdaten sind erlaubt.'; } } const trimString = ({ value }: { value: unknown }) => typeof value === 'string' ? value.trim() : value; /** DTO fuer das Anlegen eines eigenen Moduls. */ export class CreateCustomModuleDto { @Transform(trimString) @IsString() @IsNotEmpty() @MaxLength(100) name!: string; @Transform(trimString) @IsString() @MaxLength(2048) @Validate(NurHttpsOhneZugangsdatenConstraint) url!: string; @IsIn([...CUSTOM_MODULE_CATEGORIES]) category!: (typeof CUSTOM_MODULE_CATEGORIES)[number]; /** * quick-260929-dzu: `true` legt einen gemeinsamen Eintrag fuer alle Benutzer * an (nur Administratoren, sonst 403 im Dienst). Fehlt das Feld oder ist es * `false`, ist der Eintrag persoenlich und gehoert dem Aufrufer. */ @IsOptional() @IsBoolean() shared?: boolean; } /** * Teil-Update: jedes gesetzte Feld wird genauso geprueft wie beim Anlegen. * `shared` ist ausgenommen — ob ein Eintrag gemeinsam oder persoenlich ist, * aendert sich nach dem Anlegen nicht (die globale Pipe verwirft das Feld * dank `whitelist: true`). * `skipNullProperties: false`: fehlende Felder bleiben unveraendert, ein * ausdrueckliches `null` wird aber geprueft und damit abgelehnt (400) — sonst * liefe `{"name": null}` bis in die Datenbank und endete als 500. */ export class UpdateCustomModuleDto extends PartialType( OmitType(CreateCustomModuleDto, ['shared'] as const), { skipNullProperties: false }, ) {}