Spaces:
Runtime error
Runtime error
| import { | |
| Controller, | |
| Get, | |
| Post, | |
| Put, | |
| Delete, | |
| Param, | |
| Body, | |
| Header, | |
| HttpCode, | |
| HttpStatus, | |
| UseInterceptors, | |
| UploadedFile, | |
| } from '@nestjs/common'; | |
| import { FileInterceptor } from '@nestjs/platform-express'; | |
| import { ApiTags, ApiOperation, ApiResponse, ApiConsumes } from '@nestjs/swagger'; | |
| import { PluginsService } from './plugins.service'; | |
| import { PluginDto, PluginConfigDto, PluginSessionsDto, InstallFromUrlDto } from './dto/plugin.dto'; | |
| import type { CatalogPlugin } from './catalog'; | |
| import { RequireRole, CurrentApiKey } from '../auth/decorators/auth.decorators'; | |
| import { ApiKey, ApiKeyRole } from '../auth/entities/api-key.entity'; | |
| /** Max accepted upload size for a plugin package (compressed). */ | |
| const MAX_PLUGIN_UPLOAD_BYTES = 5 * 1024 * 1024; | |
| ('plugins') | |
| ('plugins') | |
| export class PluginsController { | |
| constructor(private readonly pluginsService: PluginsService) {} | |
| () | |
| (ApiKeyRole.ADMIN) | |
| ({ summary: 'List all plugins' }) | |
| ({ status: 200, description: 'List of all plugins' }) | |
| findAll(): PluginDto[] { | |
| return this.pluginsService.findAll(); | |
| } | |
| ('install') | |
| (ApiKeyRole.ADMIN) | |
| (FileInterceptor('file', { limits: { fileSize: MAX_PLUGIN_UPLOAD_BYTES } })) | |
| ('multipart/form-data') | |
| ({ summary: 'Install a plugin from an uploaded .zip package' }) | |
| ({ status: 201, description: 'Plugin installed' }) | |
| ({ status: 400, description: 'Invalid package' }) | |
| ({ status: 409, description: 'Plugin already installed' }) | |
| install(() file: { buffer?: Buffer }): PluginDto { | |
| return this.pluginsService.install(file); | |
| } | |
| ('install-url') | |
| (ApiKeyRole.ADMIN) | |
| ({ summary: 'Install a plugin by downloading its .zip from a URL (SSRF-guarded)' }) | |
| ({ status: 201, description: 'Plugin installed' }) | |
| ({ status: 400, description: 'Invalid URL, download failed, or invalid package' }) | |
| ({ status: 409, description: 'Plugin already installed' }) | |
| async installFromUrl(() dto: InstallFromUrlDto): Promise<PluginDto> { | |
| return await this.pluginsService.installFromUrl(dto.url); | |
| } | |
| // Declared before `:id` so `GET /plugins/catalog` is not captured by the `:id` route. | |
| ('catalog') | |
| (ApiKeyRole.ADMIN) | |
| ({ summary: 'List the remote plugin catalog, annotated with install state' }) | |
| ({ status: 200, description: 'Catalog entries' }) | |
| ({ status: 400, description: 'Catalog could not be fetched or parsed' }) | |
| async catalog(): Promise<CatalogPlugin[]> { | |
| return await this.pluginsService.getCatalog(); | |
| } | |
| (':id') | |
| (ApiKeyRole.ADMIN) | |
| ({ summary: 'Get plugin by ID' }) | |
| ({ status: 200, description: 'Plugin details' }) | |
| ({ status: 404, description: 'Plugin not found' }) | |
| findOne(('id') id: string): PluginDto { | |
| return this.pluginsService.findOne(id); | |
| } | |
| (':id/enable') | |
| (ApiKeyRole.ADMIN) | |
| (HttpStatus.OK) | |
| ({ summary: 'Enable a plugin' }) | |
| ({ status: 200, description: 'Plugin enabled successfully' }) | |
| async enable(('id') id: string): Promise<{ success: boolean; message: string }> { | |
| return await this.pluginsService.enable(id); | |
| } | |
| (':id/disable') | |
| (ApiKeyRole.ADMIN) | |
| (HttpStatus.OK) | |
| ({ summary: 'Disable a plugin' }) | |
| ({ status: 200, description: 'Plugin disabled successfully' }) | |
| async disable(('id') id: string): Promise<{ success: boolean; message: string }> { | |
| return await this.pluginsService.disable(id); | |
| } | |
| (':id/config') | |
| (ApiKeyRole.ADMIN) | |
| ({ summary: 'Update plugin configuration' }) | |
| ({ status: 200, description: 'Plugin configuration updated' }) | |
| updateConfig(('id') id: string, () configDto: PluginConfigDto): { success: boolean; message: string } { | |
| return this.pluginsService.updateConfig(id, configDto.config); | |
| } | |
| // The dashboard fetches this WITH the API key and injects the body as an opaque-origin sandboxed | |
| // iframe srcdoc. It attaches that document's response-specific nonce to inline scripts so the | |
| // inherited CSP allows only the isolated editor bootstrap, without enabling parent unsafe-inline. | |
| (':id/config-ui') | |
| (ApiKeyRole.ADMIN) | |
| ('Content-Type', 'text/html; charset=utf-8') | |
| ('Content-Security-Policy', 'sandbox') | |
| ('X-Content-Type-Options', 'nosniff') | |
| ({ summary: "Serve a plugin's sandboxed config-UI entry HTML (for an iframe srcdoc)" }) | |
| ({ status: 200, description: 'Config UI HTML' }) | |
| ({ status: 404, description: 'Plugin not found or has no config UI' }) | |
| getConfigUi(('id') id: string): string { | |
| return this.pluginsService.getConfigUiHtml(id); | |
| } | |
| (':id/config/:sessionId') | |
| (ApiKeyRole.ADMIN) | |
| ({ summary: 'Set a plugin config override for a specific session (empty = clear it)' }) | |
| ({ status: 200, description: 'Per-session plugin configuration updated' }) | |
| ({ status: 400, description: 'Plugin is global (not session-scoped)' }) | |
| ({ status: 404, description: 'Plugin not found' }) | |
| updateSessionConfig( | |
| ('id') id: string, | |
| ('sessionId') sessionId: string, | |
| () configDto: PluginConfigDto, | |
| ): { success: boolean; message: string } { | |
| return this.pluginsService.updateSessionConfig(id, sessionId, configDto.config); | |
| } | |
| (':id/sessions') | |
| (ApiKeyRole.ADMIN) | |
| ({ summary: "Set which sessions a session-scoped plugin is activated for (['*'] = all)" }) | |
| ({ status: 200, description: 'Plugin session activation updated', type: PluginDto }) | |
| ({ status: 400, description: 'Plugin is global (not session-scoped)' }) | |
| ({ status: 404, description: 'Plugin not found' }) | |
| updateSessions(('id') id: string, () dto: PluginSessionsDto, () apiKey?: ApiKey): PluginDto { | |
| // The target sessions live in the body, which the ApiKeyGuard (keyed off route params) never | |
| // inspects — so a session-restricted key's allowedSessions scope must be enforced here. | |
| return this.pluginsService.updateSessions(id, dto.sessions, apiKey?.allowedSessions); | |
| } | |
| (':id/update') | |
| (ApiKeyRole.ADMIN) | |
| ({ summary: 'Update an installed plugin in place from a URL (preserves config + enabled state)' }) | |
| ({ status: 201, description: 'Plugin updated' }) | |
| ({ status: 400, description: 'Invalid URL/package, id mismatch, or built-in' }) | |
| ({ status: 404, description: 'Plugin not found' }) | |
| async update(('id') id: string, () dto: InstallFromUrlDto): Promise<PluginDto> { | |
| return await this.pluginsService.updateFromUrl(id, dto.url); | |
| } | |
| (':id') | |
| (ApiKeyRole.ADMIN) | |
| ({ summary: 'Uninstall a plugin (removes its files; built-ins are protected)' }) | |
| ({ status: 200, description: 'Plugin uninstalled' }) | |
| ({ status: 400, description: 'Cannot uninstall (e.g. built-in)' }) | |
| ({ status: 404, description: 'Plugin not found' }) | |
| async uninstall(('id') id: string): Promise<{ success: boolean; message: string }> { | |
| return await this.pluginsService.uninstall(id); | |
| } | |
| (':id/health') | |
| (ApiKeyRole.ADMIN) | |
| ({ summary: 'Check plugin health' }) | |
| ({ status: 200, description: 'Plugin health status' }) | |
| async healthCheck(('id') id: string): Promise<{ healthy: boolean; message?: string }> { | |
| return await this.pluginsService.healthCheck(id); | |
| } | |
| } | |