From d55e6bad2aac5ba14a4c4c5c48eca9fe8e253f22 Mon Sep 17 00:00:00 2001 From: Andreas <58698758+holzi1005@users.noreply.github.com> Date: Sun, 12 Oct 2025 14:55:08 +0200 Subject: [PATCH 01/18] Update websocket.js --- nodejs/websocket.js | 188 ++++++++++++++++++++++++-------------------- 1 file changed, 101 insertions(+), 87 deletions(-) diff --git a/nodejs/websocket.js b/nodejs/websocket.js index b98727c0c..7db33b946 100644 --- a/nodejs/websocket.js +++ b/nodejs/websocket.js @@ -1,75 +1,105 @@ import express from 'express'; import bodyParser from "body-parser"; - -const router = express.Router(); -const app = express(); -import http from 'http' +import http from 'http'; import https from "https"; import fs from "fs"; +import jwt from 'jsonwebtoken'; +import { Server } from "socket.io"; +import { createClient } from "ioredis"; + +import { checkFileContains } from './checkCertAndKey.js'; +import { getOnlineUSer, loginUser } from './login.mjs'; +import { websocketState } from './websocketState.mjs'; +import { + MERCURE_INTERNAL_URL, + PORT, + WEBSOCKET_SECRET, + KEY_FILE, + CERT_FILE, + REDIS_HOST = 'redis', + REDIS_PORT = 6379 +} from "./config.mjs"; -import {checkFileContains} from './checkCertAndKey.js'; -import {Server} from "socket.io"; -import jwt from 'jsonwebtoken' - -import {getOnlineUSer, loginUser} from './login.mjs' -import {websocketState} from './websocketState.mjs'; -import {MERCURE_INTERNAL_URL, PORT, WEBSOCKET_SECRET, KEY_FILE, CERT_FILE} from "./config.mjs"; - +const app = express(); +const router = express.Router(); -const keyPath = KEY_FILE; -const certPath = CERT_FILE; +// --- HTTPS oder HTTP --- let server; try { - if (checkFileContains(certPath, 'BEGIN CERTIFICATE') && checkFileContains(keyPath, 'BEGIN PRIVATE KEY')) { - console.log('We found the cert and the key file'); - console.log('The cert and key are valid.') - console.log('We start an HTTPS Server.'); + if (checkFileContains(CERT_FILE, 'BEGIN CERTIFICATE') && checkFileContains(KEY_FILE, 'BEGIN PRIVATE KEY')) { + console.log('Zertifikat & Key gefunden, starte HTTPS-Server.'); server = https.createServer({ - key: fs.readFileSync(keyPath), - cert: fs.readFileSync(certPath) - }, - app); + key: fs.readFileSync(KEY_FILE), + cert: fs.readFileSync(CERT_FILE) + }, app); } else { + console.log('Kein gültiges Zertifikat gefunden – starte HTTP-Server.'); server = http.createServer(app); } } catch (err) { - console.error(err) + console.error('Fehler beim Laden der Zertifikate:', err); server = http.createServer(app); } - -export const io = new Server(server, { +// --- Socket.IO Setup --- +const io = new Server(server, { path: '/ws', - cors: { - origin: "*", - methods: ["GET", "POST"], - } + cors: { origin: "*", methods: ["GET", "POST"] }, }); -io.use(function (socket, next) { - if (socket.handshake.query && socket.handshake.query.token) { - jwt.verify(socket.handshake.query.token, WEBSOCKET_SECRET, function (err, decoded) { - if (err) { - console.log('wrong secret. Check your secrets'); - return next(new Error('Authentication error')); - } - socket.decoded = decoded; - next(); - }); - } else { - - next(new Error('Authentication error')); - } +// --- Redis optional verbinden --- +let redisEnabled = false; + +try { + const pubClient = createClient({ host: REDIS_HOST, port: REDIS_PORT }); + const subClient = pubClient.duplicate(); + + await Promise.allSettled([pubClient.connect(), subClient.connect()]); + + // prüfen, ob Redis verbunden ist + if (pubClient.status === "ready" && subClient.status === "ready") { + const { createAdapter } = await import("@socket.io/redis-adapter"); + io.adapter(createAdapter(pubClient, subClient)); + redisEnabled = true; + console.log(`Redis-Adapter aktiviert @ ${REDIS_HOST}:${REDIS_PORT}`); + } else { + console.log("Redis nicht verfügbar – Socket.IO läuft im Standalone-Modus."); + } + + // Fehlerbehandlung + pubClient.on("error", () => { + console.log("Redis-Verbindung verloren – bleibe im Standalone-Modus."); + }); +} catch (err) { + console.log("Konnte keine Redis-Verbindung herstellen – Socket.IO läuft Standalone."); +} + +// --- Auth Middleware --- +io.use((socket, next) => { + if (socket.handshake.query && socket.handshake.query.token) { + jwt.verify(socket.handshake.query.token, WEBSOCKET_SECRET, (err, decoded) => { + if (err) { + console.log('Falscher JWT-Secret.'); + return next(new Error('Authentication error')); + } + socket.decoded = decoded; + next(); + }); + } else { + next(new Error('Authentication error')); } -) +}); +// --- WebSocket Event Handling --- io.on("connection", async (socket) => { - var jwtObj = jwt.decode(socket.handshake.query.token); - for (var i = 0; i < jwtObj.rooms.length; i++) { - socket.join(jwtObj.rooms[i]); + console.log(`🔌 Neue Verbindung: ${socket.id} (${redisEnabled ? 'Cluster' : 'Standalone'})`); + + const jwtObj = jwt.decode(socket.handshake.query.token); + if (jwtObj?.rooms) { + jwtObj.rooms.forEach(room => socket.join(room)); } - var user = loginUser(socket); + const user = loginUser(socket); if (user) { user.initUserAway(); socket.emit('sendUserStatus', user.getStatus()); @@ -77,55 +107,39 @@ io.on("connection", async (socket) => { io.emit('sendOnlineUser', JSON.stringify(getOnlineUSer())); } - socket.on('disconnect', function () { - websocketState('disconnect', socket, null); - }) + socket.on('disconnect', () => websocketState('disconnect', socket, null)); + socket.onAny((event, data) => websocketState(event, socket, data)); +}); - socket.onAny(function (event, data) { - websocketState(event, socket, data); - }) -}) -app.use(bodyParser.urlencoded({extended: false})); +// --- Express API --- +app.use(bodyParser.urlencoded({ extended: false })); app.use(bodyParser.json()); -router.post(MERCURE_INTERNAL_URL, (request, response) => { -//code to perform particular action. -//To access POST variable use req.body()methods. - console.log('Receive new Backend Request'); - const authHeader = request.headers.authorization; + +router.post(MERCURE_INTERNAL_URL, (req, res) => { + console.log('Backend Request empfangen'); + const authHeader = req.headers.authorization; if (authHeader) { const token = authHeader.split(' ')[1]; - - jwt.verify(token, WEBSOCKET_SECRET, (err, user) => { + jwt.verify(token, WEBSOCKET_SECRET, (err) => { if (err) { - console.log('Wrong JWT signature'); - return response.sendStatus(403); - } else { - - var data = request.body.data; - var room = request.body.topic; - io.to(room).emit('mercure', data); - - response.end('OK'); + console.log('Ungültige JWT-Signatur'); + return res.sendStatus(403); } + const { topic: room, data } = req.body; + io.to(room).emit('mercure', data); + res.end('OK'); }); } else { - - response.sendStatus(403); - response.end('OK'); + res.sendStatus(403); } }); -router.get(MERCURE_INTERNAL_URL, (request, response) => { -//code to perform particular action. -//To access POST variable use req.body()methods. - return response.sendStatus(200); -}); -router.get('/healthz', (request, response) => { -//code to perform particular action. -//To access POST variable use req.body()methods. - return response.sendStatus(200); -}); + +router.get(MERCURE_INTERNAL_URL, (_, res) => res.sendStatus(200)); +router.get('/healthz', (_, res) => res.sendStatus(200)); app.use("/", router); + +// --- Server starten --- server.listen(PORT, () => { - console.log('listening on *:' + PORT); -}); \ No newline at end of file + console.log(`WebSocket-Server läuft auf Port ${PORT} (${redisEnabled ? 'mit Redis-Cluster' : 'Standalone'})`); +}); From 1fcbf95cb97656d83425d902c4d398a769c919d8 Mon Sep 17 00:00:00 2001 From: Andreas <58698758+holzi1005@users.noreply.github.com> Date: Sun, 12 Oct 2025 14:57:03 +0200 Subject: [PATCH 02/18] Update config.mjs --- nodejs/config.mjs | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/nodejs/config.mjs b/nodejs/config.mjs index b002ce869..18dbb76e0 100644 --- a/nodejs/config.mjs +++ b/nodejs/config.mjs @@ -1,7 +1,13 @@ -export var WEBSOCKET_SECRET=process.env.WEBSOCKET_SECRET ||"MY_SECRET" -export var MERCURE_INTERNAL_URL=process.env.MERCURE_INTERNAL_URL || "/.well-known/mercure"; -export var PORT =process.env.PORT || 3000; -export var AWAY_TIME =process.env.AWAY_TIME || 5; -export var DEFAULT_STATE =process.env.DEFAULT_STATE || 'offline'; -export var KEY_FILE =process.env.KEY_FILE || './tls_certificate/key.pem'; -export var CERT_FILE =process.env.CERT_FILE || './tls_certificate/cert.pem'; \ No newline at end of file +export const WEBSOCKET_SECRET = process.env.WEBSOCKET_SECRET || "MY_SECRET"; +export const MERCURE_INTERNAL_URL = process.env.MERCURE_INTERNAL_URL || "/.well-known/mercure"; +export const PORT = parseInt(process.env.PORT || "3000", 10); +export const AWAY_TIME = parseInt(process.env.AWAY_TIME || "5", 10); +export const DEFAULT_STATE = process.env.DEFAULT_STATE || "offline"; +export const KEY_FILE = process.env.KEY_FILE || "./tls_certificate/key.pem"; +export const CERT_FILE = process.env.CERT_FILE || "./tls_certificate/cert.pem"; + +export const REDIS_HOST = process.env.REDIS_HOST || "redis"; +export const REDIS_PORT = parseInt(process.env.REDIS_PORT || "6379", 10); +export const REDIS_PASSWORD = process.env.REDIS_PASSWORD || ""; +export const LOG_LEVEL = process.env.LOG_LEVEL || "info"; +export const REDIS_RETRY_DELAY = parseInt(process.env.REDIS_RETRY_DELAY || "2000", 10); From 31c3f6d3b4e4a9502454d91c56e2a6bda8be7f0c Mon Sep 17 00:00:00 2001 From: Andreas <58698758+holzi1005@users.noreply.github.com> Date: Sun, 12 Oct 2025 14:58:13 +0200 Subject: [PATCH 03/18] Update config.mjs --- nodejs/config.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nodejs/config.mjs b/nodejs/config.mjs index 18dbb76e0..5d40599e3 100644 --- a/nodejs/config.mjs +++ b/nodejs/config.mjs @@ -1,7 +1,7 @@ export const WEBSOCKET_SECRET = process.env.WEBSOCKET_SECRET || "MY_SECRET"; export const MERCURE_INTERNAL_URL = process.env.MERCURE_INTERNAL_URL || "/.well-known/mercure"; -export const PORT = parseInt(process.env.PORT || "3000", 10); -export const AWAY_TIME = parseInt(process.env.AWAY_TIME || "5", 10); +export const PORT = process.env.PORT || 3000; +export const AWAY_TIME = process.env.AWAY_TIME || 5; export const DEFAULT_STATE = process.env.DEFAULT_STATE || "offline"; export const KEY_FILE = process.env.KEY_FILE || "./tls_certificate/key.pem"; export const CERT_FILE = process.env.CERT_FILE || "./tls_certificate/cert.pem"; From c4a9179aee64c2e3edaca46909a3412125e1eddb Mon Sep 17 00:00:00 2001 From: Andreas <58698758+holzi1005@users.noreply.github.com> Date: Sun, 12 Oct 2025 14:59:32 +0200 Subject: [PATCH 04/18] Update config.mjs --- nodejs/config.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nodejs/config.mjs b/nodejs/config.mjs index 5d40599e3..18dbb76e0 100644 --- a/nodejs/config.mjs +++ b/nodejs/config.mjs @@ -1,7 +1,7 @@ export const WEBSOCKET_SECRET = process.env.WEBSOCKET_SECRET || "MY_SECRET"; export const MERCURE_INTERNAL_URL = process.env.MERCURE_INTERNAL_URL || "/.well-known/mercure"; -export const PORT = process.env.PORT || 3000; -export const AWAY_TIME = process.env.AWAY_TIME || 5; +export const PORT = parseInt(process.env.PORT || "3000", 10); +export const AWAY_TIME = parseInt(process.env.AWAY_TIME || "5", 10); export const DEFAULT_STATE = process.env.DEFAULT_STATE || "offline"; export const KEY_FILE = process.env.KEY_FILE || "./tls_certificate/key.pem"; export const CERT_FILE = process.env.CERT_FILE || "./tls_certificate/cert.pem"; From 95b7d820c299da03b4390d6dfe626d96b5a7a839 Mon Sep 17 00:00:00 2001 From: Andreas <58698758+holzi1005@users.noreply.github.com> Date: Sun, 12 Oct 2025 15:01:31 +0200 Subject: [PATCH 05/18] Rename websocket.js to server.mjs --- nodejs/{websocket.js => server.mjs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename nodejs/{websocket.js => server.mjs} (100%) diff --git a/nodejs/websocket.js b/nodejs/server.mjs similarity index 100% rename from nodejs/websocket.js rename to nodejs/server.mjs From ebc64c71e5829fafeb8890f124d3fbe53e5ebcaa Mon Sep 17 00:00:00 2001 From: Andreas <58698758+holzi1005@users.noreply.github.com> Date: Sun, 12 Oct 2025 15:01:45 +0200 Subject: [PATCH 06/18] Update package.json --- nodejs/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nodejs/package.json b/nodejs/package.json index d0625dbcf..e7c2ae74b 100644 --- a/nodejs/package.json +++ b/nodejs/package.json @@ -2,7 +2,7 @@ "name": "jitsi-admin-websocket", "version": "1.0.0", "description": "", - "main": "websocket.js", + "main": "server.mjs", "type": "module", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" From e4d9fd6f89feefcbf445c5f51425822878c3570a Mon Sep 17 00:00:00 2001 From: Andreas <58698758+holzi1005@users.noreply.github.com> Date: Sun, 12 Oct 2025 15:02:08 +0200 Subject: [PATCH 07/18] Update websocketState.mjs --- nodejs/websocketState.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nodejs/websocketState.mjs b/nodejs/websocketState.mjs index 07f8cb22b..ff0c6b977 100644 --- a/nodejs/websocketState.mjs +++ b/nodejs/websocketState.mjs @@ -10,7 +10,7 @@ import { disconnectUser, checkEmptySockets, setAwayTime, getStatusForListOfIds } from './login.mjs' -import {io} from './websocket.js' +import {io} from './server.mjs' export function websocketState(event, socket, message) { From 25e6b724f13b2666b431793cfd4b4ee1facd665b Mon Sep 17 00:00:00 2001 From: Andreas <58698758+holzi1005@users.noreply.github.com> Date: Sun, 12 Oct 2025 15:08:47 +0200 Subject: [PATCH 08/18] Update login.mjs --- nodejs/login.mjs | 275 +++++++++++++++++++++++++++++------------------ 1 file changed, 171 insertions(+), 104 deletions(-) diff --git a/nodejs/login.mjs b/nodejs/login.mjs index 8933f95a0..06ab8c870 100644 --- a/nodejs/login.mjs +++ b/nodejs/login.mjs @@ -1,130 +1,197 @@ -import jwt from 'jsonwebtoken' -import {User} from "./User.mjs"; -import {WEBSOCKET_SECRET} from "./config.mjs"; - -let user = {}; - -export function loginUser(socket) { - - if (jwt.verify(socket.handshake.query.token, WEBSOCKET_SECRET)) { - var userId = getUserId(socket); - console.log('create new user'); - if (userId){ - if (typeof user[userId] === 'undefined') { - user[userId] = new User(userId, socket, getUserInitialOnlineStatus(socket)); - } else { - console.log('add user'); - user[userId].addSocket(socket); - } - return user[userId] - } - } - return null; +import jwt from "jsonwebtoken"; +import { User } from "./User.mjs"; +import { WEBSOCKET_SECRET, REDIS_HOST, REDIS_PORT, REDIS_ENABLED } from "./config.mjs"; + +let redis = null; +let user = {}; // Lokales Fallback + +// 🧠 Versuche Redis zu verbinden, wenn konfiguriert +if (REDIS_ENABLED) { + try { + const { createClient } = await import("ioredis"); + redis = createClient({ + host: REDIS_HOST, + port: REDIS_PORT, + }); + await redis.connect(); + console.log("🔗 [login.mjs] Redis-Client verbunden für globale User-Verwaltung"); + } catch (err) { + console.warn("⚠️ [login.mjs] Konnte Redis nicht verbinden, verwende lokalen User-State:", err.message); + redis = null; + } +} else { + console.log("⚙️ [login.mjs] Redis deaktiviert – verwende lokalen User-State."); } - -export function disconnectUser(socket) { - var userId = getUserId(socket); - leaveMeeting(socket); - if (user[userId]) { - user[userId].removeSocket(socket); +/** + * Benutzer beim Login registrieren + */ +export async function loginUser(socket) { + if (jwt.verify(socket.handshake.query.token, WEBSOCKET_SECRET)) { + const userId = getUserId(socket); + if (!userId) return null; + + console.log("👤 Login:", userId); + + if (redis) { + // 🧩 Benutzer in Redis registrieren + const initialStatus = getUserInitialOnlineStatus(socket); + const userData = JSON.stringify({ + id: userId, + status: initialStatus, + updatedAt: Date.now(), + }); + await redis.hset("users", userId, userData); + } else { + // 🔁 Lokales Verhalten + if (typeof user[userId] === "undefined") { + user[userId] = new User(userId, socket, getUserInitialOnlineStatus(socket)); + } else { + user[userId].addSocket(socket); + } } + return getUserFromSocket(socket); + } + return null; } -export function checkEmptySockets(socket) { - try { - return user[getUserId(socket)].checkUserLeftTheApp() - }catch (e) { - return false; - } - +/** + * Benutzer beim Disconnect abmelden + */ +export async function disconnectUser(socket) { + const userId = getUserId(socket); + leaveMeeting(socket); + if (redis) { + await redis.hdel("users", userId); + } else if (user[userId]) { + user[userId].removeSocket(socket); + } } -export function setStatus(socket, status) { - var userId = getUserId(socket); - user[userId].setStatus(status); - return user[userId]; -} - -export function setAwayTime(socket, awayTime) { - var user = getUserFromSocket(socket); - user.setAwayTime(awayTime); - return user; -} - - -export function stillOnline(socket) { - if (user[getUserId(socket)]) { - user[getUserId(socket)].initUserAway(); - } - return 0; -} - -export function enterMeeting(socket) { - if (user[getUserId(socket)]) { - user[getUserId(socket)].enterMeeting(socket); +/** + * Online-User prüfen + */ +export async function getOnlineUSer() { + if (redis) { + const all = await redis.hgetall("users"); + const result = { online: [], offline: [], away: [], busy: [] }; + + Object.entries(all).forEach(([id, data]) => { + try { + const parsed = JSON.parse(data); + const st = parsed.status || "online"; + if (!result[st]) result[st] = []; + result[st].push(id); + } catch { + result.online.push(id); + } + }); + return result; + } else { + const tmpUser = {}; + for (const prop in user) { + const u = user[prop]; + const tmpStatus = u.getStatus(); + if (typeof tmpUser[tmpStatus] === "undefined") { + tmpUser[tmpStatus] = []; + } + tmpUser[tmpStatus].push(u.getUserId()); } - return 0; + return tmpUser; + } } -export function leaveMeeting(socket) { - if (user[getUserId(socket)]) { - user[getUserId(socket)].leaveMeeting(socket); +/** + * Benutzerstatus setzen + */ +export async function setStatus(socket, status) { + const userId = getUserId(socket); + if (redis) { + const u = await redis.hget("users", userId); + if (u) { + const parsed = JSON.parse(u); + parsed.status = status; + parsed.updatedAt = Date.now(); + await redis.hset("users", userId, JSON.stringify(parsed)); } - return 0; + } else if (user[userId]) { + user[userId].setStatus(status); + } } -export function getOnlineUSer() { - var tmpUser = {}; - - for (var prop in user) { - var u = user[prop]; - - var tmpStatus = u.getStatus(); - if (typeof tmpUser[tmpStatus] === 'undefined') { - tmpUser[tmpStatus] = []; - } - tmpUser[tmpStatus].push(u.getUserId()); +/** + * Status von IDs abrufen + */ +export async function getStatusForListOfIds(socket, list) { + const ids = JSON.parse(list); + const res = {}; + + if (redis) { + const all = await redis.hmget("users", ...ids); + ids.forEach((id, idx) => { + if (all[idx]) { + const parsed = JSON.parse(all[idx]); + res[id] = parsed.status || "offline"; + } else { + res[id] = "offline"; + } + }); + } else { + for (const l of ids) { + const tmpUser = user[l]; + res[l] = tmpUser ? tmpUser.getStatus() : "offline"; } - return tmpUser; + } + socket.emit("giveOnlineStatus", JSON.stringify(res)); } -export function getUserStatus(socket) { - if (user[getUserId(socket)]) { - return user[getUserId(socket)].getStatus(); - } +/** + * Hilfsfunktionen + */ +export function getUserFromSocket(socket) { + const userId = getUserId(socket); + return user[userId] ? user[userId] : null; } function getUserId(socket) { - var jwtObj = jwt.decode(socket.handshake.query.token); - var userId = jwtObj.sub - return userId; + const jwtObj = jwt.decode(socket.handshake.query.token); + return jwtObj?.sub; } function getUserInitialOnlineStatus(socket) { - var jwtObj = jwt.decode(socket.handshake.query.token); - return jwtObj.status === 1 ? 'online' : 'offline'; + const jwtObj = jwt.decode(socket.handshake.query.token); + return jwtObj?.status === 1 ? "online" : "offline"; } -export function getUserFromSocket(socket) { - var userId = getUserId(socket); - return user[userId] ? user[userId] : null; +// Optional: Fallback-Funktionen +export function stillOnline(socket) { + if (!redis && user[getUserId(socket)]) { + user[getUserId(socket)].initUserAway(); + } } -export function getStatusForListOfIds(socket,list) { - var list = JSON.parse(list); - var res = {}; - for (var l of list){ - try { - var tmpUser = user[l]; - if (tmpUser){ - res[l] = tmpUser.getStatus(); - }else { - res[l] = 'offline'; - } - }catch (e) { - res[l] = 'offline'; - } +export function enterMeeting(socket) { + if (!redis && user[getUserId(socket)]) { + user[getUserId(socket)].enterMeeting(socket); + } +} +export function leaveMeeting(socket) { + if (!redis && user[getUserId(socket)]) { + user[getUserId(socket)].leaveMeeting(socket); + } +} +export function checkEmptySockets(socket) { + if (!redis) { + try { + return user[getUserId(socket)].checkUserLeftTheApp(); + } catch { + return false; } - - socket.emit('giveOnlineStatus',JSON.stringify(res)); -} \ No newline at end of file + } + return false; +} +export function setAwayTime(socket, awayTime) { + if (!redis) { + const u = getUserFromSocket(socket); + if (u) u.setAwayTime(awayTime); + } +} From ab7a438f92c8c1b18c1d23764d62c280d42636e1 Mon Sep 17 00:00:00 2001 From: Andreas <58698758+holzi1005@users.noreply.github.com> Date: Sun, 12 Oct 2025 15:11:01 +0200 Subject: [PATCH 09/18] Update server.mjs --- nodejs/server.mjs | 193 ++++++++++++++++++---------------------------- 1 file changed, 75 insertions(+), 118 deletions(-) diff --git a/nodejs/server.mjs b/nodejs/server.mjs index 7db33b946..412f2290c 100644 --- a/nodejs/server.mjs +++ b/nodejs/server.mjs @@ -1,145 +1,102 @@ -import express from 'express'; +import express from "express"; import bodyParser from "body-parser"; -import http from 'http'; +import http from "http"; import https from "https"; import fs from "fs"; -import jwt from 'jsonwebtoken'; +import jwt from "jsonwebtoken"; import { Server } from "socket.io"; -import { createClient } from "ioredis"; -import { checkFileContains } from './checkCertAndKey.js'; -import { getOnlineUSer, loginUser } from './login.mjs'; -import { websocketState } from './websocketState.mjs'; -import { - MERCURE_INTERNAL_URL, - PORT, - WEBSOCKET_SECRET, - KEY_FILE, - CERT_FILE, - REDIS_HOST = 'redis', - REDIS_PORT = 6379 +import { checkFileContains } from "./checkCertAndKey.js"; +import { + getOnlineUSer, + loginUser +} from "./login.mjs"; +import { websocketState } from "./websocketState.mjs"; +import { + MERCURE_INTERNAL_URL, + PORT, + WEBSOCKET_SECRET, + KEY_FILE, + CERT_FILE, + REDIS_ENABLED, + REDIS_HOST, + REDIS_PORT } from "./config.mjs"; const app = express(); const router = express.Router(); -// --- HTTPS oder HTTP --- let server; + try { - if (checkFileContains(CERT_FILE, 'BEGIN CERTIFICATE') && checkFileContains(KEY_FILE, 'BEGIN PRIVATE KEY')) { - console.log('Zertifikat & Key gefunden, starte HTTPS-Server.'); - server = https.createServer({ - key: fs.readFileSync(KEY_FILE), - cert: fs.readFileSync(CERT_FILE) - }, app); - } else { - console.log('Kein gültiges Zertifikat gefunden – starte HTTP-Server.'); - server = http.createServer(app); - } -} catch (err) { - console.error('Fehler beim Laden der Zertifikate:', err); + if ( + checkFileContains(CERT_FILE, "BEGIN CERTIFICATE") && + checkFileContains(KEY_FILE, "BEGIN PRIVATE KEY") + ) { + console.log("✅ Zertifikat & Key gefunden – HTTPS Server wird gestartet."); + server = https.createServer( + { + key: fs.readFileSync(KEY_FILE), + cert: fs.readFileSync(CERT_FILE) + }, + app + ); + } else { + console.log("⚠️ Zertifikat ungültig oder nicht gefunden – HTTP Server wird gestartet."); server = http.createServer(app); + } +} catch (err) { + console.error("❌ HTTPS Setup fehlgeschlagen:", err); + server = http.createServer(app); } -// --- Socket.IO Setup --- -const io = new Server(server, { - path: '/ws', - cors: { origin: "*", methods: ["GET", "POST"] }, +// 🧠 Socket.IO Server-Instanz +export const io = new Server(server, { + path: "/ws", + cors: { + origin: "*", + methods: ["GET", "POST"] + } }); -// --- Redis optional verbinden --- -let redisEnabled = false; +if (REDIS_ENABLED) { + try { + const { createAdapter } = await import("@socket.io/redis-adapter"); + const { createClient } = await import("redis"); -try { - const pubClient = createClient({ host: REDIS_HOST, port: REDIS_PORT }); + const pubClient = createClient({ url: `redis://${REDIS_HOST}:${REDIS_PORT}` }); const subClient = pubClient.duplicate(); - await Promise.allSettled([pubClient.connect(), subClient.connect()]); - - // prüfen, ob Redis verbunden ist - if (pubClient.status === "ready" && subClient.status === "ready") { - const { createAdapter } = await import("@socket.io/redis-adapter"); - io.adapter(createAdapter(pubClient, subClient)); - redisEnabled = true; - console.log(`Redis-Adapter aktiviert @ ${REDIS_HOST}:${REDIS_PORT}`); - } else { - console.log("Redis nicht verfügbar – Socket.IO läuft im Standalone-Modus."); - } + await pubClient.connect(); + await subClient.connect(); - // Fehlerbehandlung - pubClient.on("error", () => { - console.log("Redis-Verbindung verloren – bleibe im Standalone-Modus."); - }); -} catch (err) { - console.log("Konnte keine Redis-Verbindung herstellen – Socket.IO läuft Standalone."); + io.adapter(createAdapter(pubClient, subClient)); + console.log(`🔗 [Socket.IO] Redis-Adapter aktiviert (${REDIS_HOST}:${REDIS_PORT})`); + } catch (err) { + console.warn("⚠️ [Socket.IO] Redis-Adapter konnte nicht initialisiert werden – Fallback auf Standalone:", err.message); + } +} else { + console.log("⚙️ [Socket.IO] Redis deaktiviert – läuft im Single-Node-Modus."); } -// --- Auth Middleware --- -io.use((socket, next) => { - if (socket.handshake.query && socket.handshake.query.token) { - jwt.verify(socket.handshake.query.token, WEBSOCKET_SECRET, (err, decoded) => { - if (err) { - console.log('Falscher JWT-Secret.'); - return next(new Error('Authentication error')); - } - socket.decoded = decoded; - next(); - }); - } else { - next(new Error('Authentication error')); - } +// 🧱 Authentifizierung über JWT +io.use(function (socket, next) { + if (socket.handshake.query && socket.handshake.query.token) { + jwt.verify(socket.handshake.query.token, WEBSOCKET_SECRET, function (err, decoded) { + if (err) { + console.log("❌ Ungültiges JWT-Secret"); + return next(new Error("Authentication error")); + } + socket.decoded = decoded; + next(); + }); + } else { + next(new Error("Authentication error")); + } }); -// --- WebSocket Event Handling --- io.on("connection", async (socket) => { - console.log(`🔌 Neue Verbindung: ${socket.id} (${redisEnabled ? 'Cluster' : 'Standalone'})`); - - const jwtObj = jwt.decode(socket.handshake.query.token); - if (jwtObj?.rooms) { - jwtObj.rooms.forEach(room => socket.join(room)); - } + const jwtObj = jwt.decode(socket.handshake.query.token); + if (!jwtObj || !jwtObj.rooms) return; - const user = loginUser(socket); - if (user) { - user.initUserAway(); - socket.emit('sendUserStatus', user.getStatus()); - socket.emit('sendUserTimeAway', user.awayTime); - io.emit('sendOnlineUser', JSON.stringify(getOnlineUSer())); - } - - socket.on('disconnect', () => websocketState('disconnect', socket, null)); - socket.onAny((event, data) => websocketState(event, socket, data)); -}); - -// --- Express API --- -app.use(bodyParser.urlencoded({ extended: false })); -app.use(bodyParser.json()); - -router.post(MERCURE_INTERNAL_URL, (req, res) => { - console.log('Backend Request empfangen'); - const authHeader = req.headers.authorization; - if (authHeader) { - const token = authHeader.split(' ')[1]; - jwt.verify(token, WEBSOCKET_SECRET, (err) => { - if (err) { - console.log('Ungültige JWT-Signatur'); - return res.sendStatus(403); - } - const { topic: room, data } = req.body; - io.to(room).emit('mercure', data); - res.end('OK'); - }); - } else { - res.sendStatus(403); - } -}); - -router.get(MERCURE_INTERNAL_URL, (_, res) => res.sendStatus(200)); -router.get('/healthz', (_, res) => res.sendStatus(200)); - -app.use("/", router); - -// --- Server starten --- -server.listen(PORT, () => { - console.log(`WebSocket-Server läuft auf Port ${PORT} (${redisEnabled ? 'mit Redis-Cluster' : 'Standalone'})`); -}); + for (const room of j From 0fe2a85177ee02631cda48cf25437b57d151ea35 Mon Sep 17 00:00:00 2001 From: Andreas <58698758+holzi1005@users.noreply.github.com> Date: Sun, 12 Oct 2025 15:15:07 +0200 Subject: [PATCH 10/18] Update User.mjs --- nodejs/User.mjs | 114 ++++++++++++++++++++---------------------------- 1 file changed, 47 insertions(+), 67 deletions(-) diff --git a/nodejs/User.mjs b/nodejs/User.mjs index f05849eaa..eb17a9b40 100644 --- a/nodejs/User.mjs +++ b/nodejs/User.mjs @@ -1,6 +1,6 @@ -import {io} from "./websocket.js"; -import {getOnlineUSer, getUserStatus} from "./login.mjs"; -import {AWAY_TIME, DEFAULT_STATE} from "./config.mjs"; +import {io} from './server.mjs' +import { getOnlineUSer } from "./login.mjs"; +import { AWAY_TIME, DEFAULT_STATE } from "./config.mjs"; class User { userId; @@ -12,130 +12,110 @@ class User { offline = false; away = false; awayTime = 0; + constructor(userId, socket, status) { + this.userId = userId; this.sockets.push(socket); this.status = status; - this.initUserAway(); - this.userId = userId; this.offline = status === DEFAULT_STATE; - this.awayTime=AWAY_TIME; + this.awayTime = AWAY_TIME; + this.initUserAway(); // async-Aufruf kann bei Konstruktoren nicht await sein } addSocket(socket) { - if ((this.sockets.indexOf(socket) === -1)) { + if (!this.sockets.includes(socket)) { this.sockets.push(socket); } } removeSocket(socket) { const index = this.sockets.indexOf(socket); - if (index > -1) { // only splice array when item is found - this.sockets.splice(index, 1); // 2nd parameter means remove one item only + if (index > -1) { + this.sockets.splice(index, 1); } - - } - - setStatus(status) { - if (status === 'offline') { - this.offline = true; - } else { - this.offline = false; - } - this.status = status; - this.initUserAway(); } hasSocket(socket) { - if (socket in this.sockets) { - return true - } - return false; + return this.sockets.includes(socket); } getSockets() { - return this.sockets + return this.sockets; + } + + async setStatus(status) { + this.status = status; + this.offline = status === 'offline'; + await this.initUserAway(); } getStatus() { - if (this.offline || this.sockets.length === 0) { - return 'offline'; - } - if (this.inMeeting.length > 0) { - return 'inMeeting'; - } - if (this.away){ - return 'away'; - } + if (this.offline || this.sockets.length === 0) return 'offline'; + if (this.inMeeting.length > 0) return 'inMeeting'; + if (this.away) return 'away'; return this.status; } setAlive() { - this.initUserAway() + this.initUserAway(); } - sendStatus() { - io.emit('sendOnlineUser', JSON.stringify(getOnlineUSer())); - this.sendToAllSockets('sendUserStatus',this.status); + async sendStatus() { + const onlineUsers = await getOnlineUSer(); // global aus Redis oder lokal + io.emit('sendOnlineUser', JSON.stringify(onlineUsers)); + this.sendToAllSockets('sendUserStatus', this.getStatus()); } getUserId() { return this.userId; } - setuserId(value) { - this._userId = value; - } - - initUserAway() { + async initUserAway() { clearTimeout(this.awayTimer); this.awayTimer = null; - var that = this; + if (this.away === true) { this.away = false; - this.sendStatus(); + await this.sendStatus(); } + this.oldStatus = null; - this.awayTimer = setTimeout(function () { - that.away = true; - that.sendStatus(); - }, 60000 * this.awayTime) + this.awayTimer = setTimeout(async () => { + this.away = true; + await this.sendStatus(); + }, 60000 * this.awayTime); } enterMeeting(socket) { - if (!this.inMeeting.includes(socket)) { - this.inMeeting.push(socket); - } + if (!this.inMeeting.includes(socket)) this.inMeeting.push(socket); } leaveMeeting(socket) { - for (var i = 0; i < this.inMeeting.length; i++) { - if (this.inMeeting[i] === socket) { - this.inMeeting.splice(i, 1); - } - } + const idx = this.inMeeting.indexOf(socket); + if (idx > -1) this.inMeeting.splice(idx, 1); } checkUserLeftTheApp() { return this.sockets.length === 0; } - setAwayTime(awayTime){ + + async setAwayTime(awayTime) { try { awayTime = parseInt(awayTime); - if (Number.isInteger(awayTime)){ + if (Number.isInteger(awayTime)) { this.awayTime = awayTime; - this.sendToAllSockets('sendUserTimeAway',this.awayTime); + this.sendToAllSockets('sendUserTimeAway', this.awayTime); } - }catch (e) { - console.log(e) + } catch (e) { + console.error(e); } } - sendToAllSockets(ev,message){ - for (var prop in this.sockets) { - var tmpSocket = this.sockets[prop]; - tmpSocket.emit(ev, message); + sendToAllSockets(ev, message) { + for (const socket of this.sockets) { + socket.emit(ev, message); } } } -export {User}; \ No newline at end of file +export { User }; From 87562941e879006132c2d3658ebc7b044c76686d Mon Sep 17 00:00:00 2001 From: Andreas <58698758+holzi1005@users.noreply.github.com> Date: Sun, 12 Oct 2025 15:18:07 +0200 Subject: [PATCH 11/18] Update config.mjs --- nodejs/config.mjs | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/nodejs/config.mjs b/nodejs/config.mjs index 18dbb76e0..0a7dfa190 100644 --- a/nodejs/config.mjs +++ b/nodejs/config.mjs @@ -1,13 +1,12 @@ -export const WEBSOCKET_SECRET = process.env.WEBSOCKET_SECRET || "MY_SECRET"; -export const MERCURE_INTERNAL_URL = process.env.MERCURE_INTERNAL_URL || "/.well-known/mercure"; -export const PORT = parseInt(process.env.PORT || "3000", 10); -export const AWAY_TIME = parseInt(process.env.AWAY_TIME || "5", 10); -export const DEFAULT_STATE = process.env.DEFAULT_STATE || "offline"; -export const KEY_FILE = process.env.KEY_FILE || "./tls_certificate/key.pem"; -export const CERT_FILE = process.env.CERT_FILE || "./tls_certificate/cert.pem"; +export var WEBSOCKET_SECRET = process.env.WEBSOCKET_SECRET || "MY_SECRET"; +export var MERCURE_INTERNAL_URL = process.env.MERCURE_INTERNAL_URL || "/.well-known/mercure"; +export var PORT = process.env.PORT || 3000; +export var AWAY_TIME = process.env.AWAY_TIME || 5; +export var DEFAULT_STATE = process.env.DEFAULT_STATE || "offline"; +export var KEY_FILE = process.env.KEY_FILE || "./tls_certificate/key.pem"; +export var CERT_FILE = process.env.CERT_FILE || "./tls_certificate/cert.pem"; -export const REDIS_HOST = process.env.REDIS_HOST || "redis"; -export const REDIS_PORT = parseInt(process.env.REDIS_PORT || "6379", 10); -export const REDIS_PASSWORD = process.env.REDIS_PASSWORD || ""; -export const LOG_LEVEL = process.env.LOG_LEVEL || "info"; -export const REDIS_RETRY_DELAY = parseInt(process.env.REDIS_RETRY_DELAY || "2000", 10); +// Redis für Clusterbetrieb +export var REDIS_ENABLED = process.env.REDIS_ENABLED === "true"; +export var REDIS_HOST = process.env.REDIS_HOST || "redis"; +export var REDIS_PORT = process.env.REDIS_PORT || 6379; From 757acf188c4fe22ea50663a6f3aa331aad9d104b Mon Sep 17 00:00:00 2001 From: Andreas <58698758+holzi1005@users.noreply.github.com> Date: Sun, 12 Oct 2025 15:18:31 +0200 Subject: [PATCH 12/18] Update login.mjs --- nodejs/login.mjs | 173 +++++++++++------------------------------------ 1 file changed, 40 insertions(+), 133 deletions(-) diff --git a/nodejs/login.mjs b/nodejs/login.mjs index 06ab8c870..00b10e301 100644 --- a/nodejs/login.mjs +++ b/nodejs/login.mjs @@ -1,156 +1,96 @@ import jwt from "jsonwebtoken"; import { User } from "./User.mjs"; -import { WEBSOCKET_SECRET, REDIS_HOST, REDIS_PORT, REDIS_ENABLED } from "./config.mjs"; +import { WEBSOCKET_SECRET, REDIS_ENABLED, REDIS_HOST, REDIS_PORT } from "./config.mjs"; let redis = null; -let user = {}; // Lokales Fallback +let user = {}; // lokaler Fallback -// 🧠 Versuche Redis zu verbinden, wenn konfiguriert +// Redis verbinden, falls aktiviert if (REDIS_ENABLED) { try { const { createClient } = await import("ioredis"); - redis = createClient({ - host: REDIS_HOST, - port: REDIS_PORT, - }); + redis = createClient({ host: REDIS_HOST, port: REDIS_PORT }); await redis.connect(); - console.log("🔗 [login.mjs] Redis-Client verbunden für globale User-Verwaltung"); + console.log("🔗 Redis-Client verbunden für globale User-Liste"); } catch (err) { - console.warn("⚠️ [login.mjs] Konnte Redis nicht verbinden, verwende lokalen User-State:", err.message); + console.warn("⚠️ Redis nicht erreichbar – verwende lokalen User-State:", err.message); redis = null; } -} else { - console.log("⚙️ [login.mjs] Redis deaktiviert – verwende lokalen User-State."); } -/** - * Benutzer beim Login registrieren - */ export async function loginUser(socket) { if (jwt.verify(socket.handshake.query.token, WEBSOCKET_SECRET)) { const userId = getUserId(socket); if (!userId) return null; - console.log("👤 Login:", userId); + const initialStatus = getUserInitialOnlineStatus(socket); if (redis) { - // 🧩 Benutzer in Redis registrieren - const initialStatus = getUserInitialOnlineStatus(socket); - const userData = JSON.stringify({ - id: userId, - status: initialStatus, - updatedAt: Date.now(), - }); - await redis.hset("users", userId, userData); - } else { - // 🔁 Lokales Verhalten - if (typeof user[userId] === "undefined") { - user[userId] = new User(userId, socket, getUserInitialOnlineStatus(socket)); - } else { - user[userId].addSocket(socket); + try { + await redis.hset("users", userId, JSON.stringify({ id: userId, status: initialStatus, updatedAt: Date.now() })); + } catch (err) { + console.error(`[Redis] Fehler beim Speichern von User ${userId}:`, err.message); } + } else { + if (!user[userId]) user[userId] = new User(userId, socket, initialStatus); + else user[userId].addSocket(socket); } + return getUserFromSocket(socket); } return null; } -/** - * Benutzer beim Disconnect abmelden - */ export async function disconnectUser(socket) { const userId = getUserId(socket); leaveMeeting(socket); if (redis) { - await redis.hdel("users", userId); + try { + await redis.hdel("users", userId); + } catch (err) { + console.error(`[Redis] Fehler beim Löschen von User ${userId}:`, err.message); + } } else if (user[userId]) { user[userId].removeSocket(socket); } } -/** - * Online-User prüfen - */ export async function getOnlineUSer() { if (redis) { - const all = await redis.hgetall("users"); - const result = { online: [], offline: [], away: [], busy: [] }; - - Object.entries(all).forEach(([id, data]) => { - try { - const parsed = JSON.parse(data); - const st = parsed.status || "online"; - if (!result[st]) result[st] = []; - result[st].push(id); - } catch { - result.online.push(id); + try { + const all = await redis.hgetall("users"); + const result = {}; + for (const [id, val] of Object.entries(all)) { + try { + const parsed = JSON.parse(val); + const st = parsed.status || "online"; + if (!result[st]) result[st] = []; + result[st].push(id); + } catch { + if (!result.online) result.online = []; + result.online.push(id); + } } - }); - return result; + return result; + } catch (err) { + console.error("[Redis] Fehler beim Abrufen der User-Liste:", err.message); + return {}; + } } else { const tmpUser = {}; for (const prop in user) { const u = user[prop]; const tmpStatus = u.getStatus(); - if (typeof tmpUser[tmpStatus] === "undefined") { - tmpUser[tmpStatus] = []; - } + if (!tmpUser[tmpStatus]) tmpUser[tmpStatus] = []; tmpUser[tmpStatus].push(u.getUserId()); } return tmpUser; } } -/** - * Benutzerstatus setzen - */ -export async function setStatus(socket, status) { - const userId = getUserId(socket); - if (redis) { - const u = await redis.hget("users", userId); - if (u) { - const parsed = JSON.parse(u); - parsed.status = status; - parsed.updatedAt = Date.now(); - await redis.hset("users", userId, JSON.stringify(parsed)); - } - } else if (user[userId]) { - user[userId].setStatus(status); - } -} - -/** - * Status von IDs abrufen - */ -export async function getStatusForListOfIds(socket, list) { - const ids = JSON.parse(list); - const res = {}; - - if (redis) { - const all = await redis.hmget("users", ...ids); - ids.forEach((id, idx) => { - if (all[idx]) { - const parsed = JSON.parse(all[idx]); - res[id] = parsed.status || "offline"; - } else { - res[id] = "offline"; - } - }); - } else { - for (const l of ids) { - const tmpUser = user[l]; - res[l] = tmpUser ? tmpUser.getStatus() : "offline"; - } - } - socket.emit("giveOnlineStatus", JSON.stringify(res)); -} - -/** - * Hilfsfunktionen - */ export function getUserFromSocket(socket) { const userId = getUserId(socket); - return user[userId] ? user[userId] : null; + return user[userId] || null; } function getUserId(socket) { @@ -162,36 +102,3 @@ function getUserInitialOnlineStatus(socket) { const jwtObj = jwt.decode(socket.handshake.query.token); return jwtObj?.status === 1 ? "online" : "offline"; } - -// Optional: Fallback-Funktionen -export function stillOnline(socket) { - if (!redis && user[getUserId(socket)]) { - user[getUserId(socket)].initUserAway(); - } -} -export function enterMeeting(socket) { - if (!redis && user[getUserId(socket)]) { - user[getUserId(socket)].enterMeeting(socket); - } -} -export function leaveMeeting(socket) { - if (!redis && user[getUserId(socket)]) { - user[getUserId(socket)].leaveMeeting(socket); - } -} -export function checkEmptySockets(socket) { - if (!redis) { - try { - return user[getUserId(socket)].checkUserLeftTheApp(); - } catch { - return false; - } - } - return false; -} -export function setAwayTime(socket, awayTime) { - if (!redis) { - const u = getUserFromSocket(socket); - if (u) u.setAwayTime(awayTime); - } -} From 207b6aee5bd9cbc032fbe0ecff184c48598230ab Mon Sep 17 00:00:00 2001 From: Andreas <58698758+holzi1005@users.noreply.github.com> Date: Sun, 12 Oct 2025 15:18:45 +0200 Subject: [PATCH 13/18] Update User.mjs --- nodejs/User.mjs | 54 +++++++++++++++---------------------------------- 1 file changed, 16 insertions(+), 38 deletions(-) diff --git a/nodejs/User.mjs b/nodejs/User.mjs index eb17a9b40..bb9646723 100644 --- a/nodejs/User.mjs +++ b/nodejs/User.mjs @@ -1,4 +1,4 @@ -import {io} from './server.mjs' +import { io } from "./websocket.js"; import { getOnlineUSer } from "./login.mjs"; import { AWAY_TIME, DEFAULT_STATE } from "./config.mjs"; @@ -15,38 +15,20 @@ class User { constructor(userId, socket, status) { this.userId = userId; - this.sockets.push(socket); this.status = status; + this.sockets.push(socket); this.offline = status === DEFAULT_STATE; this.awayTime = AWAY_TIME; - this.initUserAway(); // async-Aufruf kann bei Konstruktoren nicht await sein + this.initUserAway(); // async, kein await im Konstruktor möglich } addSocket(socket) { - if (!this.sockets.includes(socket)) { - this.sockets.push(socket); - } + if (!this.sockets.includes(socket)) this.sockets.push(socket); } removeSocket(socket) { const index = this.sockets.indexOf(socket); - if (index > -1) { - this.sockets.splice(index, 1); - } - } - - hasSocket(socket) { - return this.sockets.includes(socket); - } - - getSockets() { - return this.sockets; - } - - async setStatus(status) { - this.status = status; - this.offline = status === 'offline'; - await this.initUserAway(); + if (index > -1) this.sockets.splice(index, 1); } getStatus() { @@ -56,20 +38,12 @@ class User { return this.status; } - setAlive() { - this.initUserAway(); - } - async sendStatus() { - const onlineUsers = await getOnlineUSer(); // global aus Redis oder lokal + const onlineUsers = await getOnlineUSer(); io.emit('sendOnlineUser', JSON.stringify(onlineUsers)); this.sendToAllSockets('sendUserStatus', this.getStatus()); } - getUserId() { - return this.userId; - } - async initUserAway() { clearTimeout(this.awayTimer); this.awayTimer = null; @@ -86,6 +60,16 @@ class User { }, 60000 * this.awayTime); } + async setStatus(status) { + this.status = status; + this.offline = status === 'offline'; + await this.initUserAway(); + } + + sendToAllSockets(ev, message) { + for (const socket of this.sockets) socket.emit(ev, message); + } + enterMeeting(socket) { if (!this.inMeeting.includes(socket)) this.inMeeting.push(socket); } @@ -110,12 +94,6 @@ class User { console.error(e); } } - - sendToAllSockets(ev, message) { - for (const socket of this.sockets) { - socket.emit(ev, message); - } - } } export { User }; From 2b0d824e288d5f99a562350114cc04d1de1d4892 Mon Sep 17 00:00:00 2001 From: Andreas <58698758+holzi1005@users.noreply.github.com> Date: Sun, 12 Oct 2025 15:20:32 +0200 Subject: [PATCH 14/18] Update websocketState.mjs --- nodejs/websocketState.mjs | 46 ++++++++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/nodejs/websocketState.mjs b/nodejs/websocketState.mjs index ff0c6b977..d612110c5 100644 --- a/nodejs/websocketState.mjs +++ b/nodejs/websocketState.mjs @@ -8,55 +8,67 @@ import { getUserStatus, getUserFromSocket, disconnectUser, - checkEmptySockets, setAwayTime, getStatusForListOfIds + checkEmptySockets, + setAwayTime, + getStatusForListOfIds } from './login.mjs' -import {io} from './server.mjs' -export function websocketState(event, socket, message) { +export async function websocketState(event, socket, message) { switch (event) { case 'disconnect': disconnectUser(socket); - setTimeout(function () { + setTimeout(async function () { if (checkEmptySockets(socket)) { - io.emit('sendOnlineUser', JSON.stringify(getOnlineUSer())); + io.emit('sendOnlineUser', JSON.stringify(await getOnlineUSer())); console.log('Send is Offline'); } - sendStatus(socket); + await sendStatus(socket); }, 7000); break; - case 'login'://fügt den SOcket zu dem USer hinzu. Schickt keine Benachrichtigungen an die anderen Clients + + case 'login': break; - case 'setStatus'://setzt den Status und informiert alle Clients, das sich der Status geändert hat - setStatus(socket, message); - sendStatus(socket); + + case 'setStatus': + await setStatus(socket, message); + await sendStatus(socket); break; + case 'getStatus': - io.emit('sendOnlineUser', JSON.stringify(getOnlineUSer())); + io.emit('sendOnlineUser', JSON.stringify(await getOnlineUSer())); break; + case 'getMyStatus': socket.emit('sendUserStatus', getUserStatus(socket)); break; + case 'stillOnline': stillOnline(socket); break; + case 'enterMeeting': enterMeeting(socket); - sendStatus(socket); + await sendStatus(socket); break; + case 'leaveMeeting': leaveMeeting(socket); - sendStatus(socket); + await sendStatus(socket); break; + case 'openNewIframe': sendNewIframe(socket, message) break; + case 'giveOnlineStatus': getStatusForListOfIds(socket, message); break; + case 'setAwayTime': setAwayTime(socket, message); break; + default: console.log(event); console.log('not known') @@ -64,12 +76,12 @@ export function websocketState(event, socket, message) { } } -function sendStatus(socket) { - sendStatusToOwnUSer(socket); - io.emit('sendOnlineUser', JSON.stringify(getOnlineUSer())); +async function sendStatus(socket) { + await sendStatusToOwnUSer(socket); + io.emit('sendOnlineUser', JSON.stringify(await getOnlineUSer())); } -function sendStatusToOwnUSer(socket) { +async function sendStatusToOwnUSer(socket) { var user = getUserFromSocket(socket) if (user) { user.sendToAllSockets('sendUserStatus', user.getStatus()); From 5611c51176556a725ab7c4754d6c88def46fbc9e Mon Sep 17 00:00:00 2001 From: Andreas <58698758+holzi1005@users.noreply.github.com> Date: Sun, 12 Oct 2025 15:21:25 +0200 Subject: [PATCH 15/18] Update server.mjs --- nodejs/server.mjs | 107 +++++++++++++++++++++++++++++++--------------- 1 file changed, 72 insertions(+), 35 deletions(-) diff --git a/nodejs/server.mjs b/nodejs/server.mjs index 412f2290c..e68b8b56e 100644 --- a/nodejs/server.mjs +++ b/nodejs/server.mjs @@ -7,11 +7,8 @@ import jwt from "jsonwebtoken"; import { Server } from "socket.io"; import { checkFileContains } from "./checkCertAndKey.js"; -import { - getOnlineUSer, - loginUser -} from "./login.mjs"; import { websocketState } from "./websocketState.mjs"; +import { loginUser, getOnlineUSer } from "./login.mjs"; import { MERCURE_INTERNAL_URL, PORT, @@ -25,24 +22,18 @@ import { const app = express(); const router = express.Router(); - let server; +// HTTP oder HTTPS try { - if ( - checkFileContains(CERT_FILE, "BEGIN CERTIFICATE") && - checkFileContains(KEY_FILE, "BEGIN PRIVATE KEY") - ) { - console.log("✅ Zertifikat & Key gefunden – HTTPS Server wird gestartet."); - server = https.createServer( - { - key: fs.readFileSync(KEY_FILE), - cert: fs.readFileSync(CERT_FILE) - }, - app - ); + if (checkFileContains(CERT_FILE, "BEGIN CERTIFICATE") && checkFileContains(KEY_FILE, "BEGIN PRIVATE KEY")) { + console.log("✅ HTTPS Server wird gestartet."); + server = https.createServer({ + key: fs.readFileSync(KEY_FILE), + cert: fs.readFileSync(CERT_FILE) + }, app); } else { - console.log("⚠️ Zertifikat ungültig oder nicht gefunden – HTTP Server wird gestartet."); + console.log("⚠️ HTTPS Zertifikat nicht gefunden, HTTP Server wird gestartet."); server = http.createServer(app); } } catch (err) { @@ -50,7 +41,7 @@ try { server = http.createServer(app); } -// 🧠 Socket.IO Server-Instanz +// Socket.IO Server export const io = new Server(server, { path: "/ws", cors: { @@ -59,6 +50,7 @@ export const io = new Server(server, { } }); +// Optional: Redis Adapter für Cluster if (REDIS_ENABLED) { try { const { createAdapter } = await import("@socket.io/redis-adapter"); @@ -71,32 +63,77 @@ if (REDIS_ENABLED) { await subClient.connect(); io.adapter(createAdapter(pubClient, subClient)); - console.log(`🔗 [Socket.IO] Redis-Adapter aktiviert (${REDIS_HOST}:${REDIS_PORT})`); + console.log(`🔗 Redis-Adapter aktiviert (${REDIS_HOST}:${REDIS_PORT})`); } catch (err) { - console.warn("⚠️ [Socket.IO] Redis-Adapter konnte nicht initialisiert werden – Fallback auf Standalone:", err.message); + console.warn("⚠️ Redis-Adapter konnte nicht initialisiert werden, Standalone läuft:", err.message); } } else { - console.log("⚙️ [Socket.IO] Redis deaktiviert – läuft im Single-Node-Modus."); + console.log("⚙️ Redis deaktiviert – Standalone-Modus"); } -// 🧱 Authentifizierung über JWT -io.use(function (socket, next) { - if (socket.handshake.query && socket.handshake.query.token) { - jwt.verify(socket.handshake.query.token, WEBSOCKET_SECRET, function (err, decoded) { - if (err) { - console.log("❌ Ungültiges JWT-Secret"); - return next(new Error("Authentication error")); - } +// JWT Auth +io.use((socket, next) => { + if (socket.handshake.query?.token) { + jwt.verify(socket.handshake.query.token, WEBSOCKET_SECRET, (err, decoded) => { + if (err) return next(new Error("Authentication error")); socket.decoded = decoded; next(); }); - } else { - next(new Error("Authentication error")); - } + } else next(new Error("Authentication error")); }); +// Socket.IO Events io.on("connection", async (socket) => { const jwtObj = jwt.decode(socket.handshake.query.token); - if (!jwtObj || !jwtObj.rooms) return; + if (jwtObj?.rooms) jwtObj.rooms.forEach(room => socket.join(room)); + + const user = await loginUser(socket); + if (user) { + user.initUserAway?.(); + socket.emit("sendUserStatus", user.getStatus?.()); + socket.emit("sendUserTimeAway", user.awayTime ?? 0); + io.emit("sendOnlineUser", JSON.stringify(await getOnlineUSer())); + } + + socket.on("disconnect", () => websocketState("disconnect", socket, null)); + socket.onAny((event, data) => websocketState(event, socket, data)); +}); + +// Express Middleware +app.use(bodyParser.urlencoded({ extended: false })); +app.use(bodyParser.json()); + +// MERCURE Webhook +router.post(MERCURE_INTERNAL_URL, async (req, res) => { + const authHeader = req.headers.authorization; + if (!authHeader) return res.sendStatus(403); - for (const room of j + const token = authHeader.split(" ")[1]; + jwt.verify(token, WEBSOCKET_SECRET, async (err) => { + if (err) return res.sendStatus(403); + + const data = req.body.data; + const room = req.body.topic; + io.to(room).emit("mercure", data); + res.end("OK"); + }); +}); + +router.get(MERCURE_INTERNAL_URL, (_, res) => res.sendStatus(200)); +router.get("/healthz", (_, res) => res.sendStatus(200)); +app.use("/", router); + +// Server starten +server.listen(PORT, () => { + console.log(`🚀 Server läuft auf Port ${PORT} (${REDIS_ENABLED ? "Cluster" : "Standalone"})`); +}); + +// 🌐 Test: alle 10 Sekunden globale Userliste ausgeben +setInterval(async () => { + try { + const users = await getOnlineUSer(); + console.log("🌐 Globale Userliste:", users); + } catch (err) { + console.error("Fehler beim Abrufen der globalen Userliste:", err.message); + } +}, 10000); From 9ab2b0ba036f002f087fac1a6fff86a2bbafd58f Mon Sep 17 00:00:00 2001 From: Andreas <58698758+holzi1005@users.noreply.github.com> Date: Sun, 12 Oct 2025 15:24:22 +0200 Subject: [PATCH 16/18] Update package.json --- nodejs/package.json | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/nodejs/package.json b/nodejs/package.json index e7c2ae74b..9fb341d50 100644 --- a/nodejs/package.json +++ b/nodejs/package.json @@ -7,12 +7,14 @@ "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, - "author": "", - "license": "ISC", + "author": "H2 invent GmbH", + "license": "AGPLv3", "dependencies": { "body-parser": "^1.20.0", "express": "^4.18.1", "jsonwebtoken": "^8.5.1", - "socket.io": "^4.5.1" + "socket.io": "^4.5.1", + "redis": "^5.3.1", + "@socket.io/redis-adapter": "^8.2.0" } } From 1b8987c8318d2faea454b4138767be4a7e23a2f2 Mon Sep 17 00:00:00 2001 From: Andreas <58698758+holzi1005@users.noreply.github.com> Date: Sun, 12 Oct 2025 15:40:18 +0200 Subject: [PATCH 17/18] Update server.mjs --- nodejs/server.mjs | 58 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 48 insertions(+), 10 deletions(-) diff --git a/nodejs/server.mjs b/nodejs/server.mjs index e68b8b56e..f95f58203 100644 --- a/nodejs/server.mjs +++ b/nodejs/server.mjs @@ -8,7 +8,7 @@ import { Server } from "socket.io"; import { checkFileContains } from "./checkCertAndKey.js"; import { websocketState } from "./websocketState.mjs"; -import { loginUser, getOnlineUSer } from "./login.mjs"; +import { loginUser, getOnlineUSer, getUserId } from "./login.mjs"; import { MERCURE_INTERNAL_URL, PORT, @@ -50,6 +50,8 @@ export const io = new Server(server, { } }); +let redis = null; + // Optional: Redis Adapter für Cluster if (REDIS_ENABLED) { try { @@ -62,6 +64,7 @@ if (REDIS_ENABLED) { await pubClient.connect(); await subClient.connect(); + redis = pubClient; // global für Heartbeat io.adapter(createAdapter(pubClient, subClient)); console.log(`🔗 Redis-Adapter aktiviert (${REDIS_HOST}:${REDIS_PORT})`); } catch (err) { @@ -128,12 +131,47 @@ server.listen(PORT, () => { console.log(`🚀 Server läuft auf Port ${PORT} (${REDIS_ENABLED ? "Cluster" : "Standalone"})`); }); -// 🌐 Test: alle 10 Sekunden globale Userliste ausgeben -setInterval(async () => { - try { - const users = await getOnlineUSer(); - console.log("🌐 Globale Userliste:", users); - } catch (err) { - console.error("Fehler beim Abrufen der globalen Userliste:", err.message); - } -}, 10000); +// 🔄 Heartbeat: lokale User alle 10 Sekunden erneut in Redis registrieren +if (REDIS_ENABLED && redis) { + setInterval(async () => { + try { + const usersFromRedis = {}; + + // Alle lokal verbundenen Sockets in Redis registrieren (Heartbeat) + for (const socket of io.sockets.sockets.values()) { + const userId = getUserId(socket); + if (!userId) continue; + + const userData = { + id: userId, + status: socket.decoded?.status === 1 ? 'online' : 'offline', + updatedAt: Date.now() + }; + + await redis.hset("users", userId, JSON.stringify(userData)); + + // Für Testausgabe vorbereiten + const status = userData.status; + if (!usersFromRedis[status]) usersFromRedis[status] = []; + usersFromRedis[status].push(userId); + } + + // 🌐 Test: globale Userliste ausgeben + console.log("🌐 Globale Userliste (Redis + Heartbeat):", usersFromRedis); + + } catch (err) { + console.error("Fehler beim Heartbeat / globale Userliste:", err.message); + } + }, 10000); // alle 10 Sekunden +} else { + // Standalone-Modus: nur lokale Userliste ausgeben + setInterval(async () => { + try { + const users = await getOnlineUSer(); + console.log("🌐 Globale Userliste (Standalone):", users); + } catch (err) { + console.error("Fehler beim Abrufen der Userliste:", err.message); + } + }, 10000); +} + From cbfc9b3695da24839574cc3ff2f176c640297647 Mon Sep 17 00:00:00 2001 From: Andreas <58698758+holzi1005@users.noreply.github.com> Date: Sun, 12 Oct 2025 15:57:03 +0200 Subject: [PATCH 18/18] Update server.mjs --- nodejs/server.mjs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/nodejs/server.mjs b/nodejs/server.mjs index f95f58203..c307b9b31 100644 --- a/nodejs/server.mjs +++ b/nodejs/server.mjs @@ -163,15 +163,5 @@ if (REDIS_ENABLED && redis) { console.error("Fehler beim Heartbeat / globale Userliste:", err.message); } }, 10000); // alle 10 Sekunden -} else { - // Standalone-Modus: nur lokale Userliste ausgeben - setInterval(async () => { - try { - const users = await getOnlineUSer(); - console.log("🌐 Globale Userliste (Standalone):", users); - } catch (err) { - console.error("Fehler beim Abrufen der Userliste:", err.message); - } - }, 10000); }