ai generated solutions to our ai generated problems
This commit is contained in:
+34
-6
@@ -63,6 +63,9 @@ 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
|
||||||
const MAX_ANALYTICS_BODY_BYTES = 16 * 1024
|
const MAX_ANALYTICS_BODY_BYTES = 16 * 1024
|
||||||
|
const MAX_UPSTREAM_BODY_BYTES = Number(process.env.MAX_UPSTREAM_BODY_BYTES || 1024 * 1024)
|
||||||
|
const SERVER_REQUEST_TIMEOUT_MS = Number(process.env.SERVER_REQUEST_TIMEOUT_MS || 30000)
|
||||||
|
const SERVER_HEADERS_TIMEOUT_MS = Number(process.env.SERVER_HEADERS_TIMEOUT_MS || 10000)
|
||||||
const RUN_BACKGROUND_JOBS = !process.env.NODE_APP_INSTANCE || process.env.NODE_APP_INSTANCE === '0'
|
const RUN_BACKGROUND_JOBS = !process.env.NODE_APP_INSTANCE || process.env.NODE_APP_INSTANCE === '0'
|
||||||
|
|
||||||
const TRUST_PROXY = (() => {
|
const TRUST_PROXY = (() => {
|
||||||
@@ -300,8 +303,16 @@ function requestJson(url, timeoutMs = 10000) {
|
|||||||
},
|
},
|
||||||
(response) => {
|
(response) => {
|
||||||
const chunks = []
|
const chunks = []
|
||||||
|
let size = 0
|
||||||
|
|
||||||
response.on('data', (chunk) => chunks.push(chunk))
|
response.on('data', (chunk) => {
|
||||||
|
size += chunk.length
|
||||||
|
if (size > MAX_UPSTREAM_BODY_BYTES) {
|
||||||
|
req.destroy(new Error('Upstream response too large'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
chunks.push(chunk)
|
||||||
|
})
|
||||||
response.on('end', () => {
|
response.on('end', () => {
|
||||||
const body = Buffer.concat(chunks).toString('utf8')
|
const body = Buffer.concat(chunks).toString('utf8')
|
||||||
const latency = Date.now() - startedAt
|
const latency = Date.now() - startedAt
|
||||||
@@ -1726,6 +1737,7 @@ function proxyRequest(req, res) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const responseChunks = []
|
const responseChunks = []
|
||||||
|
let proxiedBytes = 0
|
||||||
const proxy = http.request(
|
const proxy = http.request(
|
||||||
target,
|
target,
|
||||||
{
|
{
|
||||||
@@ -1752,6 +1764,12 @@ function proxyRequest(req, res) {
|
|||||||
res.writeHead(proxyRes.statusCode || 502, headers)
|
res.writeHead(proxyRes.statusCode || 502, headers)
|
||||||
|
|
||||||
proxyRes.on('data', (chunk) => {
|
proxyRes.on('data', (chunk) => {
|
||||||
|
proxiedBytes += chunk.length
|
||||||
|
if (proxiedBytes > MAX_UPSTREAM_BODY_BYTES) {
|
||||||
|
proxy.destroy(new Error('Upstream response too large'))
|
||||||
|
res.destroy()
|
||||||
|
return
|
||||||
|
}
|
||||||
if (cacheKey && (proxyRes.statusCode || 0) >= 200 && (proxyRes.statusCode || 0) < 300) {
|
if (cacheKey && (proxyRes.statusCode || 0) >= 200 && (proxyRes.statusCode || 0) < 300) {
|
||||||
responseChunks.push(chunk)
|
responseChunks.push(chunk)
|
||||||
}
|
}
|
||||||
@@ -1772,6 +1790,7 @@ function proxyRequest(req, res) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
proxy.on('error', (error) => {
|
proxy.on('error', (error) => {
|
||||||
|
if (res.destroyed || res.headersSent) return
|
||||||
sendJson(res, 502, { error: 'API proxy failed', detail: error.message })
|
sendJson(res, 502, { error: 'API proxy failed', detail: error.message })
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1782,8 +1801,8 @@ function pagePublicOrigin(req) {
|
|||||||
const configured = PUBLIC_ORIGIN.split(',').map((origin) => origin.trim()).filter(Boolean)[0]
|
const configured = PUBLIC_ORIGIN.split(',').map((origin) => origin.trim()).filter(Boolean)[0]
|
||||||
if (configured) return configured.replace(/\/$/, '')
|
if (configured) return configured.replace(/\/$/, '')
|
||||||
|
|
||||||
const host = req.headers['x-forwarded-host'] || req.headers.host || `localhost:${PORT}`
|
const host = trustedForwardedHost(req) || `localhost:${PORT}`
|
||||||
const proto = req.headers['x-forwarded-proto'] || (req.socket.encrypted ? 'https' : 'http')
|
const proto = trustedForwardedProto(req) || (req.socket.encrypted ? 'https' : 'http')
|
||||||
return `${String(proto).split(',')[0].trim()}://${String(host).split(',')[0].trim()}`.replace(/\/$/, '')
|
return `${String(proto).split(',')[0].trim()}://${String(host).split(',')[0].trim()}`.replace(/\/$/, '')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1827,15 +1846,21 @@ function sendComingSoonPage(req, res) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function serveStatic(req, res) {
|
function serveStatic(req, res) {
|
||||||
const requestPath = decodeURIComponent(new URL(req.url, `http://localhost:${PORT}`).pathname)
|
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' })
|
||||||
|
}
|
||||||
if (COMING_SOON) {
|
if (COMING_SOON) {
|
||||||
return sendComingSoonPage(req, res)
|
return sendComingSoonPage(req, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
const relativePath = requestPath === '/' ? '/index.html' : requestPath
|
const relativePath = requestPath === '/' ? '/index.html' : requestPath
|
||||||
const filePath = path.normalize(path.join(DIST_DIR, relativePath))
|
const filePath = path.resolve(DIST_DIR, `.${relativePath}`)
|
||||||
|
const relativeToDist = path.relative(DIST_DIR, filePath)
|
||||||
|
|
||||||
if (!filePath.startsWith(DIST_DIR)) {
|
if (relativeToDist.startsWith('..') || path.isAbsolute(relativeToDist)) {
|
||||||
return send(res, 403, 'Forbidden', { 'content-type': 'text/plain; charset=utf-8' })
|
return send(res, 403, 'Forbidden', { 'content-type': 'text/plain; charset=utf-8' })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2006,6 +2031,9 @@ const server = http.createServer((req, res) => {
|
|||||||
serveStatic(req, res)
|
serveStatic(req, res)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
server.requestTimeout = SERVER_REQUEST_TIMEOUT_MS
|
||||||
|
server.headersTimeout = SERVER_HEADERS_TIMEOUT_MS
|
||||||
|
|
||||||
server.listen(PORT, '0.0.0.0', () => {
|
server.listen(PORT, '0.0.0.0', () => {
|
||||||
console.log(`tssbot-web serving http://localhost:${PORT}`)
|
console.log(`tssbot-web serving http://localhost:${PORT}`)
|
||||||
console.log(`proxying API requests to ${API_UPSTREAM}`)
|
console.log(`proxying API requests to ${API_UPSTREAM}`)
|
||||||
|
|||||||
+17
-4
@@ -668,6 +668,7 @@ function AppContent() {
|
|||||||
const [uptime, setUptime] = useState({ status: 'idle', checks: [], history: [], updatedAt: null })
|
const [uptime, setUptime] = useState({ status: 'idle', checks: [], history: [], updatedAt: null })
|
||||||
const [viewers, setViewers] = useState({ status: 'idle', data: null, error: null, updatedAt: null })
|
const [viewers, setViewers] = useState({ status: 'idle', data: null, error: null, updatedAt: null })
|
||||||
const [songOfDay, setSongOfDay] = useState({ status: 'idle', data: null, error: null })
|
const [songOfDay, setSongOfDay] = useState({ status: 'idle', data: null, error: null })
|
||||||
|
const [songOfDayRequest, setSongOfDayRequest] = useState(0)
|
||||||
const [analyticsPreferences, setAnalyticsPreferences] = useState(() => storedAnalyticsPreferences())
|
const [analyticsPreferences, setAnalyticsPreferences] = useState(() => storedAnalyticsPreferences())
|
||||||
const [theme, setTheme] = useState(() => storedThemePreference())
|
const [theme, setTheme] = useState(() => storedThemePreference())
|
||||||
const [showFloatingNav, setShowFloatingNav] = useState(() => window.scrollY > 40)
|
const [showFloatingNav, setShowFloatingNav] = useState(() => window.scrollY > 40)
|
||||||
@@ -1010,7 +1011,6 @@ function AppContent() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (route.page !== 'home') return
|
if (route.page !== 'home') return
|
||||||
if (songOfDay.status === 'ready' || songOfDay.status === 'loading') return
|
|
||||||
|
|
||||||
const controller = new AbortController()
|
const controller = new AbortController()
|
||||||
let timedOut = false
|
let timedOut = false
|
||||||
@@ -1038,7 +1038,7 @@ function AppContent() {
|
|||||||
window.clearTimeout(timeout)
|
window.clearTimeout(timeout)
|
||||||
controller.abort()
|
controller.abort()
|
||||||
}
|
}
|
||||||
}, [route.page, songOfDay.status])
|
}, [route.page, songOfDayRequest])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (route.page !== 'team' || !route.teamName) return
|
if (route.page !== 'team' || !route.teamName) return
|
||||||
@@ -1422,6 +1422,7 @@ function AppContent() {
|
|||||||
onTeamSearch={handleTeamSearch}
|
onTeamSearch={handleTeamSearch}
|
||||||
searchPlaceholder={searchPlaceholder}
|
searchPlaceholder={searchPlaceholder}
|
||||||
setTeamQuery={setTeamQuery}
|
setTeamQuery={setTeamQuery}
|
||||||
|
setSongOfDayRequest={setSongOfDayRequest}
|
||||||
songOfDay={songOfDay}
|
songOfDay={songOfDay}
|
||||||
teamSuggestions={teamSuggestions}
|
teamSuggestions={teamSuggestions}
|
||||||
teams={teams}
|
teams={teams}
|
||||||
@@ -1795,6 +1796,7 @@ function Landing({
|
|||||||
onTeamSearch,
|
onTeamSearch,
|
||||||
searchPlaceholder,
|
searchPlaceholder,
|
||||||
setTeamQuery,
|
setTeamQuery,
|
||||||
|
setSongOfDayRequest,
|
||||||
songOfDay,
|
songOfDay,
|
||||||
teamSuggestions,
|
teamSuggestions,
|
||||||
teams,
|
teams,
|
||||||
@@ -1865,7 +1867,10 @@ function Landing({
|
|||||||
Search teams
|
Search teams
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<SongOfDayCard songOfDay={songOfDay} />
|
<SongOfDayCard
|
||||||
|
onRetry={() => setSongOfDayRequest((value) => value + 1)}
|
||||||
|
songOfDay={songOfDay}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1892,7 +1897,7 @@ function Landing({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function SongOfDayCard({ songOfDay }) {
|
function SongOfDayCard({ onRetry, songOfDay }) {
|
||||||
const track = songOfDay.data?.track
|
const track = songOfDay.data?.track
|
||||||
const isLoading = songOfDay.status === 'loading'
|
const isLoading = songOfDay.status === 'loading'
|
||||||
const message =
|
const message =
|
||||||
@@ -1941,6 +1946,14 @@ function SongOfDayCard({ songOfDay }) {
|
|||||||
>
|
>
|
||||||
Play
|
Play
|
||||||
</a>
|
</a>
|
||||||
|
) : songOfDay.status === 'error' ? (
|
||||||
|
<button
|
||||||
|
className="shrink-0 rounded-md border border-ring px-3 py-2 text-sm font-semibold text-fury-cyan transition hover:bg-surface hover:text-text"
|
||||||
|
onClick={onRetry}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Retry
|
||||||
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|||||||
+25
-6
@@ -43,10 +43,14 @@ const DISCORD_INCLUDE_PATCH = /^(1|true|yes)$/i.test(String(process.env.DISCORD_
|
|||||||
const RESTART_TARGETS = (process.env.PM2_RESTART_TARGETS || 'tssbot-web')
|
const RESTART_TARGETS = (process.env.PM2_RESTART_TARGETS || 'tssbot-web')
|
||||||
.split(',')
|
.split(',')
|
||||||
.map((target) => target.trim())
|
.map((target) => target.trim())
|
||||||
|
.filter((target) => /^[A-Za-z0-9_.:-]{1,80}$/.test(target))
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
const DIST_DIR = path.join(__dirname, 'dist')
|
const DIST_DIR = path.join(__dirname, 'dist')
|
||||||
const NEXT_DIST_DIR = path.join(__dirname, 'dist-next')
|
const NEXT_DIST_DIR = path.join(__dirname, 'dist-next')
|
||||||
const PREVIOUS_DIST_DIR = path.join(__dirname, 'dist-previous')
|
const PREVIOUS_DIST_DIR = path.join(__dirname, 'dist-previous')
|
||||||
|
const MAX_WEBHOOK_BODY_BYTES = Number(process.env.WEBHOOK_MAX_BODY_BYTES || 1024 * 1024)
|
||||||
|
const WEBHOOK_REQUEST_TIMEOUT_MS = Number(process.env.WEBHOOK_REQUEST_TIMEOUT_MS || 30000)
|
||||||
|
const WEBHOOK_HEADERS_TIMEOUT_MS = Number(process.env.WEBHOOK_HEADERS_TIMEOUT_MS || 10000)
|
||||||
const ALLOWED_REFS = new Set(
|
const ALLOWED_REFS = new Set(
|
||||||
(process.env.GITHUB_WEBHOOK_REFS || 'refs/heads/main')
|
(process.env.GITHUB_WEBHOOK_REFS || 'refs/heads/main')
|
||||||
.split(',')
|
.split(',')
|
||||||
@@ -420,8 +424,7 @@ async function deploy(push) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
http
|
const webhookServer = http.createServer((req, res) => {
|
||||||
.createServer((req, res) => {
|
|
||||||
if (req.method === 'GET' && req.url === '/health') {
|
if (req.method === 'GET' && req.url === '/health') {
|
||||||
json(res, 200, { ok: true, deploying, restart_targets: RESTART_TARGETS })
|
json(res, 200, { ok: true, deploying, restart_targets: RESTART_TARGETS })
|
||||||
return
|
return
|
||||||
@@ -433,8 +436,20 @@ http
|
|||||||
}
|
}
|
||||||
|
|
||||||
const chunks = []
|
const chunks = []
|
||||||
req.on('data', (chunk) => chunks.push(chunk))
|
let size = 0
|
||||||
|
let tooLarge = false
|
||||||
|
req.on('data', (chunk) => {
|
||||||
|
size += chunk.length
|
||||||
|
if (size > MAX_WEBHOOK_BODY_BYTES) {
|
||||||
|
tooLarge = true
|
||||||
|
json(res, 413, { error: 'Webhook body too large' })
|
||||||
|
req.destroy()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
chunks.push(chunk)
|
||||||
|
})
|
||||||
req.on('end', () => {
|
req.on('end', () => {
|
||||||
|
if (tooLarge) return
|
||||||
const rawBody = Buffer.concat(chunks)
|
const rawBody = Buffer.concat(chunks)
|
||||||
const push = safeJsonParse(rawBody)
|
const push = safeJsonParse(rawBody)
|
||||||
|
|
||||||
@@ -485,12 +500,16 @@ http
|
|||||||
deploying = false
|
deploying = false
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.listen(PORT, '0.0.0.0', () => {
|
|
||||||
|
webhookServer.requestTimeout = WEBHOOK_REQUEST_TIMEOUT_MS
|
||||||
|
webhookServer.headersTimeout = WEBHOOK_HEADERS_TIMEOUT_MS
|
||||||
|
|
||||||
|
webhookServer.listen(PORT, '0.0.0.0', () => {
|
||||||
console.log(`tssbot webhook listening on http://localhost:${PORT}/github`)
|
console.log(`tssbot webhook listening on http://localhost:${PORT}/github`)
|
||||||
console.log(`restart targets: ${RESTART_TARGETS.join(', ')}`)
|
console.log(`restart targets: ${RESTART_TARGETS.join(', ')}`)
|
||||||
notifyDiscordRestart()
|
notifyDiscordRestart()
|
||||||
})
|
})
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
console.log('24 hour webhook refresh reached; exiting for PM2 restart')
|
console.log('24 hour webhook refresh reached; exiting for PM2 restart')
|
||||||
|
|||||||
Reference in New Issue
Block a user