Files
tessera-ctl/apps/api/src/custom-modules/dto/custom-module.dto.ts
T
schalli be1e0035e0 fix(custom-modules): null beim Aendern ablehnen, Ladefehler statt 404, Fehlertexte, Seitenleiste eingeklappt
- PATCH mit null fuer name/url/category ergibt 400 statt 500
- Modulansicht unterscheidet Ladefehler von "nicht gefunden"
- Formular/Loeschdialog nennen 403 und 400 eigens
- eingeklappte Seitenleiste folgt der Gruppenreihenfolge der ausgeklappten
- neue Eintraege sind mit "Eigene Module" vorbelegt
- Verwaltung zeigt bei Ladefehler nicht zusaetzlich "keine Eintraege"

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 03:20:16 +02:00

89 lines
2.7 KiB
TypeScript

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 },
) {}