Files
tessera-ctl/apps/api/src/proxmox/dto/proxmox-server.dto.ts
T
schalli 710034c80a fix(260923-ku6): Verbindungstest prueft Formularwerte statt gespeicherten Stand
Nachbesserung aus dem Browser-Rundgang zu 260923-dhh (Befund 1): "Verbindung
testen" pruefte bislang immer den in der Datenbank gespeicherten Server, nicht
das ungespeicherte Formular. Eine im Formular abgeschaltete Zertifikatspruefung
oder ein neu eingetipptes Token-Geheimnis wurden dadurch beim Test ignoriert
und erst nach "Speichern" wirksam — eine Falle fuer genau den Ablauf, den
Nutzer instinktiv waehlen (eintippen, testen, dann erst speichern).

Neues `TestProxmoxServerDto` plus Merge-Baustein `resolveEffectiveTestServer`
in `ProxmoxService`: normale Felder folgen dem Formular (auch wenn absichtlich
geleert), Geheimnisfelder folgen der bestehenden "leer -> gespeicherten Wert
behalten"-Regel, weil `ServerForm` sie beim Laden nie aus der Datenbank
vorbefuellt. Neue Route `POST servers/test` (ohne `:id`) deckt die Neuanlage
ab, wo es noch keinen gespeicherten Server gibt. Der Testen-Knopf steht jetzt
immer zur Verfuegung, nicht mehr nur nach dem ersten Speichern.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 15:11:56 +02:00

164 lines
4.3 KiB
TypeScript

import {
IsBoolean,
IsIn,
IsInt,
IsNotEmpty,
IsOptional,
IsString,
IsUrl,
Max,
Min,
Validate,
ValidateIf,
type ValidationArguments,
ValidatorConstraint,
type ValidatorConstraintInterface,
} from 'class-validator';
/**
* D-03: PMG kennt laut Recherche keinen API-Token (Annahme A1) — ein Server
* vom Typ `pmg` mit `authMethod: 'token'` wird bereits beim Speichern mit
* einer deutschen Klartextmeldung abgelehnt (400), nicht erst beim
* Abfragen. Angebracht am Feld `authMethod`, liest aber `productType`
* desselben Objekts (`args.object`) — class-validator erlaubt das.
*/
@ValidatorConstraint({ name: 'pmgOhneToken', async: false })
class PmgOhneTokenConstraint implements ValidatorConstraintInterface {
validate(_value: unknown, args: ValidationArguments): boolean {
const obj = args.object as { productType?: string; authMethod?: string };
return !(obj.productType === 'pmg' && obj.authMethod === 'token');
}
defaultMessage(): string {
return 'PMG unterstuetzt keinen API-Token-Zugang. Bitte Benutzer und Passwort waehlen.';
}
}
/**
* DTO fuer das Anlegen eines Proxmox-Servers (Aufgabe 1). Pflichtfelder je
* `authMethod` mit `@ValidateIf` (Aufgabe 2): ein Token-Zugang verlangt
* `tokenId`/`tokenSecret`, ein Passwort-Zugang `username`/`password`.
*/
export class CreateProxmoxServerDto {
@IsString()
@IsNotEmpty()
name!: string;
@IsIn(['pve', 'pbs', 'pmg'])
productType!: 'pve' | 'pbs' | 'pmg';
// require_tld: false — interne Namen wie "pve.intern" sind sonst abgelehnt.
@IsUrl({ protocols: ['http', 'https'], require_tld: false })
baseUrl!: string;
@IsIn(['token', 'password'])
@Validate(PmgOhneTokenConstraint)
authMethod!: 'token' | 'password';
@ValidateIf((o) => o.authMethod === 'token')
@IsString()
@IsNotEmpty()
tokenId?: string;
@ValidateIf((o) => o.authMethod === 'token')
@IsString()
@IsNotEmpty()
tokenSecret?: string;
@ValidateIf((o) => o.authMethod === 'password')
@IsString()
@IsNotEmpty()
username?: string;
@ValidateIf((o) => o.authMethod === 'password')
@IsString()
@IsNotEmpty()
password?: string;
@IsBoolean()
@IsOptional()
tlsRejectUnauthorized?: boolean;
@IsInt()
@Min(1)
@Max(1440)
@IsOptional()
pollIntervalMin?: number;
@IsBoolean()
@IsOptional()
isActive?: boolean;
}
/**
* DTO fuer das Bearbeiten (Aufgabe 5). Alle Felder optional; ein NICHT
* gesendetes Geheimnisfeld laesst den gespeicherten Wert unveraendert, eine
* LEERE Zeichenkette bedeutet "loeschen" (Muster `LdapConfigService.updateConfig`)
* — diese Unterscheidung lebt im Service, nicht im DTO, deshalb bleiben
* `tokenSecret`/`password` hier einfache optionale Zeichenketten ohne
* `IsNotEmpty`.
*/
export class UpdateProxmoxServerDto {
@IsString()
@IsNotEmpty()
@IsOptional()
name?: string;
@IsIn(['pve', 'pbs', 'pmg'])
@IsOptional()
productType?: 'pve' | 'pbs' | 'pmg';
@IsUrl({ protocols: ['http', 'https'], require_tld: false })
@IsOptional()
baseUrl?: string;
@IsIn(['token', 'password'])
@Validate(PmgOhneTokenConstraint)
@IsOptional()
authMethod?: 'token' | 'password';
@IsString()
@IsOptional()
tokenId?: string;
@IsString()
@IsOptional()
tokenSecret?: string;
@IsString()
@IsOptional()
username?: string;
@IsString()
@IsOptional()
password?: string;
@IsBoolean()
@IsOptional()
tlsRejectUnauthorized?: boolean;
@IsInt()
@Min(1)
@Max(1440)
@IsOptional()
pollIntervalMin?: number;
@IsBoolean()
@IsOptional()
isActive?: boolean;
}
/**
* DTO fuer den Verbindungstest (Nachbesserung Befund 1, Rundgang zu Aufgabe 4):
* derselbe Feldsatz wie `UpdateProxmoxServerDto` — der Test soll auf JEDEM
* dieser Felder den ungespeicherten Formularwert pruefen koennen, nicht den
* gespeicherten Stand. Ein NICHT gesendetes oder leeres Geheimnisfeld heisst
* "gespeicherten Wert weiterverwenden" (Merge-Logik in
* `ProxmoxService.resolveEffectiveTestServer`), genau wie beim Bearbeiten.
* Fuer die Neuanlage (noch kein gespeicherter Server) bleiben alle Felder
* optional, weil es dort keinen gespeicherten Fallback gibt — ein fehlendes
* Pflichtfeld fuehrt dort einfach zum selben Fehlerschluessel wie ein leer
* gelassenes Feld beim Anlegen selbst (z. B. `zugang` ohne Geheimnis).
*/
export class TestProxmoxServerDto extends UpdateProxmoxServerDto {}