feat: Wire uMod browse proxy and custom plugin upload
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>
This commit is contained in:
Vantz Stockwell
2026-02-21 16:09:19 -05:00
parent 38e6d28248
commit 2b45413c20
5 changed files with 422 additions and 23 deletions

View File

@@ -3,10 +3,39 @@ import { ref } from 'vue'
import { useApi } from '@/composables/useApi'
import type { PluginEntry } from '@/types'
export interface UmodPlugin {
name: string
title: string
slug: string
author: string
description: string
downloads: number
downloads_shortened: string
download_url: string
latest_release_version: string
latest_release_version_formatted: string
icon_url: string
url: string
tags_all: string
watchers_shortened: string
created_at: string
updated_at: string
}
export interface UmodBrowseResult {
current_page: number
data: UmodPlugin[]
last_page: number
per_page: number
total: number
}
export const usePluginStore = defineStore('plugins', () => {
const plugins = ref<PluginEntry[]>([])
const searchResults = ref<any[]>([])
const browseResults = ref<UmodBrowseResult | null>(null)
const isLoading = ref(false)
const isBrowseLoading = ref(false)
const api = useApi()
async function fetchPlugins() {
@@ -18,7 +47,7 @@ export const usePluginStore = defineStore('plugins', () => {
}
}
async function installPlugin(data: { plugin_name: string; source: string }) {
async function installPlugin(data: { plugin_name: string; umod_slug?: string; source: string }) {
await api.post('/plugins/install', data)
await fetchPlugins()
}
@@ -37,6 +66,7 @@ export const usePluginStore = defineStore('plugins', () => {
await fetchPlugins()
}
// Legacy — kept for backwards compatibility, routes to browse
async function searchPlugins(query: string) {
isLoading.value = true
try {
@@ -46,15 +76,38 @@ export const usePluginStore = defineStore('plugins', () => {
}
}
async function browseUmod(query: string, page = 1, sort = 'downloads') {
isBrowseLoading.value = true
try {
const params = new URLSearchParams({ page: String(page), sort })
if (query.trim()) params.set('query', query.trim())
browseResults.value = await api.get<UmodBrowseResult>(`/plugins/browse?${params.toString()}`)
} finally {
isBrowseLoading.value = false
}
}
async function uploadPlugin(file: File): Promise<PluginEntry> {
const formData = new FormData()
formData.append('file', file)
const result = await api.upload<PluginEntry>('/plugins/upload', formData)
await fetchPlugins()
return result
}
return {
plugins,
searchResults,
browseResults,
isLoading,
isBrowseLoading,
fetchPlugins,
installPlugin,
uninstallPlugin,
reloadPlugin,
updatePluginConfig,
searchPlugins,
browseUmod,
uploadPlugin,
}
})