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:
@@ -0,0 +1,6 @@
|
||||
import { ModuleAccessGate } from '@/components/modules/module-access-gate';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export default function ProxmoxLayout({ children }: { children: ReactNode }) {
|
||||
return <ModuleAccessGate moduleSlug="proxmox">{children}</ModuleAccessGate>;
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { listServers, type ProxmoxServer } from '@/lib/proxmox-api';
|
||||
|
||||
/**
|
||||
* Modulseite (Aufgabe 1: duenne Liste — Name, Typ, Adresse; Aufgabe 6
|
||||
* ergaenzt die produktabhaengige Auslastungsanzeige ueber `ServerCard`).
|
||||
* Liest ausschliesslich aus dem Zwischenlager, das `GET servers` liefert —
|
||||
* kein Live-Zugriff bei Proxmox von hier aus (D-05).
|
||||
*/
|
||||
export default function ProxmoxPage() {
|
||||
const t = useTranslations('proxmox');
|
||||
const [servers, setServers] = useState<ProxmoxServer[] | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
listServers()
|
||||
.then((data) => {
|
||||
if (!cancelled) setServers(data);
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) setError(t('loadError'));
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [t]);
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-3xl space-y-6 p-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold tracking-tight">{t('title')}</h1>
|
||||
<p className="mt-1 text-sm text-muted-foreground">{t('description')}</p>
|
||||
</div>
|
||||
|
||||
{error && <p className="text-sm text-destructive">{error}</p>}
|
||||
|
||||
{!error && servers === null && (
|
||||
<p className="text-sm text-muted-foreground">{t('loading')}</p>
|
||||
)}
|
||||
|
||||
{!error && servers !== null && servers.length === 0 && (
|
||||
<div className="rounded-lg border border-border bg-card p-6 text-sm text-muted-foreground shadow-sm">
|
||||
{t('emptyState')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!error && servers !== null && servers.length > 0 && (
|
||||
<ul className="space-y-3">
|
||||
{servers.map((server) => (
|
||||
<li
|
||||
key={server.id}
|
||||
className="rounded-lg border border-border bg-card p-4 shadow-sm"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-medium">{server.name}</span>
|
||||
<span className="text-xs uppercase text-muted-foreground">
|
||||
{server.productType}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 text-sm text-muted-foreground">{server.baseUrl}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -53,6 +53,12 @@ export const MODULE_REGISTRY: Record<string, ModuleRegistryEntry> = {
|
||||
{ ssr: false },
|
||||
),
|
||||
},
|
||||
proxmox: {
|
||||
component: dynamic(
|
||||
() => import('@/app/(portal)/modules/proxmox/page'),
|
||||
{ ssr: false },
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* Proxmox Module API client (260923-dhh). Konsumiert `/modules/proxmox/*`.
|
||||
* Vorbild `dkv-api.ts`: `credentials: 'include'` fuer Cookie-Auth,
|
||||
* `NEXT_PUBLIC_API_URL` als Basis.
|
||||
*
|
||||
* Sicherheit (T-DHH-01): keine Antwort dieses Clients enthaelt jemals ein
|
||||
* Geheimnisfeld — der Server waehlt `encryptedTokenSecret`/`encryptedPassword`
|
||||
* per `select` gar nicht erst aus.
|
||||
*/
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
|
||||
|
||||
export type ProxmoxProductType = 'pve' | 'pbs' | 'pmg';
|
||||
export type ProxmoxAuthMethod = 'token' | 'password';
|
||||
export type ProxmoxErrorKind =
|
||||
| 'netz'
|
||||
| 'zugang'
|
||||
| 'rechte'
|
||||
| 'zertifikat'
|
||||
| 'antwortform'
|
||||
| 'server'
|
||||
| 'unbekannt';
|
||||
|
||||
export interface ProxmoxServerStatus {
|
||||
id: string;
|
||||
serverId: string;
|
||||
lastPolledAt: string | null;
|
||||
lastOkAt: string | null;
|
||||
reachable: boolean;
|
||||
errorKind: ProxmoxErrorKind | null;
|
||||
errorDetail: string | null;
|
||||
metrics: unknown;
|
||||
rawSample: unknown;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface ProxmoxServer {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
name: string;
|
||||
productType: ProxmoxProductType;
|
||||
baseUrl: string;
|
||||
authMethod: ProxmoxAuthMethod;
|
||||
tokenId: string | null;
|
||||
username: string | null;
|
||||
tlsRejectUnauthorized: boolean;
|
||||
isActive: boolean;
|
||||
pollIntervalMin: number;
|
||||
position: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
status: ProxmoxServerStatus | null;
|
||||
}
|
||||
|
||||
export interface CreateProxmoxServerPayload {
|
||||
name: string;
|
||||
productType: ProxmoxProductType;
|
||||
baseUrl: string;
|
||||
authMethod: ProxmoxAuthMethod;
|
||||
tokenId?: string;
|
||||
tokenSecret?: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
tlsRejectUnauthorized?: boolean;
|
||||
pollIntervalMin?: number;
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
export type UpdateProxmoxServerPayload = Partial<CreateProxmoxServerPayload>;
|
||||
|
||||
/** GET /modules/proxmox/servers — Serverliste samt Zwischenlager. */
|
||||
export async function listServers(): Promise<ProxmoxServer[]> {
|
||||
const res = await fetch(`${API_URL}/modules/proxmox/servers`, {
|
||||
credentials: 'include',
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to fetch proxmox servers');
|
||||
return res.json();
|
||||
}
|
||||
|
||||
/** POST /modules/proxmox/servers — Server anlegen (ADMIN/SUPER_ADMIN). */
|
||||
export async function createServer(
|
||||
payload: CreateProxmoxServerPayload,
|
||||
): Promise<ProxmoxServer> {
|
||||
const res = await fetch(`${API_URL}/modules/proxmox/servers`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to create proxmox server');
|
||||
return res.json();
|
||||
}
|
||||
|
||||
/** POST /modules/proxmox/servers/:id/poll — sofortige Abfrage (ADMIN/SUPER_ADMIN). */
|
||||
export async function pollServer(id: string): Promise<unknown> {
|
||||
const res = await fetch(`${API_URL}/modules/proxmox/servers/${id}/poll`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to poll proxmox server');
|
||||
return res.json();
|
||||
}
|
||||
@@ -703,6 +703,13 @@
|
||||
"checking": "Prüfe...",
|
||||
"error": "Fehler bei der Prüfung"
|
||||
},
|
||||
"proxmox": {
|
||||
"title": "Proxmox",
|
||||
"description": "Zustand Ihrer Proxmox-Server (PVE/PBS/PMG) auf einen Blick — Tessera schaut nur zu, es verändert nichts.",
|
||||
"loading": "Lade Serverliste...",
|
||||
"loadError": "Die Serverliste konnte nicht geladen werden.",
|
||||
"emptyState": "Noch kein Server eingetragen. Legen Sie in den Moduleinstellungen einen Server an."
|
||||
},
|
||||
"dkvFleet": {
|
||||
"pageTitle": "DKV-Rechnung",
|
||||
"checkNow": "Jetzt prüfen",
|
||||
|
||||
@@ -703,6 +703,13 @@
|
||||
"checking": "Checking...",
|
||||
"error": "Error checking domain"
|
||||
},
|
||||
"proxmox": {
|
||||
"title": "Proxmox",
|
||||
"description": "State of your Proxmox servers (PVE/PBS/PMG) at a glance — Tessera only observes, it never changes anything.",
|
||||
"loading": "Loading server list...",
|
||||
"loadError": "Could not load the server list.",
|
||||
"emptyState": "No server configured yet. Add one in the module settings."
|
||||
},
|
||||
"dkvFleet": {
|
||||
"pageTitle": "DKV Invoice",
|
||||
"checkNow": "Check Now",
|
||||
|
||||
Reference in New Issue
Block a user