From 177ddd408d22d120cc57b691cdf7cb630d94d71c Mon Sep 17 00:00:00 2001 From: FURRO404 Date: Thu, 18 Jun 2026 00:38:00 -0700 Subject: [PATCH] feat(web): serve vehicle icons statically + deploy symlink + dev guard for logs/lang --- server.cjs | 33 +++++++++++++++++++++++++++++++++ vite.config.js | 6 ++++++ webhook.cjs | 24 ++++++++++++++++++++++++ 3 files changed, 63 insertions(+) diff --git a/server.cjs b/server.cjs index b841b4b..7c974b7 100644 --- a/server.cjs +++ b/server.cjs @@ -56,6 +56,10 @@ const TURNSTILE_VERIFY_TIMEOUT_MS = Number(process.env.TURNSTILE_VERIFY_TIMEOUT_ const TURNSTILE_MAX_TOKEN_LENGTH = 2048 const TURNSTILE_TOKEN_MAX_AGE_MS = 5 * 60 * 1000 const DIST_DIR = path.join(__dirname, 'dist') +const VEHICLE_ICONS_DIR = path.resolve( + __dirname, + process.env.VEHICLE_ICONS_DIR || path.join('dist', 'vehicle-icons'), +) const MAX_TEAM_NAME_LENGTH = 80 const MAX_CACHE_ENTRIES = 200 const MAX_RATE_LIMIT_KEYS = 1000 @@ -2027,6 +2031,30 @@ function serveStatic(req, res) { }) } +function serveVehicleIcon(req, res) { + let requestPath = '/' + try { + requestPath = decodeURIComponent(new URL(req.url, `http://localhost:${PORT}`).pathname) + } catch { + return send(res, 400, 'Bad request', { 'content-type': 'text/plain; charset=utf-8' }) + } + const name = requestPath.slice('/vehicle-icons/'.length) + const filePath = path.resolve(VEHICLE_ICONS_DIR, `./${name}`) + const relative = path.relative(VEHICLE_ICONS_DIR, filePath) + if (relative.startsWith('..') || path.isAbsolute(relative) || path.extname(filePath) !== '.png') { + return send(res, 403, 'Forbidden', { 'content-type': 'text/plain; charset=utf-8' }) + } + fs.readFile(filePath, (error, data) => { + if (error) { + return send(res, 404, 'Not found', { 'content-type': 'text/plain; charset=utf-8' }) + } + send(res, 200, data, { + 'content-type': 'image/png', + 'cache-control': 'public, max-age=604800', + }) + }) +} + const server = http.createServer((req, res) => { if (req.method === 'GET' && req.url === '/robots.txt') { sendRobotsTxt(req, res) @@ -2152,6 +2180,11 @@ const server = http.createServer((req, res) => { return } + if (req.method === 'GET' && req.url.startsWith('/vehicle-icons/')) { + serveVehicleIcon(req, res) + return + } + if (req.url.startsWith('/api/')) { proxyRequest(req, res) return diff --git a/vite.config.js b/vite.config.js index 7c8ca23..0c773f0 100644 --- a/vite.config.js +++ b/vite.config.js @@ -27,6 +27,12 @@ function isAllowedApiUrl(req) { } if (/^\/api\/tss\/games\/[A-Za-z0-9_-]{1,96}$/.test(url.pathname)) { + const keys = [...params.keys()] + const lang = params.get('lang') || 'en' + return keys.every((key) => key === 'lang') && /^[A-Za-z-]{2,8}$/.test(lang) + } + + if (/^\/api\/tss\/games\/[A-Za-z0-9_-]{1,96}\/logs$/.test(url.pathname)) { return [...params.keys()].length === 0 } diff --git a/webhook.cjs b/webhook.cjs index 3ea5e95..500f1df 100644 --- a/webhook.cjs +++ b/webhook.cjs @@ -348,6 +348,29 @@ function promoteBuiltDist() { } } +function syncVehicleIcons() { + // Point the served icon dir at the bot-managed SHARED/ICONS/VEHICLES so the + // scoreboard can render vehicle icons without bloating this repo. Symlink when + // possible; fall back to skipping (icons hide gracefully) if the source is absent. + const src = process.env.VEHICLE_ICONS_SRC || '/home/deploy/BOTS/SHARED/ICONS/VEHICLES' + const dst = path.resolve( + __dirname, + process.env.VEHICLE_ICONS_DIR || path.join('dist', 'vehicle-icons'), + ) + if (!fs.existsSync(src)) { + console.warn(`vehicle icons source missing, skipping: ${src}`) + return + } + try { + fs.rmSync(dst, { recursive: true, force: true }) + fs.mkdirSync(path.dirname(dst), { recursive: true }) + fs.symlinkSync(src, dst) + console.log(`linked vehicle icons ${dst} -> ${src}`) + } catch (error) { + console.error(`vehicle icon sync failed: ${error.message}`) + } +} + function validateBuiltDist() { const indexPath = path.join(NEXT_DIST_DIR, 'index.html') if (!fs.existsSync(indexPath)) { @@ -535,6 +558,7 @@ async function deploy(push) { validateBuiltDist() await run('cargo', ['build', '--manifest-path', 'backend/Cargo.toml', '--release']) promoteBuiltDist() + syncVehicleIcons() for (const target of RESTART_TARGETS) { await run('pm2', ['reload', target, '--update-env'])