myheats

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

server.cjs (26472B)


      1 const ws = require('ws');
      2 const http = require('node:http');
      3 const url = require('url');
      4 const jwt = require('jsonwebtoken');
      5 const nodemailer = require('nodemailer');
      6 const db = require('./db.cjs');
      7 
      8 require('dotenv').config({
      9   // Configure common config files and modes
     10   // - https://www.npmjs.com/package/dotenv
     11   // - https://vitejs.dev/guide/env-and-mode
     12   path: [
     13     '.env.local',
     14     '.env.development', '.env.development.local',
     15     '.env.test',        '.env.test.local',
     16     '.env.production',  '.env.production.local',
     17     '.env'
     18   ]
     19 });
     20 
     21 // Read environment and set defaults
     22 api_uri = process.env.VITE_API_URI;
     23 api_port = process.env.VITE_API_PORT;
     24 redirect_uri = process.env.API_REDIRECT_URI;
     25 cors_allow_origin = process.env.API_CORS_ALLOW_ORIGIN;
     26 jwt_secret = process.env.API_JWT_SECRET;
     27 jwt_ttl = process.env.API_JWT_TTL;
     28 
     29 if (jwt_secret === undefined) {
     30   console.log('Balls, API_JWT_SECRET undefined 🍳');
     31   process.exit()
     32 } else if (process.env.SMTP_HOST === undefined ||
     33            process.env.SMTP_PORT === undefined ||
     34            process.env.SMTP_STARTTLS === undefined ||
     35            process.env.SMTP_USER === undefined ||
     36            process.env.SMTP_FROM === undefined ||
     37            process.env.SMTP_PASSWORD === undefined) {
     38   console.log('Balls, Not all SMTP settings defined 🍳');
     39   process.exit()
     40 }
     41 
     42 // Configure mail transport
     43 // https://nodemailer.com/smtp
     44 const transport = nodemailer.createTransport({
     45   host: process.env.SMTP_HOST,
     46   port: process.env.SMTP_PORT,
     47   secure: process.env.SMTP_STARTTLS,
     48   auth: {
     49     user: process.env.SMTP_USER,
     50     pass: process.env.SMTP_PASSWORD,
     51   },
     52 });
     53 
     54 // Define API paths and allowed request methods
     55 const paths = [
     56     '/v1/healthz',
     57     '/v1/auth/verify',
     58     '/v1/echo',
     59     '/v1/auth/requestMagicLink',
     60     '/v1/auth/invalidateToken',                      // not implemented
     61     '/v1/leaderboard/allHeats',
     62     '/v1/leaderboard/allAthletes',                   // πŸ”’ authenticated
     63     '/v1/leaderboard/newHeat',                       // πŸ”’ authenticated
     64     '/v1/leaderboard/getHeat',                       // πŸ”’ authenticated
     65     '/v1/leaderboard/removeHeat',                    // πŸ”’ authenticated
     66     '/v1/leaderboard/distinctStartlist',
     67     '/v1/leaderboard/startlistWithAthletes',         // πŸ”’ authenticated
     68     '/v1/leaderboard/scoresForHeatAndAthlete',
     69     '/v1/leaderboard/scoreSummaryForHeatAndAthlete',
     70     '/v1/leaderboard/getScore',                      // πŸ”’ authenticated
     71     '/v1/leaderboard/setScore',                      // πŸ”’ authenticated
     72     '/v1/leaderboard/addAthleteToHeat',              // πŸ”’ authenticated
     73     '/v1/leaderboard/removeAthleteFromHeat',         // πŸ”’ authenticated
     74     '/v1/leaderboard/addAthlete',                    // πŸ”’ authenticated
     75     '/v1/leaderboard/removeAthlete',                 // πŸ”’ authenticated
     76     '/v1/leaderboard/allSettings',
     77     '/v1/leaderboard/getSetting',                    // πŸ”’ authenticated
     78     '/v1/leaderboard/updateSetting',                 // πŸ”’ authenticated
     79     '/v1/leaderboard/removeSetting',                 // πŸ”’ authenticated
     80 ]
     81 
     82 console.log("Backend API:", api_uri);
     83 console.log("Backend API port:", api_port);
     84 console.log("Redirect URI:", redirect_uri);
     85 console.log("CORS Allow Origin:", cors_allow_origin);
     86 console.log("SMTP server:", process.env.SMTP_HOST);
     87 console.log("SMTP port:", process.env.SMTP_PORT);
     88 console.log("SMTP starttls:", process.env.SMTP_STARTTLS);
     89 console.log("* * * * * * *", "Uncle roger ready 🍚", "* * * * * * *");
     90 
     91 // Create a simple http API server
     92 // - https://nodejs.org/en/learn/modules/anatomy-of-an-http-transaction
     93 // - https://nodejs.org/api/http.html#httpcreateserveroptions-requestlistener
     94 const server = http.createServer();
     95 
     96 // Listen on request events
     97 server.on('request', async (req, res) => {
     98   const url = new URL(`${api_uri}:${api_port}${req.url}`);
     99   const search_params = url.searchParams;
    100 
    101   console.log('> Path', url.pathname);
    102   console.log('  Method', req.method);
    103 
    104   // set headers
    105   res.setHeader('Content-Type', 'application/json');
    106   res.setHeader('Access-Control-Allow-Origin', cors_allow_origin);
    107 
    108   // cors pre-flight request uses options method
    109   res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
    110   res.setHeader('Access-Control-Allow-Headers', 'Content-Type,Authorization');
    111 
    112   // extact authorization token
    113   const authHeader = req.headers.authorization
    114   let token = undefined
    115   try {
    116     if (authHeader.startsWith("Bearer ")) {
    117       token = authHeader.substring(7, authHeader.length)
    118       console.log('  Bearer token', token);
    119     }
    120   } catch(error) {
    121     console.warn("x Warning: No authorization bearer token in request header")
    122     //serverError(res, new Error("No authorization bearer token in request header"));
    123     //return
    124   }
    125 
    126   // cors pre-flight request uses options method
    127   if (req.method === 'OPTIONS') {
    128     res.end()
    129   } else if (req.method === 'GET') {
    130     switch(url.pathname) {
    131       case '/v1/healthz':
    132         res.end(JSON.stringify({
    133           message: 'egg fried rice 🍚 fuiyooh!',
    134         }));
    135         break
    136       case '/v1/auth/verify':
    137         try {
    138           token = search_params.get('token');
    139           let user = await verifyToken(req, token)
    140           res.end(JSON.stringify({
    141             message: 'Uncle roger verified 🀝 fuuiiyooh!',
    142             data: user,
    143           }));
    144         } catch (error) {
    145           serverError(res, error);
    146         }
    147         break
    148       case '/v1/leaderboard/allHeats':
    149         try {
    150           const heats = await db.allHeats()
    151 
    152           res.end(JSON.stringify({
    153             message: 'All heats',
    154             data: heats,
    155           }));
    156         } catch(error) {
    157           serverError(res, error);
    158         }
    159         break
    160       case '/v1/leaderboard/allAthletes':
    161         try {
    162           await verifyToken(req, token)
    163           const athletes = await db.allAthletes()
    164 
    165           res.end(JSON.stringify({
    166             message: 'All athletes',
    167             data: athletes,
    168           }));
    169         } catch(error) {
    170           serverError(res, error);
    171         }
    172         break
    173       case '/v1/leaderboard/getSetting':
    174         try {
    175           await verifyToken(req, token)
    176 
    177           let name = search_params.get('name');
    178           const setting = await db.getSetting(name)
    179 
    180           if (setting.length < 1) {
    181             noContent(res);
    182             return
    183           }
    184           res.end(JSON.stringify({
    185             message: `Setting with name ${name}`,
    186             data: setting[0].value,
    187           }));
    188         } catch(error) {
    189           serverError(res, error);
    190         }
    191         break
    192       case '/v1/leaderboard/allSettings':
    193         try {
    194           const settings = await db.allSettings()
    195 
    196           if (settings.length < 1) {
    197             noContent(res);
    198             return
    199           }
    200           res.end(JSON.stringify({
    201             message: 'All settings',
    202             data: settings,
    203           }));
    204         } catch(error) {
    205           serverError(res, error);
    206         }
    207         break
    208       default:
    209         const pathExists = paths.find((i) => i === url.pathname);
    210         if (pathExists) {
    211           // wrong method for this path
    212           notAllowed(res, req.method);
    213         } else {
    214           notFound(res, url.pathname);
    215         }
    216     } // end switch
    217   } else if (req.method === 'POST') {
    218     let body = [];
    219 
    220     switch(url.pathname) {
    221       case '/v1/echo':
    222         req.on('data', chunk => {
    223           body.push(chunk);
    224         }).on('end', () => {
    225           body = JSON.stringify({
    226             message: 'Uncle roger knows do egg fried rice since 3 years old, lah',
    227             data: Buffer.concat(body).toString(),
    228           });
    229           res.end(body);
    230         })
    231         break
    232       case '/v1/auth/requestMagicLink':
    233         req.on('data', chunk => {
    234           body.push(chunk);
    235         }).on('end', async () => {
    236           const b = Buffer.concat(body);
    237           try {
    238             if (b.length < 1) {
    239               throw new Error("Empty request body")
    240             }
    241             input = JSON.parse(b);
    242             console.log('  Link request for:', input.email);
    243 
    244             const users = await db.getUser(input.email);
    245             if (users.length < 1) {
    246               throw new Error("User not found")
    247             }
    248 
    249             const user = users[0]
    250 
    251             // create magic link
    252             const exp = new Date(Date.now() + 1000 * jwt_ttl)
    253             const token = jwt.sign({ email: user.email }, jwt_secret, {
    254               expiresIn: 1000 * jwt_ttl,
    255             })
    256             const magic_link = `${redirect_uri}?token=${token}`
    257             const magic_href = `${api_uri}:${api_port}/v1/auth/verify?token=${token}`
    258             console.log("πŸ”—Magic href:", magic_href)
    259 
    260             // save token with expiration time
    261             await db.saveToken(user.id, token, exp)
    262 
    263             // send magic link to user
    264             await transport.sendMail({
    265               from: process.env.SMTP_FROM,
    266               to: user.email,
    267               subject: 'MyHeats Magic Link',
    268               //text: `Click here ✨ to log in: ${magic_link}`,
    269               html: `Click <a href="${magic_link}">here</a> ✨ to log in.`,
    270             })
    271 
    272             res.end(JSON.stringify({
    273               message: 'Magic link requested',
    274             }));
    275           } catch (error) {
    276             serverError(res, error);
    277           }
    278         })
    279         break
    280       case '/v1/leaderboard/distinctStartlist':
    281         req.on('data', chunk => {
    282           body.push(chunk);
    283         }).on('end', async () => {
    284           const b = Buffer.concat(body);
    285           try {
    286             if (b.length < 1) {
    287               throw new Error("Empty request body")
    288             }
    289             input = JSON.parse(b);
    290             console.log('  distinctStartlist request with headIds:', input.heat_ids);
    291 
    292             const startlist = await db.distinctStartlist(input.heat_ids);
    293 
    294             if (startlist.length < 1) {
    295               noContent(res);
    296               return
    297             }
    298             res.end(JSON.stringify({
    299               message: 'Distinct athletes for multiple heats',
    300               data: startlist,
    301             }));
    302           } catch(error) {
    303             serverError(res, error);
    304           }
    305         })
    306         break
    307       case '/v1/leaderboard/startlistWithAthletes':
    308         req.on('data', chunk => {
    309           body.push(chunk);
    310         }).on('end', async () => {
    311           const b = Buffer.concat(body);
    312           try {
    313             await verifyToken(req, token)
    314             if (b.length < 1) {
    315               throw new Error("Empty request body")
    316             }
    317             input = JSON.parse(b);
    318             console.log('  startlistWithAthletes request with headId:', input.heat_id);
    319 
    320             const startlist = await db.startlistWithAthletes(input.heat_id);
    321 
    322             if (startlist.length < 1) {
    323               throw new Error("No athletes for this startlist")
    324             }
    325             res.end(JSON.stringify({
    326               message: 'Startlist with athletes for heat',
    327               data: startlist,
    328             }));
    329           } catch(error) {
    330             serverError(res, error);
    331           }
    332         })
    333         break
    334       case '/v1/leaderboard/scoresForHeatAndAthlete':
    335         req.on('data', chunk => {
    336           body.push(chunk);
    337         }).on('end', async () => {
    338           const b = Buffer.concat(body);
    339           try {
    340             if (b.length < 1) {
    341               throw new Error("Empty request body")
    342             }
    343             input = JSON.parse(b);
    344             console.log('  scoresForHeatAndAthlete request with heat and athlete:',
    345                     input.heat, input.athlete);
    346 
    347             const scores = await db.scoresForHeatAndAthlete(
    348                     input.heat,
    349                     input.athlete
    350             )
    351 
    352             if (scores.length < 1) {
    353               noContent(res);
    354               return
    355             }
    356             res.end(JSON.stringify({
    357               message: 'Scores for heat and athlete',
    358               data: scores,
    359             }));
    360           } catch(error) {
    361             serverError(res, error);
    362           }
    363         })
    364         break
    365       case '/v1/leaderboard/scoreSummaryForHeatAndAthlete':
    366         req.on('data', chunk => {
    367           body.push(chunk);
    368         }).on('end', async () => {
    369           const b = Buffer.concat(body);
    370           try {
    371             if (b.length < 1) {
    372               throw new Error("Empty request body")
    373             }
    374             input = JSON.parse(b);
    375             console.log('  scoreSummaryForHeatAndAthlete request with heat and athlete:',
    376                     input.heat, input.athlete);
    377 
    378             const summary = await db.scoreSummaryForHeatAndAthlete(
    379                     input.heat,
    380                     input.athlete
    381             )
    382 
    383             if (summary.length < 1) {
    384               noContent(res);
    385               return
    386             }
    387             res.end(JSON.stringify({
    388               message: 'Score summary for heat and athlete',
    389               data: summary[0],
    390             }));
    391           } catch(error) {
    392             serverError(res, error);
    393           }
    394         })
    395         break
    396       case '/v1/leaderboard/getScore':
    397         req.on('data', chunk => {
    398           body.push(chunk);
    399         }).on('end', async () => {
    400           const b = Buffer.concat(body);
    401           try {
    402             await verifyToken(req, token)
    403             if (b.length < 1) {
    404               throw new Error("Empty request body")
    405             }
    406             input = JSON.parse(b);
    407             console.log('  GetScore request for:', input);
    408 
    409             const scores = await db.getScore(
    410                     input.heat,
    411                     input.athlete,
    412                     input.judge
    413             );
    414 
    415             if (scores.length < 1) {
    416               noContent(res);
    417               return
    418             }
    419 
    420             res.end(JSON.stringify({
    421               message: 'Requested score for heat, user and judge',
    422               data: scores[0],
    423             }));
    424           } catch (error) {
    425             serverError(res, error);
    426           }
    427         })
    428         break
    429       case '/v1/leaderboard/setScore':
    430         req.on('data', chunk => {
    431           body.push(chunk);
    432         }).on('end', async () => {
    433           const b = Buffer.concat(body);
    434           try {
    435             await verifyToken(req, token)
    436             if (b.length < 1) {
    437               throw new Error("Empty request body")
    438             }
    439             input = JSON.parse(b);
    440             console.log('  SetScore request for:', input);
    441 
    442             const scores = await db.setScore(
    443                     input.heat,
    444                     input.athlete,
    445                     input.judge,
    446                     input.score,
    447             );
    448 
    449             if (scores.length < 1) {
    450               throw new Error("Score not updated")
    451             }
    452 
    453             res.end(JSON.stringify({
    454               message: 'Score update for heat, user and judge',
    455               data: scores[0],
    456             }));
    457           } catch (error) {
    458             serverError(res, error);
    459           }
    460         })
    461         break
    462       case '/v1/leaderboard/newHeat':
    463         req.on('data', chunk => {
    464           body.push(chunk);
    465         }).on('end', async () => {
    466           const b = Buffer.concat(body);
    467           try {
    468             await verifyToken(req, token)
    469             if (b.length < 1) {
    470               throw new Error("Empty request body")
    471             }
    472             input = JSON.parse(b);
    473             console.log('  newHeat request for:', input);
    474 
    475             const heats = await db.newHeat(
    476                     input.name,
    477                     input.location,
    478                     input.planned_start,
    479             );
    480             if (heats.length < 1) {
    481               throw new Error("Heat not created")
    482             }
    483 
    484             res.end(JSON.stringify({
    485               message: 'New heat',
    486               data: heats[0],
    487             }));
    488           } catch (error) {
    489             serverError(res, error);
    490           }
    491         })
    492         break
    493       case '/v1/leaderboard/getHeat':
    494         req.on('data', chunk => {
    495           body.push(chunk);
    496         }).on('end', async () => {
    497           const b = Buffer.concat(body);
    498           try {
    499             await verifyToken(req, token)
    500             if (b.length < 1) {
    501               throw new Error("Empty request body")
    502             }
    503             input = JSON.parse(b);
    504             console.log('  getHeat request for:', input);
    505 
    506             const heats = await db.getHeat(
    507                     input.heat_id,
    508             );
    509             if (heats.length < 1) {
    510               throw new Error("Heat not found")
    511             }
    512 
    513             res.end(JSON.stringify({
    514               message: 'New heat',
    515               data: heats[0],
    516             }));
    517           } catch (error) {
    518             serverError(res, error);
    519           }
    520         })
    521         break
    522       case '/v1/leaderboard/removeHeat':
    523         req.on('data', chunk => {
    524           body.push(chunk);
    525         }).on('end', async () => {
    526           const b = Buffer.concat(body);
    527           try {
    528             await verifyToken(req, token)
    529             if (b.length < 1) {
    530               throw new Error("Empty request body")
    531             }
    532             input = JSON.parse(b);
    533             console.log('  removeHeat request for:', input);
    534 
    535             const heats = await db.removeHeat(input.heat_id)
    536             if (heats.length < 1) {
    537               throw new Error("Heat not removed")
    538             }
    539             res.end(JSON.stringify({
    540               message: 'Heat removed',
    541               data: heats[0],
    542             }));
    543           } catch (error) {
    544             serverError(res, error);
    545           }
    546         })
    547         break
    548       case '/v1/leaderboard/addAthleteToHeat':
    549         req.on('data', chunk => {
    550           body.push(chunk);
    551         }).on('end', async () => {
    552           const b = Buffer.concat(body);
    553           try {
    554             await verifyToken(req, token)
    555             if (b.length < 1) {
    556               throw new Error("Empty request body")
    557             }
    558             input = JSON.parse(b);
    559             console.log('  addAthleteToHeat request for:', input);
    560 
    561             const startlist = await db.addAthleteToHeat(
    562                     input.athlete,
    563                     input.heat,
    564             );
    565             if (startlist.length < 1) {
    566               throw new Error("Startlist not updated")
    567             }
    568 
    569             res.end(JSON.stringify({
    570               message: 'Athlete added to startlist',
    571               data: startlist[0],
    572             }));
    573           } catch (error) {
    574             serverError(res, error);
    575           }
    576         })
    577         break
    578       case '/v1/leaderboard/removeAthleteFromHeat':
    579         req.on('data', chunk => {
    580           body.push(chunk);
    581         }).on('end', async () => {
    582           const b = Buffer.concat(body);
    583           try {
    584             await verifyToken(req, token)
    585             if (b.length < 1) {
    586               throw new Error("Empty request body")
    587             }
    588             input = JSON.parse(b);
    589             console.log('  removeAthleteFromHeat request for:', input);
    590 
    591             const startlist = await db.removeAthleteFromHeat(input.startlist_id)
    592             if (startlist.length < 1) {
    593               throw new Error("Startlist not updated")
    594             }
    595 
    596             res.end(JSON.stringify({
    597               message: 'Athlete removed from startlist',
    598               data: startlist[0],
    599             }));
    600           } catch (error) {
    601             serverError(res, error);
    602           }
    603         })
    604         break
    605       case '/v1/leaderboard/addAthlete':
    606         req.on('data', chunk => {
    607           body.push(chunk);
    608         }).on('end', async () => {
    609           const b = Buffer.concat(body);
    610           try {
    611             await verifyToken(req, token)
    612             if (b.length < 1) {
    613               throw new Error("Empty request body")
    614             }
    615             input = JSON.parse(b);
    616             console.log('  addAthlete request for:', input);
    617 
    618             const athlete = await db.addAthlete(
    619                     input.nr,
    620                     input.firstname,
    621                     input.lastname,
    622                     input.birthday,
    623                     input.school,
    624             );
    625             if (athlete.length < 1) {
    626               throw new Error("Startlist not updated")
    627             }
    628 
    629             res.end(JSON.stringify({
    630               message: 'Athlete created',
    631               data: athlete[0]
    632             }));
    633           } catch (error) {
    634             serverError(res, error);
    635           }
    636         })
    637         break
    638       case '/v1/leaderboard/removeAthlete':
    639         req.on('data', chunk => {
    640           body.push(chunk);
    641         }).on('end', async () => {
    642           const b = Buffer.concat(body);
    643           try {
    644             await verifyToken(req, token)
    645             if (b.length < 1) {
    646               throw new Error("Empty request body")
    647             }
    648             input = JSON.parse(b);
    649             console.log('  removeAthlete request for:', input);
    650 
    651             const athlete = await db.removeAthlete(input.athlete_id)
    652             if (athlete.length < 1) {
    653               throw new Error("Athlete not removed")
    654             }
    655             res.end(JSON.stringify({
    656               message: 'Athlete removed',
    657               data: athlete[0],
    658             }));
    659           } catch (error) {
    660             serverError(res, error);
    661           }
    662         })
    663         break
    664       case '/v1/leaderboard/updateSetting':
    665         req.on('data', chunk => {
    666           body.push(chunk);
    667         }).on('end', async () => {
    668           const b = Buffer.concat(body);
    669           try {
    670             await verifyToken(req, token)
    671             if (b.length < 1) {
    672               throw new Error("Empty request body")
    673             }
    674             input = JSON.parse(b);
    675             console.log('  updateSetting request for:', input);
    676 
    677             const setting = await db.updateSetting(input.name, input.value)
    678             if (setting.length < 1) {
    679               throw new Error("Setting not updated")
    680             }
    681             res.end(JSON.stringify({
    682               message: 'Setting updated',
    683               data: setting[0],
    684             }));
    685           } catch (error) {
    686             serverError(res, error);
    687           }
    688         })
    689         break
    690       case '/v1/leaderboard/removeSetting':
    691         req.on('data', chunk => {
    692           body.push(chunk);
    693         }).on('end', async () => {
    694           const b = Buffer.concat(body);
    695           try {
    696             await verifyToken(req, token)
    697             if (b.length < 1) {
    698               throw new Error("Empty request body")
    699             }
    700             input = JSON.parse(b);
    701             console.log('  removeSetting request for:', input);
    702 
    703             const settings = await db.removeSetting(input.name)
    704             if (settings.length < 1) {
    705               throw new Error("Setting not removed")
    706             }
    707             res.end(JSON.stringify({
    708               message: 'Setting removed',
    709               data: settings[0],
    710             }));
    711           } catch (error) {
    712             serverError(res, error);
    713           }
    714         })
    715         break
    716       default:
    717         const pathExists = paths.find((i) => i === url.pathname);
    718         if (pathExists) {
    719           // wrong method for this path
    720           notAllowed(res, req.method);
    721         } else {
    722           notFound(res, url.pathname);
    723         }
    724     } // end switch
    725   } else {
    726     notAllowed(res, req.method);
    727   }
    728 });
    729 
    730 // returns user on success, throws error otherwise
    731 async function verifyToken(req, token) {
    732   if (!token){
    733     throw {message: "No token found in verifyToken"}
    734   } else if (token.length === 0) {
    735     throw {message: "Token empty in verifyToken"}
    736   }
    737 
    738   // lookup token in the database
    739   const users = await db.lookupToken(token);
    740   if (users.length < 1) {
    741     throw {message: "User not found in verifyToken"}
    742   }
    743 
    744   const user = users[0]
    745 
    746   console.warn("  Token expires_at:", users[0].expires_at);
    747   console.log("  Time now, lah:", new Date());
    748 
    749   // check expiration date of token in the database
    750   if (new Date(users[0].expires_at) < new Date()) {
    751     throw new Error('Token expired.. tthink about yu saad live 😿')
    752   }
    753 
    754   // verify token signature
    755   const v = jwt.verify(token, jwt_secret);
    756   //await db.invalidateToken(token);
    757   console.log("  Verified token", v);
    758 
    759   if (v.email !== user.email) {
    760     throw new Error("Token signature invalid")
    761   }
    762   return user
    763 }
    764 
    765 function noContent(res) {
    766   console.warn('x Warning: 204 no rice 😱');
    767   res.statusCode = 204;
    768   res.end();
    769 }
    770 function notFound(res, path) {
    771   console.error('x Error: 404 not found');
    772   res.statusCode = 404;
    773   res.end(JSON.stringify({
    774     message: 'no tea cup 🍡 use fingaa πŸ‘‡ haaiiyaa!',
    775     error: `404 path ${path} not found`,
    776   }));
    777 }
    778 function notAllowed(res, method) {
    779   console.error('x Error: 403 method', method, 'not allowed');
    780   res.statusCode = 403;
    781   res.end(JSON.stringify({
    782     message: 'why english tea πŸ«– cup to measure rice? Use fingaa πŸ‘‡ haaiiyaa!',
    783     error: `403 method ${method} not allowed`,
    784   }));
    785 }
    786 function serverError(res, err) {
    787   console.error('x Error:', err);
    788   res.statusCode = 500;
    789   res.end(JSON.stringify({
    790     message: 'no colander, haaiiyaa, rice 🍚 wet you fucked up!',
    791     error: err.message,
    792   }));
    793 }
    794 
    795 // Create websocket listeners
    796 // https://github.com/websockets/ws?tab=readme-ov-file#multiple-servers-sharing-a-single-https-server
    797 const wss1 = new ws.WebSocketServer({ noServer: true});
    798 
    799 // Listen for websocket connections
    800 wss1.on('connection', function connection(sock) {
    801   sock.on('message', async function message(m) {
    802     try {
    803       msg = JSON.parse(m)
    804       console.log('  Uncle roger hears:', msg);
    805 
    806       if (msg.method === 'watchScores') {
    807         console.log(`~ Client requesting new scores`);
    808         db.watchScores(sock)
    809       }
    810     } catch (error) {
    811       console.error('x Error:', error.message);
    812     }
    813   });
    814 
    815   sock.on('error', console.error);
    816 
    817   // Client closes the connection
    818   sock.on('close', function(event) {
    819     console.log("  Close event:", event.code);
    820     console.log("  Close reason:", event.reason);
    821     db.removeClient(sock);
    822   });
    823 
    824   console.log(`~ Received a new websocket connection`);
    825   db.addClient(sock)
    826 });
    827 
    828 // Listen to upgrade event
    829 server.on('upgrade', function upgrade(req, sock, head) {
    830   const url = new URL(`${api_uri}:${api_port}${req.url}`);
    831 
    832   console.log('> WS path', url.pathname);
    833 
    834   if (url.pathname === '/v1/leaderboard') {
    835     wss1.handleUpgrade(req, sock, head, function done(sock) {
    836       wss1.emit('connection', sock, req);
    837     });
    838   } else {
    839     sock.destroy();
    840   }
    841 });
    842 
    843 // Listen and serve
    844 server.listen(api_port);