myheats

Live heats, scoring and leaderboard for sport events
git clone https://git.in0rdr.ch/myheats.git
Log | Files | Refs | Pull requests | README | LICENSE

commit 502b52933722f499dc2286b1c0ee34e73ce5c11d
parent a8dd2af60f4bd1e12858d6d5d1e817548583b372
Author: Andreas Gruhler <andreas.gruhler@adfinis.com>
Date:   Fri, 20 Sep 2024 16:00:11 +0200

feat: add simple websocket listener

Diffstat:
Mpackage-lock.json | 11+++++------
Mpackage.json | 3++-
Msrc/api/server.cjs | 99+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
3 files changed, 97 insertions(+), 16 deletions(-)

diff --git a/package-lock.json b/package-lock.json @@ -17,7 +17,8 @@ "react-cookie": "^7.2.0", "react-dom": "^18.3.1", "react-router-dom": "^6.25.1", - "react-select": "^5.8.0" + "react-select": "^5.8.0", + "ws": "^8.18.0" }, "devDependencies": { "@types/react": "^18.3.3", @@ -4607,11 +4608,10 @@ } }, "node_modules/vite": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz", - "integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==", + "version": "4.5.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.5.tgz", + "integrity": "sha512-ifW3Lb2sMdX+WU91s3R0FyQlAyLxOzCSCP37ujw0+r5POeHPwe6udWVIElKQq8gk3t7b8rkmvqC6IHBpCff4GQ==", "dev": true, - "license": "MIT", "dependencies": { "esbuild": "^0.18.10", "postcss": "^8.4.27", @@ -4796,7 +4796,6 @@ "version": "8.18.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", - "license": "MIT", "engines": { "node": ">=10.0.0" }, diff --git a/package.json b/package.json @@ -20,7 +20,8 @@ "react-cookie": "^7.2.0", "react-dom": "^18.3.1", "react-router-dom": "^6.25.1", - "react-select": "^5.8.0" + "react-select": "^5.8.0", + "ws": "^8.18.0" }, "devDependencies": { "@types/react": "^18.3.3", diff --git a/src/api/server.cjs b/src/api/server.cjs @@ -1,3 +1,4 @@ +const ws = require('ws'); const http = require('node:http'); const url = require('url'); const jwt = require('jsonwebtoken'); @@ -38,7 +39,7 @@ if (jwt_secret === undefined) { process.exit() } -// configure mail transport +// Configure mail transport // https://nodemailer.com/smtp const transport = nodemailer.createTransport({ host: process.env.SMTP_HOST, @@ -50,6 +51,14 @@ const transport = nodemailer.createTransport({ }, }); +// Define API paths and allowed request methods +const paths = [ + '/v1/healthz', + '/v1/auth/verify', + '/v1/echo', + '/v1/auth/requestMagicLink', +] + console.log("Backend API:", api_uri); console.log("Backend API port:", api_port); console.log("Redirect URI:", redirect_uri); @@ -60,14 +69,17 @@ console.log("SMTP starttls:", process.env.SMTP_STARTTLS); console.log("* * * * * * *", "Uncle roger ready 🍚", "* * * * * * *"); // Create a simple http API server -// https://nodejs.org/en/learn/modules/anatomy-of-an-http-transaction -const server = http.createServer(async (req, res) => { +// - https://nodejs.org/en/learn/modules/anatomy-of-an-http-transaction +// - https://nodejs.org/api/http.html#httpcreateserveroptions-requestlistener +const server = http.createServer(); + +// Listen on request events +server.on('request', async (req, res) => { const url = new URL(`${api_uri}:${api_port}${req.url}`); const search_params = url.searchParams; console.log('> Path', url.pathname); console.log(' Method', req.method); - console.log(' Status code', req.client._httpMessage.statusCode); // set headers res.setHeader('Content-Type', 'application/json'); @@ -118,6 +130,14 @@ const server = http.createServer(async (req, res) => { } catch (error) { serverError(res, error); } + } else { + const pathExists = paths.find((i) => i === url.pathname); + if (pathExists) { + // wrong method for this path + notAllowed(res, req.method); + } else { + notFound(res, url.pathname); + } } // cors pre-flight request uses options method } else if (req.method === 'OPTIONS') { @@ -183,18 +203,33 @@ const server = http.createServer(async (req, res) => { } }) } else { - notFound(res); + const pathExists = paths.find((i) => i === url.pathname); + if (pathExists) { + // wrong method for this path + notAllowed(res, req.method); + } else { + notFound(res, url.pathname); + } } } else { - notFound(res); + notAllowed(res, req.method); } }); -function notFound(res) { +function notFound(res, path) { + console.log('x Error: 404 not found'); res.statusCode = 404; res.end(JSON.stringify({ message: 'no tea cup 🍵 use fingaa 👇 haaiiyaa!', - error: '404 not found', + error: `404 path ${path} not found`, + })); +} +function notAllowed(res, method) { + console.log('x Error: 403 method', method, 'not allowed'); + res.statusCode = 403; + res.end(JSON.stringify({ + message: 'why english tea 🫖 cup to measure rice? Use fingaa 👇 haaiiyaa!', + error: `403 method ${method} not allowed`, })); } function serverError(res, err) { @@ -270,5 +305,51 @@ async function getUser(email) { } } -// listen and serve +// Create websocket listeners +// https://github.com/websockets/ws?tab=readme-ov-file#multiple-servers-sharing-a-single-https-server +const wss1 = new ws.WebSocketServer({ noServer: true}); + +// Keep track of connected websocket clients +// https://javascript.info/websocket +const clients = new Set(); + +// Listen for websocket connections +wss1.on('connection', function connection(sock) { + sock.on('message', function message(data) { + console.log(' Uncle roger hears: %s', data); + }); + + sock.on('error', console.error); + + // Client closes the connection + sock.on('close', function(event) { + console.log(" Close event:", event.code); + console.log(" Close reason:", event.reason); + clients.delete(ws); + }); + + clients.add(sock); + console.log(`~ Received a new websocket connection`); + console.log(` ${sock} connected`); + + sock.send('🎵 Streaming leaderboard live data..'); + sock.close(1000, '< Websocket rice party done 👋 uncle roger disowns niece/nephew'); +}); + +// Listen to upgrade event +server.on('upgrade', function upgrade(req, sock, head) { + const url = new URL(`${api_uri}:${api_port}${req.url}`); + + console.log('> WS path', url.pathname); + + if (url.pathname === '/v1/leaderboard') { + wss1.handleUpgrade(req, sock, head, function done(sock) { + wss1.emit('connection', sock, req); + }); + } else { + sock.destroy(); + } +}); + +// Listen and serve server.listen(api_port);