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>> { return this.marketplaceService.providers(); } @Get('repositories/:provider') @Roles('ADMIN') repositories(@Param('provider') provider: string): ReturnType { 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 { 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()); } }