ee97b4ed9f
- gemeinsame Oberflaeche (CustomModuleManager, Formular, Loeschdialog) fuer Verwaltung und Einstellungen - Einstellungen: persoenliche Eintraege, Verwaltung sendet shared: true und zeigt nur gemeinsame - Navigation, Texte de/en, Komponententests Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
128 lines
3.9 KiB
TypeScript
128 lines
3.9 KiB
TypeScript
/**
|
|
* Eigene Module — API-Client (quick-260929-9wc). Konsumiert `/custom-modules`.
|
|
* Muster `favorites-api.ts`/`proxmox-api.ts`: `credentials: 'include'` fuer
|
|
* Cookie-Auth, `NEXT_PUBLIC_API_URL` als Basis.
|
|
*/
|
|
|
|
import { isHttpsUrl } from '@/components/dashboard/widgets/xframe-config';
|
|
|
|
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
|
|
|
|
export interface CustomModule {
|
|
id: string;
|
|
name: string;
|
|
url: string;
|
|
category: string;
|
|
/** `true` = persoenlicher Eintrag des angemeldeten Benutzers, `false` = gemeinsam (vom Administrator). */
|
|
personal: boolean;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
}
|
|
|
|
export interface CustomModuleInput {
|
|
name: string;
|
|
url: string;
|
|
category: string;
|
|
}
|
|
|
|
/**
|
|
* Beim Anlegen: `shared: true` legt einen gemeinsamen Eintrag fuer alle an
|
|
* (nur Administratoren, sonst 403); ohne das Feld ist der Eintrag persoenlich.
|
|
*/
|
|
export interface CustomModuleCreateInput extends CustomModuleInput {
|
|
shared?: boolean;
|
|
}
|
|
|
|
/** Fehler mit HTTP-Status und Servermeldung (falls vorhanden). */
|
|
export class CustomModuleRequestError extends Error {
|
|
constructor(
|
|
readonly status: number,
|
|
message: string,
|
|
) {
|
|
super(message);
|
|
this.name = 'CustomModuleRequestError';
|
|
}
|
|
}
|
|
|
|
/** Ergebnis der Adresspruefung im Web — dieselbe Regel wie die API. */
|
|
export type CustomModuleUrlCheck = 'ok' | 'notHttps' | 'credentials';
|
|
|
|
/**
|
|
* Gueltig ist nur eine https-Adresse ohne Zugangsdaten. Die https-Regel ist
|
|
* EINE im ganzen Web (`isHttpsUrl` aus `xframe-config.ts`); Zugangsdaten
|
|
* erkennt der URL-Parser an `username`/`password`.
|
|
*/
|
|
export function checkCustomModuleUrl(value: string): CustomModuleUrlCheck {
|
|
if (!isHttpsUrl(value)) return 'notHttps';
|
|
try {
|
|
const parsed = new URL(value);
|
|
if (parsed.username !== '' || parsed.password !== '') return 'credentials';
|
|
if (parsed.hostname === '') return 'notHttps';
|
|
} catch {
|
|
return 'notHttps';
|
|
}
|
|
return 'ok';
|
|
}
|
|
|
|
async function failure(res: Response): Promise<CustomModuleRequestError> {
|
|
let message = `Request failed (${res.status})`;
|
|
try {
|
|
const body = await res.json();
|
|
const raw = body?.message;
|
|
if (Array.isArray(raw)) message = raw.join(' ');
|
|
else if (typeof raw === 'string') message = raw;
|
|
} catch {
|
|
// Antwort ohne JSON-Koerper — Standardmeldung bleibt.
|
|
}
|
|
return new CustomModuleRequestError(res.status, message);
|
|
}
|
|
|
|
export async function listCustomModules(): Promise<CustomModule[]> {
|
|
const res = await fetch(`${API_URL}/custom-modules`, { credentials: 'include' });
|
|
if (!res.ok) throw await failure(res);
|
|
return res.json();
|
|
}
|
|
|
|
/** `null` bei 404 (Eintrag geloescht oder fremd). */
|
|
export async function getCustomModule(id: string): Promise<CustomModule | null> {
|
|
const res = await fetch(`${API_URL}/custom-modules/${encodeURIComponent(id)}`, {
|
|
credentials: 'include',
|
|
});
|
|
if (res.status === 404) return null;
|
|
if (!res.ok) throw await failure(res);
|
|
return res.json();
|
|
}
|
|
|
|
export async function createCustomModule(input: CustomModuleCreateInput): Promise<CustomModule> {
|
|
const res = await fetch(`${API_URL}/custom-modules`, {
|
|
method: 'POST',
|
|
credentials: 'include',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(input),
|
|
});
|
|
if (!res.ok) throw await failure(res);
|
|
return res.json();
|
|
}
|
|
|
|
export async function updateCustomModule(
|
|
id: string,
|
|
input: Partial<CustomModuleInput>,
|
|
): Promise<CustomModule> {
|
|
const res = await fetch(`${API_URL}/custom-modules/${encodeURIComponent(id)}`, {
|
|
method: 'PATCH',
|
|
credentials: 'include',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(input),
|
|
});
|
|
if (!res.ok) throw await failure(res);
|
|
return res.json();
|
|
}
|
|
|
|
export async function deleteCustomModule(id: string): Promise<void> {
|
|
const res = await fetch(`${API_URL}/custom-modules/${encodeURIComponent(id)}`, {
|
|
method: 'DELETE',
|
|
credentials: 'include',
|
|
});
|
|
if (!res.ok) throw await failure(res);
|
|
}
|