/** * 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 { 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 { 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 { 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 { 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, ): Promise { 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 { const res = await fetch(`${API_URL}/custom-modules/${encodeURIComponent(id)}`, { method: 'DELETE', credentials: 'include', }); if (!res.ok) throw await failure(res); }