Files
tessera-ctl/apps/web/src/lib/custom-modules-api.ts
T
schalli ee97b4ed9f feat(260929-dzu): Einstellungen > Eigene Module fuer jeden Benutzer, Verwaltung nur gemeinsam
- 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>
2026-09-29 10:17:22 +02:00

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);
}