Files
TREK/client/src/api/websocket.js
T
Maurice d845057f84 Security hardening, backup restore fix & restore warning modal
- Fix backup restore: try/finally ensures DB always reopens after closeDb
- Fix EBUSY on uploads during restore (in-place overwrite instead of rmSync)
- Add DB proxy null guard for clearer errors during restore window
- Add red warning modal before backup restore (DE/EN, dark mode support)
- JWT secret: empty docker-compose default so auto-generation kicks in
- OIDC: pass token via URL fragment instead of query param (no server logs)
- Block SVG uploads on photos, files and covers (stored XSS prevention)
- Add helmet for security headers (HSTS, X-Frame, nosniff, etc.)
- Explicit express.json body size limit (100kb)
- Fix XSS in Leaflet map markers (escape image_url in HTML)
- Remove verbose WebSocket debug logging from client
2026-03-21 15:09:41 +01:00

142 lines
3.4 KiB
JavaScript

// Singleton WebSocket manager for real-time collaboration
let socket = null
let reconnectTimer = null
let reconnectDelay = 1000
const MAX_RECONNECT_DELAY = 30000
const listeners = new Set()
const activeTrips = new Set()
let currentToken = null
let refetchCallback = null
let mySocketId = null
export function getSocketId() {
return mySocketId
}
export function setRefetchCallback(fn) {
refetchCallback = fn
}
function getWsUrl(token) {
const protocol = location.protocol === 'https:' ? 'wss' : 'ws'
return `${protocol}://${location.host}/ws?token=${token}`
}
function handleMessage(event) {
try {
const parsed = JSON.parse(event.data)
// Store our socket ID from welcome message
if (parsed.type === 'welcome') {
mySocketId = parsed.socketId
return
}
listeners.forEach(fn => {
try { fn(parsed) } catch (err) { console.error('WebSocket listener error:', err) }
})
} catch (err) {
console.error('WebSocket message parse error:', err)
}
}
function scheduleReconnect() {
if (reconnectTimer) return
reconnectTimer = setTimeout(() => {
reconnectTimer = null
if (currentToken) {
connectInternal(currentToken, true)
}
}, reconnectDelay)
reconnectDelay = Math.min(reconnectDelay * 2, MAX_RECONNECT_DELAY)
}
function connectInternal(token, isReconnect = false) {
if (socket && (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)) {
return
}
const url = getWsUrl(token)
socket = new WebSocket(url)
socket.onopen = () => {
// connection established
reconnectDelay = 1000
// Join active trips on any connect (initial or reconnect)
if (activeTrips.size > 0) {
activeTrips.forEach(tripId => {
if (socket && socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify({ type: 'join', tripId }))
// joined trip room
}
})
// Refetch trip data for active trips
if (refetchCallback) {
activeTrips.forEach(tripId => {
try { refetchCallback(tripId) } catch (err) {
console.error('Failed to refetch trip data on reconnect:', err)
}
})
}
}
}
socket.onmessage = handleMessage
socket.onclose = () => {
socket = null
if (currentToken) {
scheduleReconnect()
}
}
socket.onerror = () => {
// onclose will fire after onerror, reconnect handled there
}
}
export function connect(token) {
currentToken = token
reconnectDelay = 1000
if (reconnectTimer) {
clearTimeout(reconnectTimer)
reconnectTimer = null
}
connectInternal(token, false)
}
export function disconnect() {
currentToken = null
if (reconnectTimer) {
clearTimeout(reconnectTimer)
reconnectTimer = null
}
activeTrips.clear()
if (socket) {
socket.onclose = null // prevent reconnect
socket.close()
socket = null
}
}
export function joinTrip(tripId) {
activeTrips.add(String(tripId))
if (socket && socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify({ type: 'join', tripId: String(tripId) }))
}
}
export function leaveTrip(tripId) {
activeTrips.delete(String(tripId))
if (socket && socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify({ type: 'leave', tripId: String(tripId) }))
}
}
export function addListener(fn) {
listeners.add(fn)
}
export function removeListener(fn) {
listeners.delete(fn)
}