feat(web): serve vehicle icons statically + deploy symlink + dev guard for logs/lang
This commit is contained in:
+33
@@ -56,6 +56,10 @@ const TURNSTILE_VERIFY_TIMEOUT_MS = Number(process.env.TURNSTILE_VERIFY_TIMEOUT_
|
|||||||
const TURNSTILE_MAX_TOKEN_LENGTH = 2048
|
const TURNSTILE_MAX_TOKEN_LENGTH = 2048
|
||||||
const TURNSTILE_TOKEN_MAX_AGE_MS = 5 * 60 * 1000
|
const TURNSTILE_TOKEN_MAX_AGE_MS = 5 * 60 * 1000
|
||||||
const DIST_DIR = path.join(__dirname, 'dist')
|
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_TEAM_NAME_LENGTH = 80
|
||||||
const MAX_CACHE_ENTRIES = 200
|
const MAX_CACHE_ENTRIES = 200
|
||||||
const MAX_RATE_LIMIT_KEYS = 1000
|
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) => {
|
const server = http.createServer((req, res) => {
|
||||||
if (req.method === 'GET' && req.url === '/robots.txt') {
|
if (req.method === 'GET' && req.url === '/robots.txt') {
|
||||||
sendRobotsTxt(req, res)
|
sendRobotsTxt(req, res)
|
||||||
@@ -2152,6 +2180,11 @@ const server = http.createServer((req, res) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (req.method === 'GET' && req.url.startsWith('/vehicle-icons/')) {
|
||||||
|
serveVehicleIcon(req, res)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (req.url.startsWith('/api/')) {
|
if (req.url.startsWith('/api/')) {
|
||||||
proxyRequest(req, res)
|
proxyRequest(req, res)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -27,6 +27,12 @@ function isAllowedApiUrl(req) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (/^\/api\/tss\/games\/[A-Za-z0-9_-]{1,96}$/.test(url.pathname)) {
|
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
|
return [...params.keys()].length === 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+24
@@ -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() {
|
function validateBuiltDist() {
|
||||||
const indexPath = path.join(NEXT_DIST_DIR, 'index.html')
|
const indexPath = path.join(NEXT_DIST_DIR, 'index.html')
|
||||||
if (!fs.existsSync(indexPath)) {
|
if (!fs.existsSync(indexPath)) {
|
||||||
@@ -535,6 +558,7 @@ async function deploy(push) {
|
|||||||
validateBuiltDist()
|
validateBuiltDist()
|
||||||
await run('cargo', ['build', '--manifest-path', 'backend/Cargo.toml', '--release'])
|
await run('cargo', ['build', '--manifest-path', 'backend/Cargo.toml', '--release'])
|
||||||
promoteBuiltDist()
|
promoteBuiltDist()
|
||||||
|
syncVehicleIcons()
|
||||||
|
|
||||||
for (const target of RESTART_TARGETS) {
|
for (const target of RESTART_TARGETS) {
|
||||||
await run('pm2', ['reload', target, '--update-env'])
|
await run('pm2', ['reload', target, '--update-env'])
|
||||||
|
|||||||
Reference in New Issue
Block a user