All checks were successful
Test Asgard Runner / test (push) Successful in 2s
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>
109 lines
3.6 KiB
TypeScript
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);
|
|
}
|
|
}
|