feat: Phase 3 – Modul-System (Manifest, ZIP-Installation, Lifecycle, Prozess-Manager)
This commit is contained in:
100
apps/platform-frontend/src/lib/modules-api.ts
Normal file
100
apps/platform-frontend/src/lib/modules-api.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { apiRequest, ApiError } from './api-client';
|
||||
import { moduleSchema, type Module } from './schemas';
|
||||
|
||||
/** Typsichere API-Funktionen für die Modul-Verwaltung. */
|
||||
|
||||
export async function fetchModules(): Promise<Module[]> {
|
||||
const response = await apiRequest<{ modules: unknown[] }>('/api/v1/modules');
|
||||
return response.modules.map((module) => moduleSchema.parse(module));
|
||||
}
|
||||
|
||||
export async function installModule(file: File): Promise<Module> {
|
||||
const formData = new FormData();
|
||||
formData.append('package', file);
|
||||
|
||||
const headers: Record<string, string> = {};
|
||||
const csrfToken = readCsrfToken();
|
||||
if (csrfToken) {
|
||||
headers['X-CSRF-Token'] = csrfToken;
|
||||
}
|
||||
|
||||
const response = await fetch('/api/v1/modules/install', {
|
||||
method: 'POST',
|
||||
headers,
|
||||
credentials: 'same-origin',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
let message = 'Installation fehlgeschlagen';
|
||||
let details: Record<string, string | string[]> | undefined;
|
||||
try {
|
||||
const errorBody = (await response.json()) as {
|
||||
message?: string | string[];
|
||||
details?: Record<string, string | string[]>;
|
||||
};
|
||||
if (typeof errorBody.message === 'string') {
|
||||
message = errorBody.message;
|
||||
} else if (Array.isArray(errorBody.message)) {
|
||||
message = errorBody.message.join(', ');
|
||||
}
|
||||
details = errorBody.details;
|
||||
} catch {
|
||||
// Kein JSON in der Antwort – Standardmeldung verwenden.
|
||||
}
|
||||
throw new ApiError(response.status, message, details);
|
||||
}
|
||||
|
||||
const response_ = (await response.json()) as { module: unknown };
|
||||
return moduleSchema.parse(response_.module);
|
||||
}
|
||||
|
||||
export async function startModule(id: string): Promise<Module> {
|
||||
const response = await apiRequest<{ module: unknown }>(`/api/v1/modules/${id}/start`, {
|
||||
method: 'POST',
|
||||
});
|
||||
return moduleSchema.parse(response.module);
|
||||
}
|
||||
|
||||
export async function stopModule(id: string): Promise<Module> {
|
||||
const response = await apiRequest<{ module: unknown }>(`/api/v1/modules/${id}/stop`, {
|
||||
method: 'POST',
|
||||
});
|
||||
return moduleSchema.parse(response.module);
|
||||
}
|
||||
|
||||
export async function restartModule(id: string): Promise<Module> {
|
||||
const response = await apiRequest<{ module: unknown }>(`/api/v1/modules/${id}/restart`, {
|
||||
method: 'POST',
|
||||
});
|
||||
return moduleSchema.parse(response.module);
|
||||
}
|
||||
|
||||
export async function setModuleEnabled(id: string, enabled: boolean): Promise<Module> {
|
||||
const response = await apiRequest<{ module: unknown }>(`/api/v1/modules/${id}/enabled`, {
|
||||
method: 'PATCH',
|
||||
body: { enabled },
|
||||
});
|
||||
return moduleSchema.parse(response.module);
|
||||
}
|
||||
|
||||
export async function removeModule(id: string): Promise<void> {
|
||||
await apiRequest(`/api/v1/modules/${id}`, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
export async function checkModuleHealth(
|
||||
id: string,
|
||||
): Promise<{ healthy: boolean; latencyMs: number; detail: string }> {
|
||||
return apiRequest(`/api/v1/modules/${id}/health`);
|
||||
}
|
||||
|
||||
/** Liest das CSRF-Token aus dem nicht-HttpOnly-Cookie. */
|
||||
function readCsrfToken(): string | null {
|
||||
for (const part of document.cookie.split(';')) {
|
||||
const [name, ...value] = part.trim().split('=');
|
||||
if (name === 'mpm_csrf') {
|
||||
return decodeURIComponent(value.join('='));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -106,4 +106,33 @@ export const changePasswordSchema = z
|
||||
message: 'Passwörter stimmen nicht überein',
|
||||
path: ['confirmPassword'],
|
||||
});
|
||||
export type ChangePasswordDto = z.infer<typeof changePasswordSchema>;
|
||||
export type ChangePasswordDto = z.infer<typeof changePasswordSchema>;
|
||||
|
||||
/** Modul-Lifecycle-Zustände (muss zum Backend passen). */
|
||||
export const MODULE_STATUSES = [
|
||||
'INSTALLED',
|
||||
'STARTING',
|
||||
'RUNNING',
|
||||
'STOPPING',
|
||||
'STOPPED',
|
||||
'ERROR',
|
||||
'DISABLED',
|
||||
] as const;
|
||||
export type ModuleStatus = (typeof MODULE_STATUSES)[number];
|
||||
|
||||
/** Modul-Datensatz der Admin-API (/api/v1/modules). */
|
||||
export const moduleSchema = z.object({
|
||||
id: z.string(),
|
||||
moduleId: z.string(),
|
||||
name: z.string(),
|
||||
slug: z.string(),
|
||||
version: z.string(),
|
||||
description: z.string(),
|
||||
author: z.string(),
|
||||
status: z.enum(MODULE_STATUSES),
|
||||
internalPort: z.number(),
|
||||
healthcheckUrl: z.string(),
|
||||
enabled: z.boolean(),
|
||||
createdAt: z.string(),
|
||||
});
|
||||
export type Module = z.infer<typeof moduleSchema>;
|
||||
Reference in New Issue
Block a user