feat(260923-dhh): Proxmox-Modul Aufgabe 5 - Einstellungsseite (anlegen, bearbeiten, loeschen, testen)

- proxmox.service.ts: updateServer (Muster LdapConfigService.updateConfig
  - nicht gesendet laesst unveraendert, leer loescht, gefuellt
  verschluesselt neu; PMG+Token auch beim Bearbeiten abgelehnt, geprueft
  gegen den EFFEKTIVEN Stand nach Zusammenfuehren), deleteServer
- proxmox.controller.ts: PUT/DELETE servers/:id, beide zusaetzlich mit
  scheduler.refreshTenant() nach dem Schreiben
- Frontend: proxmox-api.ts (updateServer/deleteServer/testServer),
  settings/page.tsx (Rollenpruefung nur Anzeige, Serverliste,
  Loeschen mit Rueckfrage), ServerForm.tsx (PMG bietet Token gar nicht
  an, Geheimnisfelder nie vorbefuellt, Zertifikatspruefung-Schalter
  Standard "pruefen", Verbindungstest mit Klartext-Fehlertext)
- umlaut-dictionary.ts: zwei neue, bereits korrekte Woerter
  (bewusst/gemessene) auf die Positivliste des Regressions-Waechters

Tore: api 1311/1311 (>=1240), web 701/701 (>=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 14:41:21 +02:00
parent fccaf8db0f
commit 723cf6814b
10 changed files with 1112 additions and 10 deletions
+69 -2
View File
@@ -1,4 +1,4 @@
import { Injectable, Logger } from '@nestjs/common';
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
import type { ProxmoxServer } from '@prisma/client';
import { CryptoService } from '../crypto/crypto.service';
import { PrismaService } from '../prisma/prisma.service';
@@ -6,7 +6,7 @@ import { forSystem, forTenant } from '../prisma/prisma-tenant.extension';
import { buildTicketCookieHeader, buildTokenAuthHeader, loginTicket } from './proxmox-auth';
import { proxmoxGet, type ProxmoxGetResult } from './proxmox-client.service';
import { listPbsDatastoreNames, normalizePbs, normalizePmg, normalizePve } from './proxmox-normalize';
import type { CreateProxmoxServerDto } from './dto/proxmox-server.dto';
import type { CreateProxmoxServerDto, UpdateProxmoxServerDto } from './dto/proxmox-server.dto';
import type { ProxmoxErrorKind, ProxmoxPollResult, ProxmoxProductType } from './proxmox.types';
/**
@@ -144,6 +144,73 @@ export class ProxmoxService {
});
}
/**
* Bearbeiten (Aufgabe 5). Dieselbe Regel wie
* `LdapConfigService.updateConfig`: ein NICHT gesendetes Geheimnisfeld
* laesst den gespeicherten Wert unveraendert, eine LEERE Zeichenkette
* bedeutet "loeschen", ein gefuellter Wert wird neu verschluesselt. Die
* Ablehnung "PMG plus Token" gilt auch hier — geprueft gegen den
* EFFEKTIVEN Stand nach dem Zusammenfuehren mit der vorhandenen Zeile,
* nicht nur gegen die gesendeten Felder (ein Teil-Update, das nur
* `authMethod` aendert, wuerde die DTO-eigene Pruefung sonst umgehen,
* weil `productType` in diesem Aufruf gar nicht gesendet wird).
*/
async updateServer(tenantId: string, serverId: string, dto: UpdateProxmoxServerDto) {
const tenantPrisma = forTenant(this.prisma, tenantId);
const existing = await tenantPrisma.proxmoxServer.findUnique({ where: { id: serverId } });
if (!existing || existing.tenantId !== tenantId) {
return null;
}
const effectiveProductType = dto.productType ?? existing.productType;
const effectiveAuthMethod = dto.authMethod ?? existing.authMethod;
if (effectiveProductType === 'pmg' && effectiveAuthMethod === 'token') {
throw new BadRequestException(
'PMG unterstuetzt keinen API-Token-Zugang. Bitte Benutzer und Passwort waehlen.',
);
}
const data: Record<string, unknown> = {};
if (dto.name !== undefined) data.name = dto.name;
if (dto.productType !== undefined) data.productType = dto.productType;
if (dto.baseUrl !== undefined) data.baseUrl = dto.baseUrl;
if (dto.authMethod !== undefined) data.authMethod = dto.authMethod;
if (dto.tokenId !== undefined) data.tokenId = dto.tokenId || null;
if (dto.tokenSecret !== undefined) {
data.encryptedTokenSecret = dto.tokenSecret ? this.crypto.encrypt(dto.tokenSecret) : null;
}
if (dto.username !== undefined) data.username = dto.username || null;
if (dto.password !== undefined) {
data.encryptedPassword = dto.password ? this.crypto.encrypt(dto.password) : null;
}
if (dto.tlsRejectUnauthorized !== undefined) {
data.tlsRejectUnauthorized = dto.tlsRejectUnauthorized;
}
if (dto.pollIntervalMin !== undefined) data.pollIntervalMin = dto.pollIntervalMin;
if (dto.isActive !== undefined) data.isActive = dto.isActive;
return tenantPrisma.proxmoxServer.update({
where: { id: serverId },
data,
select: SAFE_SERVER_SELECT,
});
}
/**
* Loeschen (Aufgabe 5) — entfernt den Server samt Zwischenlagerzeile
* (Fremdschluessel mit Loeschweitergabe, `onDelete: Cascade`). Liefert
* `false`, wenn der Server unter diesem Mandanten nicht existiert.
*/
async deleteServer(tenantId: string, serverId: string): Promise<boolean> {
const tenantPrisma = forTenant(this.prisma, tenantId);
const existing = await tenantPrisma.proxmoxServer.findUnique({ where: { id: serverId } });
if (!existing || existing.tenantId !== tenantId) {
return false;
}
await tenantPrisma.proxmoxServer.delete({ where: { id: serverId } });
return true;
}
/**
* Baut die Anmeldekopfzeile fuer GENAU diesen Server ueber
* `proxmox-auth.ts` (D-03). Beim Passwort-Zweig loest das eine