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>
This commit is contained in:
@@ -6,9 +6,33 @@ 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, UpdateProxmoxServerDto } from './dto/proxmox-server.dto';
|
||||
import type {
|
||||
CreateProxmoxServerDto,
|
||||
TestProxmoxServerDto,
|
||||
UpdateProxmoxServerDto,
|
||||
} from './dto/proxmox-server.dto';
|
||||
import type { ProxmoxErrorKind, ProxmoxPollResult, ProxmoxProductType } from './proxmox.types';
|
||||
|
||||
/**
|
||||
* Nur die Felder, die eine Abfrage tatsaechlich braucht (Nachbesserung
|
||||
* Befund 1) — `pollOne`/`buildAuthHeaders`/`getWithRetry` nehmen diesen
|
||||
* schmalen Ausschnitt statt der vollen `ProxmoxServer`-Zeile entgegen, damit
|
||||
* `resolveEffectiveTestServer` unten eine rein im Speicher gebaute Mischung
|
||||
* aus Formular- und gespeicherten Werten uebergeben kann, ohne eine
|
||||
* vollstaendige Datenbankzeile vorzutaeuschen.
|
||||
*/
|
||||
type ProxmoxCredentialSource = Pick<
|
||||
ProxmoxServer,
|
||||
| 'productType'
|
||||
| 'baseUrl'
|
||||
| 'authMethod'
|
||||
| 'tokenId'
|
||||
| 'encryptedTokenSecret'
|
||||
| 'username'
|
||||
| 'encryptedPassword'
|
||||
| 'tlsRejectUnauthorized'
|
||||
>;
|
||||
|
||||
/**
|
||||
* Erkennungsform fuer "schon verschluesselt" — woertlich aus
|
||||
* `ldap-config.service.ts:17` uebernommen (Format `iv:authTag:ciphertext`,
|
||||
@@ -217,7 +241,7 @@ export class ProxmoxService {
|
||||
* Ticket-Anmeldung aus (die einzige nicht-lesende Anfrage des Moduls,
|
||||
* D-01) — deshalb `async`.
|
||||
*/
|
||||
private async buildAuthHeaders(server: ProxmoxServer): Promise<AuthHeaderResult> {
|
||||
private async buildAuthHeaders(server: ProxmoxCredentialSource): Promise<AuthHeaderResult> {
|
||||
if (server.authMethod === 'token') {
|
||||
const tokenSecret = this.decryptSecret(server.encryptedTokenSecret);
|
||||
if (!server.tokenId || !tokenSecret) {
|
||||
@@ -273,7 +297,7 @@ export class ProxmoxService {
|
||||
* zwei Stunden einen Fehlalarm. Ein zweites 401 bleibt `'zugang'`.
|
||||
*/
|
||||
private async getWithRetry(
|
||||
server: ProxmoxServer,
|
||||
server: ProxmoxCredentialSource,
|
||||
session: { headers: Record<string, string>; retried: boolean },
|
||||
path: string,
|
||||
): Promise<ProxmoxGetResult> {
|
||||
@@ -306,7 +330,7 @@ export class ProxmoxService {
|
||||
* `pbs` und `pmg` (Aufgabe 3), jeweils mit Token ODER Benutzer/Passwort
|
||||
* (Aufgabe 2).
|
||||
*/
|
||||
private async pollOne(server: ProxmoxServer): Promise<ProxmoxPollResult> {
|
||||
private async pollOne(server: ProxmoxCredentialSource): Promise<ProxmoxPollResult> {
|
||||
const authHeaders = await this.buildAuthHeaders(server);
|
||||
if (!authHeaders.ok) {
|
||||
return {
|
||||
@@ -491,21 +515,79 @@ export class ProxmoxService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Verbindungstest (Aufgabe 4, `<behavior>`, `POST servers/:id/test`):
|
||||
* benutzt denselben Klienten und dieselbe Fehleruebersetzung wie der
|
||||
* Mischt Formularwerte (`dto`, ungespeichert) mit dem gespeicherten Server
|
||||
* (`existing`, `null` bei der Neuanlage) zu genau den Feldern, die eine
|
||||
* Abfrage braucht (Nachbesserung Befund 1). Zwei Regeln, je nachdem, ob
|
||||
* das Formular das Feld beim Laden vorbefuellt (Vorbild `serverToForm`):
|
||||
*
|
||||
* - Normale Felder (`productType`, `baseUrl`, `authMethod`, `tokenId`,
|
||||
* `username`, `tlsRejectUnauthorized`): das Formular zeigt immer den
|
||||
* zuletzt gespeicherten Wert an, bis der Nutzer ihn aendert — ein vom
|
||||
* Aufrufer GESENDETES Feld gilt also als Formularwert, auch wenn es
|
||||
* absichtlich geleert wurde (`tokenId: ''` -> `null`). Nur ein NICHT
|
||||
* gesendetes Feld (Aufrufer ohne diesen Schluessel im Body) faellt auf
|
||||
* den gespeicherten Wert zurueck.
|
||||
* - Geheimnisfelder (`tokenSecret`/`password`): `ServerForm` befuellt
|
||||
* diese beim Laden bewusst NIE aus der Datenbank (Geheimnis nie im
|
||||
* Klartext anzeigen). Ein leeres Feld bedeutet hier deshalb NICHT
|
||||
* "Nutzer will loeschen", sondern "Nutzer hat nichts eingetippt" ->
|
||||
* gespeicherten (verschluesselten) Wert weiterverwenden. Ein gefuelltes
|
||||
* Feld ist der eingetippte Klartext und wird unveraendert durchgereicht;
|
||||
* `decryptSecret()` erkennt anhand der Form `iv:authTag:ciphertext`
|
||||
* automatisch, ob entschluesselt werden muss, und laesst Klartext sonst
|
||||
* unangetastet.
|
||||
*/
|
||||
private resolveEffectiveTestServer(
|
||||
existing: ProxmoxServer | null,
|
||||
dto: TestProxmoxServerDto,
|
||||
): ProxmoxCredentialSource {
|
||||
return {
|
||||
productType: dto.productType ?? existing?.productType ?? 'pve',
|
||||
baseUrl: dto.baseUrl ?? existing?.baseUrl ?? '',
|
||||
authMethod: dto.authMethod ?? existing?.authMethod ?? 'token',
|
||||
tokenId: dto.tokenId !== undefined ? dto.tokenId || null : (existing?.tokenId ?? null),
|
||||
encryptedTokenSecret: dto.tokenSecret
|
||||
? dto.tokenSecret
|
||||
: (existing?.encryptedTokenSecret ?? null),
|
||||
username: dto.username !== undefined ? dto.username || null : (existing?.username ?? null),
|
||||
encryptedPassword: dto.password ? dto.password : (existing?.encryptedPassword ?? null),
|
||||
tlsRejectUnauthorized: dto.tlsRejectUnauthorized ?? existing?.tlsRejectUnauthorized ?? true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Verbindungstest fuer einen GESPEICHERTEN Server (Aufgabe 4, `<behavior>`,
|
||||
* `POST servers/:id/test`; Nachbesserung Befund 1: prueft jetzt die
|
||||
* Formularwerte aus `dto`, nicht mehr blind den gespeicherten Stand).
|
||||
* Benutzt denselben Klienten und dieselbe Fehleruebersetzung wie der
|
||||
* Planer, schreibt aber NICHT ins Zwischenlager — ein Testklick darf den
|
||||
* zuletzt gemessenen Stand nicht ueberschreiben (Vorbild
|
||||
* `TenderEmailConfigService.testConnection`/LDAP-Test). Keine
|
||||
* Zehn-Sekunden-Sperre: ein Test ist ein bewusster Einzelklick, kein
|
||||
* automatisierter Auffrischungsweg.
|
||||
*/
|
||||
async testConnection(tenantId: string, serverId: string): Promise<ProxmoxPollResult | null> {
|
||||
async testConnection(
|
||||
tenantId: string,
|
||||
serverId: string,
|
||||
dto: TestProxmoxServerDto = {},
|
||||
): Promise<ProxmoxPollResult | null> {
|
||||
const tenantPrisma = forTenant(this.prisma, tenantId);
|
||||
const server = await tenantPrisma.proxmoxServer.findUnique({ where: { id: serverId } });
|
||||
if (!server || server.tenantId !== tenantId) {
|
||||
return null;
|
||||
}
|
||||
return this.pollOne(server);
|
||||
return this.pollOne(this.resolveEffectiveTestServer(server, dto));
|
||||
}
|
||||
|
||||
/**
|
||||
* Verbindungstest waehrend der Neuanlage (Nachbesserung Befund 1,
|
||||
* `POST servers/test`, ohne `:id`) — es gibt noch keinen gespeicherten
|
||||
* Server, also ausschliesslich die Formularwerte aus `dto`. Fehlende
|
||||
* Pflichtangaben (z. B. kein Geheimnis) fuehren zum selben Fehlerschluessel
|
||||
* wie beim Abfragen eines gespeicherten Servers ohne Zugang (`zugang`).
|
||||
*/
|
||||
async testDraftConnection(dto: TestProxmoxServerDto): Promise<ProxmoxPollResult> {
|
||||
return this.pollOne(this.resolveEffectiveTestServer(null, dto));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user