be1e0035e0
- 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>
89 lines
2.7 KiB
TypeScript
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 },
|
|
) {}
|