feat(260923-dhh): Proxmox-Modul Aufgabe 1 - PVE per Token, Ende-zu-Ende

- ProxmoxServer/ProxmoxServerStatus mit RLS (tenant_isolation_policy +
  system_read_policy auf ProxmoxServer fuer den kommenden Planer)
- proxmox-auth.ts (Token-Kopfzeilen PVE/PBS), proxmox-client.service.ts
  (proxmoxGet, ausschliesslich lesend, Dispatcher je Aufruf aus
  tlsRejectUnauthorized, nie global)
- proxmox.service.ts: Server anlegen (Geheimnis verschluesselt,
  select ohne Geheimnisfelder), Serverliste, PVE-Abfrage mit
  nachsichtiger Grundauswertung (Knoten/Gaeste)
- Controller/Modul/Seed nach Domaincheck-Vorbild, Kategorie
  "infrastructure", @UseModule('proxmox') + @Roles auf Schreibwegen
- Modulseite (duenne Liste) + proxmox-api.ts + Registrierung in
  MODULE_REGISTRY
- Zugriffsklassifikation nachgezogen (rls-access-inventory.spec.ts gruen)

Tore: api 1247/1247 (>=1240), web 693/693, type-check 4/4, lint 5/5,
Biome apps/web 53 Warnungen (unveraendert).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-23 10:17:33 +02:00
parent ec9c77956d
commit 3a1bfd943e
19 changed files with 1528 additions and 4 deletions
@@ -0,0 +1,149 @@
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;
}