184 lines
7.6 KiB
TypeScript
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());
|
|
}
|
|
}
|