Files
corrosion-admin-panel/backend-nest/src/modules/plugins/plugins.controller.ts
Vantz Stockwell 2b45413c20
All checks were successful
Test Asgard Runner / test (push) Successful in 2s
feat: Wire uMod browse proxy and custom plugin upload
Backend:
- GET /plugins/browse proxies uMod search.json filtered to Rust category,
  with 5-minute in-memory Map cache to avoid hammering the upstream API
- POST /plugins/upload accepts .cs files up to 5 MB via multipart, persists
  to plugin_registry, and dispatches plugin_upload action over NATS so the
  companion agent can write the file to the game server
- Legacy GET /plugins/search stub preserved (now directs callers to /browse)
- FileInterceptor + @UploadedFile follow the existing maps upload pattern

Frontend:
- useApi composable gains upload() method for multipart/form-data requests
  (omits Content-Type so the browser sets the correct multipart boundary)
- plugins store adds browseUmod() calling GET /plugins/browse and
  uploadPlugin() calling POST /plugins/upload with FormData;
  UmodPlugin and UmodBrowseResult TypeScript interfaces exported
- PluginsView Browse tab now calls browseUmod() through the backend proxy
  (no cross-origin requests to uMod directly); results show title,
  downloads_shortened, and latest_release_version_formatted from the
  real uMod payload
- New Upload Custom tab: drag-and-drop or click file input for .cs files,
  client-side extension/size validation, spinner during upload, success
  toast + auto-switch to Installed tab on completion

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 16:09:19 -05:00

109 lines
3.6 KiB
TypeScript

import {
Controller,
Get,
Post,
Put,
Delete,
Body,
Param,
Query,
UseGuards,
UseInterceptors,
UploadedFile,
BadRequestException,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { ApiTags, ApiBearerAuth, ApiOperation, ApiQuery, ApiConsumes } from '@nestjs/swagger';
import { PluginsService } from './plugins.service';
import { InstallPluginDto } from './dto/install-plugin.dto';
import { UpdatePluginConfigDto } from './dto/update-plugin-config.dto';
import { CurrentTenant } from '../../common/decorators/current-tenant.decorator';
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
import { PermissionsGuard } from '../../common/guards/permissions.guard';
@ApiTags('plugins')
@ApiBearerAuth()
@Controller('plugins')
@UseGuards(JwtAuthGuard, PermissionsGuard)
export class PluginsController {
constructor(private readonly pluginsService: PluginsService) {}
@Get()
@RequirePermission('plugin.view')
@ApiOperation({ summary: 'Get all installed plugins for tenant' })
getPlugins(@CurrentTenant() licenseId: string) {
return this.pluginsService.getPlugins(licenseId);
}
@Post('install')
@RequirePermission('plugin.manage')
@ApiOperation({ summary: 'Install new plugin' })
installPlugin(@CurrentTenant() licenseId: string, @Body() dto: InstallPluginDto) {
return this.pluginsService.installPlugin(licenseId, dto);
}
@Delete(':id')
@RequirePermission('plugin.manage')
@ApiOperation({ summary: 'Uninstall plugin' })
async uninstallPlugin(@CurrentTenant() licenseId: string, @Param('id') pluginId: string) {
await this.pluginsService.uninstallPlugin(licenseId, pluginId);
return { deleted: true };
}
@Post(':id/reload')
@RequirePermission('plugin.manage')
@ApiOperation({ summary: 'Reload plugin on game server' })
reloadPlugin(@CurrentTenant() licenseId: string, @Param('id') pluginId: string) {
return this.pluginsService.reloadPlugin(licenseId, pluginId);
}
@Put(':id/config')
@RequirePermission('plugin.manage')
@ApiOperation({ summary: 'Update plugin configuration' })
updateConfig(
@CurrentTenant() licenseId: string,
@Param('id') pluginId: string,
@Body() dto: UpdatePluginConfigDto,
) {
return this.pluginsService.updateConfig(licenseId, pluginId, dto);
}
@Get('search')
@RequirePermission('plugin.view')
@ApiOperation({ summary: 'Search uMod plugin directory (legacy stub)' })
@ApiQuery({ name: 'q', required: true, example: 'kits' })
searchUmod(@Query('q') query: string) {
return this.pluginsService.searchUmod(query);
}
@Get('browse')
@RequirePermission('plugin.view')
@ApiOperation({ summary: 'Browse uMod plugin directory (proxied)' })
@ApiQuery({ name: 'query', required: false, example: 'vanish' })
@ApiQuery({ name: 'page', required: false, example: 1 })
@ApiQuery({ name: 'sort', required: false, example: 'downloads' })
browseUmod(
@Query('query') query: string,
@Query('page') page: string,
@Query('sort') sort: string,
) {
return this.pluginsService.browseUmod(query, page ? parseInt(page, 10) : 1, sort);
}
@Post('upload')
@RequirePermission('plugin.manage')
@ApiOperation({ summary: 'Upload a custom .cs plugin file' })
@ApiConsumes('multipart/form-data')
@UseInterceptors(FileInterceptor('file'))
uploadPlugin(
@CurrentTenant() licenseId: string,
@UploadedFile() file: Express.Multer.File,
) {
if (!file) {
throw new BadRequestException('No file provided');
}
return this.pluginsService.uploadPlugin(licenseId, file);
}
}