feat: marketplace modules and isolated container management

This commit is contained in:
ayde64
2026-10-07 23:18:02 +02:00
parent 564f7def1c
commit 7b121fa908
68 changed files with 2560 additions and 699 deletions

View File

@@ -19,13 +19,14 @@ export class ApiError extends Error {
/** Liest das CSRF-Token aus dem nicht-HttpOnly-Cookie. */
function readCsrfToken(): string | null {
let csrfToken: string | null = null;
for (const part of document.cookie.split(';')) {
const [name, ...value] = part.trim().split('=');
if (name === 'mpm_csrf') {
return decodeURIComponent(value.join('='));
csrfToken = decodeURIComponent(value.join('='));
}
}
return null;
return csrfToken;
}
/** Callback, der bei einem 401-Fehler aufgerufen wird (Auth-Context setzt ihn). */
@@ -63,11 +64,14 @@ export async function apiRequest<TResponse>(
method,
headers,
credentials: 'same-origin',
cache: 'no-store',
body: body !== undefined ? JSON.stringify(body) : undefined,
signal,
});
if (response.status === 401 && unauthorizedHandler) {
// A 401 from a feature endpoint may be request-specific. Let the auth
// context verify the session through /auth/me before clearing the user.
if (response.status === 401 && path !== '/api/v1/auth/me' && unauthorizedHandler) {
unauthorizedHandler();
}
@@ -92,4 +96,4 @@ export async function apiRequest<TResponse>(
}
return (await response.json()) as TResponse;
}
}

View File

@@ -121,11 +121,63 @@ export async function revokeModuleAccess(userId: string, moduleId: string): Prom
/** Liest das CSRF-Token aus dem nicht-HttpOnly-Cookie. */
function readCsrfToken(): string | null {
let csrfToken: string | null = null;
for (const part of document.cookie.split(';')) {
const [name, ...value] = part.trim().split('=');
if (name === 'mpm_csrf') {
return decodeURIComponent(value.join('='));
csrfToken = decodeURIComponent(value.join('='));
}
}
return null;
}
return csrfToken;
}
export interface MarketplaceProvider {
provider: 'github' | 'gitea' | 'forgejo';
label: string;
configured: boolean;
connected: boolean;
accountLogin: string | null;
baseUrl: string;
callbackUrl: string;
}
export interface MarketplaceRepository {
owner: string;
repository: string;
htmlUrl: string;
description: string;
defaultBranch: string;
installed: boolean;
}
export async function fetchMarketplaceRepositories(provider: MarketplaceProvider['provider']): Promise<MarketplaceRepository[]> {
return apiRequest<MarketplaceRepository[]>(`/api/v1/marketplace/repositories/${provider}`);
}
export async function installMarketplaceRepository(
provider: MarketplaceProvider['provider'],
owner: string,
repository: string,
): Promise<Module> {
const response = await apiRequest<{ module: unknown }>(
`/api/v1/marketplace/repositories/${provider}/${encodeURIComponent(owner)}/${encodeURIComponent(repository)}/install`,
{ method: 'POST' },
);
return moduleSchema.parse(response.module);
}
export async function fetchMarketplaceProviders(): Promise<MarketplaceProvider[]> {
return apiRequest<MarketplaceProvider[]>('/api/v1/marketplace/providers');
}
export async function beginMarketplaceConnection(provider: MarketplaceProvider['provider']): Promise<string> {
const response = await apiRequest<{ authorizationUrl: string }>(
`/api/v1/marketplace/connections/${provider}/start`,
{ method: 'POST' },
);
return response.authorizationUrl;
}
export async function disconnectMarketplaceProvider(provider: MarketplaceProvider['provider']): Promise<void> {
await apiRequest(`/api/v1/marketplace/connections/${provider}`, { method: 'DELETE' });
}