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 (28778B)


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