Files
mpm/apps/platform-backend/src/modules/marketplace.controller.ts
2026-10-10 15:51:30 +02:00

184 lines
7.6 KiB
TypeScript

import { BadRequestException, Body, Controller, Delete, Get, Inject, 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 { extractSessionToken } from '../auth/guards/session.guard';
import { APP_CONFIG, type AppConfig } from '../config/config.tokens';
import type { AuthenticatedRequest } from '../auth/authenticated-request';
import { MarketplaceService } from './marketplace.service';
import { ModulesService } from './modules.service';
import type { ModuleRecord } from './manifest.types';
@ApiTags('Marketplace')
@Controller({ path: 'api/v1/marketplace' })
export class MarketplaceController {
constructor(
private readonly marketplaceService: MarketplaceService,
private readonly sessionService: SessionService,
private readonly modulesService: ModulesService,
@Inject(APP_CONFIG) private readonly config: AppConfig,
) {}
@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;
configuration: ModuleRecord['configuration']; configurationReady: boolean;
} }> {
const branch = await this.marketplaceService.defaultBranch(provider, owner, repository);
const { archive, commit } = await this.marketplaceService.downloadRepositoryArchive(provider, owner, repository, branch);
const manifest = await this.modulesService.validatePackage(archive);
if (await this.modulesService.findByModuleId(manifest.id)) {
throw new BadRequestException('Ein Modul mit dieser ID ist bereits installiert');
}
const module = await this.modulesService.install(archive, actor, request.ip ?? null);
await this.marketplaceService.recordInstallation(provider, owner, repository, module.id, branch, commit);
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(),
configuration: module.configuration,
configurationReady: module.configurationReady,
},
};
}
@Get('modules/:moduleId/updates')
@Roles('ADMIN')
updates(@Param('moduleId') moduleId: string) {
return this.marketplaceService.availableUpdates(moduleId);
}
@Get('operations/:operationId')
@Roles('ADMIN')
operationProgress(@Param('operationId') operationId: string, @CurrentUser() actor: AuthUser) {
return this.marketplaceService.getOperationProgress(operationId, actor.id);
}
@Post('modules/:moduleId/update')
@Roles('ADMIN')
async updateModule(
@Param('moduleId') moduleId: string,
@Body() body: { branch?: unknown; operationId?: unknown },
@CurrentUser() actor: AuthUser,
@Req() request: AuthenticatedRequest,
): Promise<{ module: ModuleRecord }> {
if (typeof body.branch !== 'string') throw new BadRequestException('Bitte eine Update-Branch auswählen');
const operationId = this.marketplaceService.beginOperation(body.operationId, actor.id);
const report = (phase: string, message: string, progress: number) =>
this.marketplaceService.reportOperation(operationId, actor.id, phase, message, progress);
try {
report('branch', 'Branch wird geprüft', 8);
const update = await this.marketplaceService.updateInstalledBranch(moduleId, body.branch, report);
report('validation', 'Update-Paket wird geprüft', 40);
const module = await this.modulesService.updateFromMarketplace(
moduleId,
update.archive,
actor,
request.ip ?? null,
report,
async () => {
report('commit', 'Neue Version wird registriert', 96);
await this.marketplaceService.commitInstalledBranch(
moduleId, update.provider, update.owner, update.repository, update.branch, update.commit,
update.previousBranch, update.previousCommit,
);
},
);
this.marketplaceService.finishOperation(operationId, actor.id, true);
return { module };
} catch (error) {
this.marketplaceService.finishOperation(operationId, actor.id, false);
throw error;
}
}
@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 = extractSessionToken(request, this.config.security.cookieSecure);
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());
}
}