Files
Chatapp/Backend/lib/simpleWebSocket.js
T
Francesco Lorenzo D'Amico a5d116358e Initial commit
2026-08-10 15:25:21 +02:00

144 lines
4.7 KiB
JavaScript

const { randomUUID } = require('crypto')
const { Server } = require('socket.io')
function createRealtimeServer(server, { clientOrigin, database, onError }) {
const io = new Server(server, {
cors: {
origin: clientOrigin,
methods: ['GET', 'POST', 'PATCH', 'DELETE', 'OPTIONS'],
},
})
const typingByRoom = new Map()
// io.use: Middleware die bei jeder eingehenden WebSocket-Verbindung läuft.
// Prüft ob room und userId im Handshake vorhanden sind und ob der User in der DB existiert.
// Wer vorher nicht POST /join aufgerufen hat, hat keine gültige userId und wird abgelehnt.
io.use(async (socket, next) => {
try {
const auth = socket.handshake.auth || {}
const query = socket.handshake.query || {}
const room = readParam(auth.room, query.room)
const userId = readParam(auth.userId, query.userId)
if (!room || !userId) {
return next(new Error('Missing room or userId'))
}
const user = await database.getUser(userId)
if (!user || user.room !== room) {
return next(new Error('User not authorized for this room'))
}
// room und userId in socket.data speichern — so sind sie in registerConnection verfügbar.
socket.data.room = room
socket.data.userId = userId
next()
} catch (error) {
next(error)
}
})
io.on('connection', (socket) => {
registerConnection(io, socket, database, onError, typingByRoom).catch((error) => {
onError('Socket connection setup failed', error)
socket.disconnect(true)
})
})
return io
}
async function registerConnection(io, socket, database, onError, typingByRoom) {
const { room, userId } = socket.data
// socket.join: Fügt den Socket einem Room-Kanal hinzu — io.to(room).emit() erreicht nur diese Sockets.
socket.join(room)
const user = await database.setUserOnline(userId, room)
if (!user) {
socket.disconnect(true)
return
}
socket.data.username = user.username
socket.emit('connection:ready', { userId, room })
// Teilnehmerliste an alle im Room senden — User sieht sofort wer online ist.
await broadcastParticipants(io, database, room)
// typingByRoom: Map pro Room — speichert wer gerade tippt { userId → { userId, username } }.
socket.on('typing:start', () => {
if (!typingByRoom.has(room)) typingByRoom.set(room, new Map())
typingByRoom.get(room).set(userId, { userId, username: socket.data.username })
io.to(room).emit('typing:update', [...typingByRoom.get(room).values()])
})
socket.on('typing:stop', () => {
typingByRoom.get(room)?.delete(userId)
io.to(room).emit('typing:update', [...(typingByRoom.get(room)?.values() ?? [])])
})
socket.on('message:send', async (payload) => {
try {
const text =
typeof payload?.text === 'string' ? payload.text : payload?.payload?.text
if (!text || !text.trim()) {
return
}
// Nochmal prüfen ob der User noch im richtigen Room ist — könnte sich seit dem Verbinden geändert haben.
const activeUser = await database.getUser(userId)
if (!activeUser || activeUser.room !== room) {
return
}
// Tipp-Indikator sofort entfernen wenn eine Nachricht gesendet wird.
typingByRoom.get(room)?.delete(userId)
io.to(room).emit('typing:update', [...(typingByRoom.get(room)?.values() ?? [])])
const message = await database.addMessage({
id: randomUUID(),
room,
userId,
username: activeUser.username,
text: text.trim(),
})
// Nachricht an alle im Room senden — inkl. Sender selbst.
io.to(room).emit('message:new', message)
} catch (error) {
onError('Failed to handle socket message', error)
}
})
socket.on('disconnect', () => {
typingByRoom.get(room)?.delete(userId)
io.to(room).emit('typing:update', [...(typingByRoom.get(room)?.values() ?? [])])
// User als offline markieren und Teilnehmerliste aktualisieren.
database
.setUserOffline(userId)
.then(() => broadcastParticipants(io, database, room))
.catch((error) => onError('Failed to update presence', error))
})
socket.on('error', (error) => {
onError('Socket error', error)
})
}
async function broadcastParticipants(io, database, room) {
const participants = await database.getParticipants(room)
io.to(room).emit('participants:update', participants)
}
// Liest einen Parameter zuerst aus der primären Quelle (auth), dann aus dem Fallback (query).
// Schützt gegen leere Strings — nur echte Werte werden akzeptiert.
function readParam(primary, fallback) {
if (typeof primary === 'string' && primary.trim()) return primary
if (typeof fallback === 'string' && fallback.trim()) return fallback
return null
}
module.exports = createRealtimeServer