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

@@ -0,0 +1,123 @@
import { BadRequestException, Controller, Delete, Get, Param, Post, Query, Req, Res } from '@nestjs/common';
import type { Request, Response } from 'express';
import { ApiTags } from '@nestjs/swagger';
import { CurrentUser } from '../common/decorators/current-user.decorator';
import { Public } from '../common/decorators/public.decorator';
import { Roles } from '../common/decorators/roles.decorator';
import type { AuthUser } from '../users/user.types';
import { SessionService } from '../auth/session.service';
import type { AuthenticatedRequest } from '../auth/authenticated-request';
import { MarketplaceService } from './marketplace.service';
import { ModulesService } from './modules.service';
@ApiTags('Marketplace')
@Controller({ path: 'api/v1/marketplace' })
export class MarketplaceController {
constructor(
private readonly marketplaceService: MarketplaceService,
private readonly sessionService: SessionService,
private readonly modulesService: ModulesService,
) {}
@Get('providers')
@Roles('ADMIN')
providers(): Promise<Awaited<ReturnType<MarketplaceService['providers']>>> {
return this.marketplaceService.providers();
}
@Get('repositories/:provider')
@Roles('ADMIN')
repositories(@Param('provider') provider: string): ReturnType<MarketplaceService['repositories']> {
return this.marketplaceService.repositories(provider);
}
@Post('repositories/:provider/:owner/:repository/install')
@Roles('ADMIN')
async installRepository(
@Param('provider') provider: string,
@Param('owner') owner: string,
@Param('repository') repository: string,
@CurrentUser() actor: AuthUser,
@Req() request: AuthenticatedRequest,
): Promise<{ module: {
id: string; moduleId: string; name: string; slug: string; version: string; description: string;
author: string; status: string; internalPort: number; healthcheckUrl: string; enabled: boolean; createdAt: string;
} }> {
const archive = await this.marketplaceService.downloadRepositoryArchive(provider, owner, repository);
const manifest = await this.modulesService.validatePackage(archive);
const module = await this.modulesService.findByModuleId(manifest.id) ??
await this.modulesService.install(archive, actor, request.ip ?? null);
await this.marketplaceService.recordInstallation(provider, owner, repository, module.id);
return {
module: {
id: module.id,
moduleId: module.moduleId,
name: module.name,
slug: module.slug,
version: module.version,
description: module.description,
author: module.author,
status: module.status,
internalPort: module.internalPort,
healthcheckUrl: module.healthcheckUrl,
enabled: module.enabled,
createdAt: module.createdAt.toISOString(),
},
};
}
@Post('connections/:provider/start')
@Roles('ADMIN')
async startConnection(
@Param('provider') provider: string,
@CurrentUser() user: AuthUser,
@Req() request: AuthenticatedRequest,
): Promise<{ authorizationUrl: string }> {
if (!request.session?.id) throw new BadRequestException('Session konnte nicht geprüft werden');
return { authorizationUrl: await this.marketplaceService.beginConnection(provider, user.id, request.session.id) };
}
@Delete('connections/:provider')
@Roles('ADMIN')
async disconnect(@Param('provider') provider: string): Promise<{ success: true }> {
await this.marketplaceService.disconnect(provider);
return { success: true };
}
@Get('oauth/:provider/callback')
@Public()
async callback(
@Param('provider') provider: string,
@Query('code') code: string | undefined,
@Query('state') state: string | undefined,
@Query('error') error: string | undefined,
@Req() request: Request,
@Res() response: Response,
): Promise<void> {
const destination = new URL('/admin/modules', this.marketplaceService.frontendUrl());
if (error) {
destination.searchParams.set('marketplace', 'denied');
} else if (!code || !state) {
destination.searchParams.set('marketplace', 'error');
destination.searchParams.set('reason', 'callback');
} else {
try {
const token = request.cookies?.mpm_session as string | undefined;
const session = token ? await this.sessionService.findValid(token) : null;
if (!session) {
destination.searchParams.set('marketplace', 'error');
destination.searchParams.set('reason', 'session');
response.redirect(302, destination.toString());
return;
}
await this.marketplaceService.completeConnection(provider, code, state, session?.id ?? null);
destination.searchParams.set('marketplace', 'connected');
destination.searchParams.set('provider', provider);
} catch {
destination.searchParams.set('marketplace', 'error');
destination.searchParams.set('reason', 'oauth');
}
}
response.redirect(302, destination.toString());
}
}