myheats

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

server.cjs (33494B)


      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',                                   //    not    authenticated
     57     '/v1/auth/verify',                               //    not    authenticated
     58     '/v1/auth/requestMagicLink',                     //    not    authenticated
     59     '/v1/auth/invalidateToken',                      //    not    implemented
     60     '/v1/leaderboard/allHeats',                      // πŸ”“ partly authenticated
     61     '/v1/leaderboard/allJudges',                     // πŸ” fully  authenticated
     62     '/v1/leaderboard/allAthletes',                   // πŸ” fully  authenticated
     63     '/v1/leaderboard/newHeat',                       // πŸ” fully  authenticated
     64     '/v1/leaderboard/getHeat',                       // πŸ” fully  authenticated
     65     '/v1/leaderboard/removeHeat',                    // πŸ” fully  authenticated
     66     '/v1/leaderboard/toggleHeatVisibility',          // πŸ” fully  authenticated
     67     '/v1/leaderboard/distinctStartlist',             // πŸ”“ partly authenticated
     68     '/v1/leaderboard/startlistWithAthletes',         // πŸ” fully  authenticated
     69     '/v1/leaderboard/scoresForHeatAndAthlete',       // πŸ”“ partly authenticated
     70     '/v1/leaderboard/scoreSummaryForHeatAndAthlete', // πŸ”“ partly authenticated
     71     '/v1/leaderboard/getScore',                      // πŸ” fully  authenticated
     72     '/v1/leaderboard/setScore',                      // πŸ” fully  authenticated
     73     '/v1/leaderboard/addAthleteToHeat',              // πŸ” fully  authenticated
     74     '/v1/leaderboard/removeAthleteFromHeat',         // πŸ” fully  authenticated
     75     '/v1/leaderboard/addAthlete',                    // πŸ” fully  authenticated
     76     '/v1/leaderboard/removeAthlete',                 // πŸ” fully  authenticated
     77     '/v1/leaderboard/addJudge',                      // πŸ” fully  authenticated
     78     '/v1/leaderboard/removeJudge',                   // πŸ” fully  authenticated
     79     '/v1/leaderboard/allSettings',                   //    not    authenticated
     80     '/v1/leaderboard/getSetting',                    // πŸ” fully  authenticated
     81     '/v1/leaderboard/updateSetting',                 // πŸ” fully  authenticated
     82     '/v1/leaderboard/removeSetting',                 // πŸ” fully  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/http/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           const user = await verifyToken(req, token)
    143 
    144           if (user !== false) {
    145             res.end(JSON.stringify({
    146               message: 'Uncle roger verified 🀝 fuuiiyooh!',
    147               data: user,
    148             }));
    149           }
    150         } catch (error) {
    151           serverError(res, error);
    152         }
    153         break
    154       case '/v1/leaderboard/allHeats':
    155         try {
    156           const user = await verifyToken(req, token)
    157           // return public heats only for unauthenticated users
    158           const heats = await db.allHeats(user === false ? true : false)
    159 
    160           res.end(JSON.stringify({
    161             message: 'All heats',
    162             data: heats,
    163           }));
    164         } catch(error) {
    165           serverError(res, error);
    166         }
    167         break
    168       case '/v1/leaderboard/allJudges':
    169         try {
    170           const user = await verifyToken(req, token)
    171           const judges = await db.allJudges()
    172 
    173           if (user !== false) {
    174             res.end(JSON.stringify({
    175               message: 'All judges',
    176               data: judges,
    177             }));
    178           } else {
    179             notAuthorized(res)
    180           }
    181         } catch(error) {
    182           serverError(res, error);
    183         }
    184         break
    185       case '/v1/leaderboard/allAthletes':
    186         try {
    187           const user = await verifyToken(req, token)
    188           const athletes = await db.allAthletes()
    189 
    190           if (user !== false) {
    191             res.end(JSON.stringify({
    192               message: 'All athletes',
    193               data: athletes,
    194             }));
    195           } else {
    196             notAuthorized(res)
    197           }
    198         } catch(error) {
    199           serverError(res, error);
    200         }
    201         break
    202       case '/v1/leaderboard/getSetting':
    203         try {
    204           const user = await verifyToken(req, token)
    205 
    206           let name = search_params.get('name');
    207           const setting = await db.getSetting(name)
    208 
    209           if (setting.length < 1) {
    210             noContent(res);
    211             return
    212           }
    213 
    214           if (user !== false) {
    215             res.end(JSON.stringify({
    216               message: `Setting with name ${name}`,
    217               data: setting[0].value,
    218             }));
    219           } else {
    220             notAuthorized(res)
    221           }
    222         } catch(error) {
    223           serverError(res, error);
    224         }
    225         break
    226       case '/v1/leaderboard/allSettings':
    227         try {
    228           const settings = await db.allSettings()
    229 
    230           if (settings.length < 1) {
    231             noContent(res);
    232             return
    233           }
    234           res.end(JSON.stringify({
    235             message: 'All settings',
    236             data: settings,
    237           }));
    238         } catch(error) {
    239           serverError(res, error);
    240         }
    241         break
    242       default:
    243         const pathExists = paths.find((i) => i === url.pathname);
    244         if (pathExists) {
    245           // wrong method for this path
    246           notAllowed(res, req.method);
    247         } else {
    248           notFound(res, url.pathname);
    249         }
    250     } // end switch
    251   } else if (req.method === 'POST') {
    252     let body = [];
    253     let input = "";
    254 
    255     switch(url.pathname) {
    256       case '/v1/auth/requestMagicLink':
    257         req.on('data', chunk => {
    258           body.push(chunk);
    259         }).on('end', async () => {
    260           const b = Buffer.concat(body);
    261           try {
    262             if (b.length < 1) {
    263               throw new Error("Empty request body")
    264             }
    265             input = JSON.parse(b);
    266             console.log('  Link request for:', input.email);
    267 
    268             const users = await db.getUser(input.email);
    269             if (users.length < 1) {
    270               throw new Error("User not found")
    271             }
    272 
    273             const user = users[0]
    274 
    275             // create magic link
    276             const exp = new Date(Date.now() + 1000 * jwt_ttl)
    277             const token = jwt.sign({ email: user.email }, jwt_secret, {
    278               expiresIn: 1000 * jwt_ttl,
    279             })
    280             const magic_link = `${redirect_uri}?token=${token}`
    281             const magic_href = `${api_uri}:${api_port}/v1/auth/verify?token=${token}`
    282             console.log("πŸ”—Magic href:", magic_href)
    283 
    284             // save token with expiration time
    285             await db.saveToken(user.id, token, exp)
    286 
    287             // send magic link to user
    288             await transport.sendMail({
    289               from: process.env.SMTP_FROM,
    290               to: user.email,
    291               subject: 'MyHeats Magic Link',
    292               //text: `Click here ✨ to log in: ${magic_link}`,
    293               html: `Click <a href="${magic_link}">here</a> ✨ to log in.`,
    294             })
    295 
    296             res.end(JSON.stringify({
    297               message: 'Magic link requested',
    298             }));
    299           } catch (error) {
    300             serverError(res, error);
    301           }
    302         })
    303         break
    304       case '/v1/leaderboard/distinctStartlist':
    305         req.on('data', chunk => {
    306           body.push(chunk);
    307         }).on('end', async () => {
    308           const b = Buffer.concat(body);
    309           try {
    310             if (b.length < 1) {
    311               throw new Error("Empty request body")
    312             }
    313             input = JSON.parse(b);
    314             console.log('  distinctStartlist request with heatIds:', input.heat_ids);
    315 
    316             const user = await verifyToken(req, token)
    317             // return public heats only for unauthenticated users
    318             const startlist = await db.distinctStartlist(input.heat_ids, user === false ? true : false);
    319 
    320             if (startlist.length < 1) {
    321               noContent(res);
    322               return
    323             }
    324             res.end(JSON.stringify({
    325               message: 'Distinct athletes for multiple heats',
    326               data: startlist,
    327             }));
    328           } catch(error) {
    329             serverError(res, error);
    330           }
    331         })
    332         break
    333       case '/v1/leaderboard/startlistWithAthletes':
    334         req.on('data', chunk => {
    335           body.push(chunk);
    336         }).on('end', async () => {
    337           const b = Buffer.concat(body);
    338           try {
    339             const user = await verifyToken(req, token)
    340             if (user !== false) {
    341               if (b.length < 1) {
    342                 throw new Error("Empty request body")
    343               }
    344               input = JSON.parse(b);
    345               console.log('  startlistWithAthletes request with headId:', input.heat_id);
    346 
    347               const startlist = await db.startlistWithAthletes(input.heat_id);
    348 
    349               if (startlist.length < 1) {
    350                 throw new Error("No athletes for this startlist")
    351               }
    352               res.end(JSON.stringify({
    353                 message: 'Startlist with athletes for heat',
    354                 data: startlist,
    355               }));
    356             } else {
    357               notAuthorized(res)
    358             }
    359           } catch(error) {
    360             serverError(res, error);
    361           }
    362         })
    363         break
    364       case '/v1/leaderboard/scoresForHeatAndAthlete':
    365         req.on('data', chunk => {
    366           body.push(chunk);
    367         }).on('end', async () => {
    368           const b = Buffer.concat(body);
    369           try {
    370             if (b.length < 1) {
    371               throw new Error("Empty request body")
    372             }
    373             input = JSON.parse(b);
    374             console.log('  scoresForHeatAndAthlete request with heat and athlete:',
    375                     input.heat, input.athlete);
    376 
    377             const user = await verifyToken(req, token)
    378             // for unauthenticated users only return public heat scores
    379             const scores = await db.scoresForHeatAndAthlete(
    380                     input.heat,
    381                     input.athlete,
    382                     user === false ? true : false
    383             )
    384 
    385             if (scores.length < 1) {
    386               noContent(res);
    387               return
    388             }
    389             res.end(JSON.stringify({
    390               message: 'Scores for heat and athlete',
    391               data: scores,
    392             }));
    393           } catch(error) {
    394             serverError(res, error);
    395           }
    396         })
    397         break
    398       case '/v1/leaderboard/scoreSummaryForHeatAndAthlete':
    399         req.on('data', chunk => {
    400           body.push(chunk);
    401         }).on('end', async () => {
    402           const b = Buffer.concat(body);
    403           try {
    404             if (b.length < 1) {
    405               throw new Error("Empty request body")
    406             }
    407             input = JSON.parse(b);
    408             console.log('  scoreSummaryForHeatAndAthlete request with heat and athlete:',
    409                     input.heat, input.athlete);
    410 
    411             const user = await verifyToken(req, token)
    412             // for unauthenticated users only return public heat summary
    413             const summary = await db.scoreSummaryForHeatAndAthlete(
    414                     input.heat,
    415                     input.athlete,
    416                     user === false ? true : false
    417             )
    418 
    419             if (summary.length < 1) {
    420               noContent(res);
    421               return
    422             }
    423             res.end(JSON.stringify({
    424               message: 'Score summary for heat and athlete',
    425               data: summary[0],
    426             }));
    427           } catch(error) {
    428             serverError(res, error);
    429           }
    430         })
    431         break
    432       case '/v1/leaderboard/getScore':
    433         req.on('data', chunk => {
    434           body.push(chunk);
    435         }).on('end', async () => {
    436           const b = Buffer.concat(body);
    437           try {
    438             const user = await verifyToken(req, token)
    439 
    440             if (user !== false) {
    441               if (b.length < 1) {
    442                 throw new Error("Empty request body")
    443               }
    444               input = JSON.parse(b);
    445               console.log('  GetScore request for:', input);
    446 
    447               const scores = await db.getScore(
    448                       input.heat,
    449                       input.athlete,
    450                       input.judge
    451               );
    452 
    453               if (scores.length < 1) {
    454                 noContent(res);
    455                 return
    456               }
    457 
    458               res.end(JSON.stringify({
    459                 message: 'Requested score for heat, user and judge',
    460                 data: scores[0],
    461               }));
    462             } else {
    463               notAuthorized(res)
    464             }
    465           } catch (error) {
    466             serverError(res, error);
    467           }
    468         })
    469         break
    470       case '/v1/leaderboard/setScore':
    471         req.on('data', chunk => {
    472           body.push(chunk);
    473         }).on('end', async () => {
    474           const b = Buffer.concat(body);
    475           try {
    476             const user = await verifyToken(req, token)
    477 
    478             if (user !== false) {
    479               if (b.length < 1) {
    480                 throw new Error("Empty request body")
    481               }
    482               input = JSON.parse(b);
    483               console.log('  SetScore request for:', input);
    484 
    485               const scores = await db.setScore(
    486                       input.heat,
    487                       input.athlete,
    488                       input.judge,
    489                       input.score,
    490               );
    491 
    492               if (scores.length < 1) {
    493                 throw new Error("Score not updated")
    494               }
    495 
    496               res.end(JSON.stringify({
    497                 message: 'Score update for heat, user and judge',
    498                 data: scores[0],
    499               }));
    500             } else {
    501               notAuthorized(res)
    502             }
    503           } catch (error) {
    504             serverError(res, error);
    505           }
    506         })
    507         break
    508       case '/v1/leaderboard/newHeat':
    509         req.on('data', chunk => {
    510           body.push(chunk);
    511         }).on('end', async () => {
    512           const b = Buffer.concat(body);
    513           try {
    514             const user = await verifyToken(req, token)
    515 
    516             if (user !== false) {
    517               if (b.length < 1) {
    518                 throw new Error("Empty request body")
    519               }
    520               input = JSON.parse(b);
    521               console.log('  newHeat request for:', input);
    522 
    523               const heats = await db.newHeat(
    524                       input.name,
    525                       input.location,
    526                       input.planned_start,
    527                       input.private
    528               );
    529               if (heats.length < 1) {
    530                 throw new Error("Heat not created")
    531               }
    532 
    533               res.end(JSON.stringify({
    534                 message: 'New heat',
    535                 data: heats[0],
    536               }));
    537             } else {
    538               notAuthorized(res)
    539             }
    540           } catch (error) {
    541             serverError(res, error);
    542           }
    543         })
    544         break
    545       case '/v1/leaderboard/getHeat':
    546         req.on('data', chunk => {
    547           body.push(chunk);
    548         }).on('end', async () => {
    549           const b = Buffer.concat(body);
    550           try {
    551             const user = await verifyToken(req, token)
    552 
    553             if (user !== false) {
    554               if (b.length < 1) {
    555                 throw new Error("Empty request body")
    556               }
    557               input = JSON.parse(b);
    558               console.log('  getHeat request for:', input);
    559 
    560               const heats = await db.getHeat(
    561                       input.heat_id,
    562               );
    563               if (heats.length < 1) {
    564                 throw new Error("Heat not found")
    565               }
    566 
    567               res.end(JSON.stringify({
    568                 message: 'New heat',
    569                 data: heats[0],
    570               }));
    571             } else {
    572               notAuthorized(res)
    573             }
    574           } catch (error) {
    575             serverError(res, error);
    576           }
    577         })
    578         break
    579       case '/v1/leaderboard/removeHeat':
    580         req.on('data', chunk => {
    581           body.push(chunk);
    582         }).on('end', async () => {
    583           const b = Buffer.concat(body);
    584           try {
    585             const user = await verifyToken(req, token)
    586 
    587             if (user !== false) {
    588               if (b.length < 1) {
    589                 throw new Error("Empty request body")
    590               }
    591               input = JSON.parse(b);
    592               console.log('  removeHeat request for:', input);
    593 
    594               const heats = await db.removeHeat(input.heat_id)
    595               if (heats.length < 1) {
    596                 throw new Error("Heat not removed")
    597               }
    598               res.end(JSON.stringify({
    599                 message: 'Heat removed',
    600                 data: heats[0],
    601               }));
    602             } else {
    603               notAuthorized(res)
    604             }
    605           } catch (error) {
    606             serverError(res, error);
    607           }
    608         })
    609         break
    610       case '/v1/leaderboard/toggleHeatVisibility':
    611         req.on('data', chunk => {
    612           body.push(chunk);
    613         }).on('end', async () => {
    614           const b = Buffer.concat(body);
    615           try {
    616             const user = await verifyToken(req, token)
    617 
    618             if (user !== false) {
    619               if (b.length < 1) {
    620                 throw new Error("Empty request body")
    621               }
    622               input = JSON.parse(b);
    623               console.log('  toggleHeatVisibility request for:', input);
    624 
    625               const heats = await db.toggleHeatVisibility(input.heat_id)
    626               if (heats.length < 1) {
    627                 throw new Error("Heat visibility unchanged")
    628               }
    629               res.end(JSON.stringify({
    630                 message: 'Heat visibility toggled',
    631                 data: heats[0],
    632               }));
    633             } else {
    634               notAuthorized(res)
    635             }
    636           } catch (error) {
    637             serverError(res, error);
    638           }
    639         })
    640         break
    641       case '/v1/leaderboard/addAthleteToHeat':
    642         req.on('data', chunk => {
    643           body.push(chunk);
    644         }).on('end', async () => {
    645           const b = Buffer.concat(body);
    646           try {
    647             const user = await verifyToken(req, token)
    648 
    649             if (user !== false) {
    650               if (b.length < 1) {
    651                 throw new Error("Empty request body")
    652               }
    653               input = JSON.parse(b);
    654               console.log('  addAthleteToHeat request for:', input);
    655 
    656               const startlist = await db.addAthleteToHeat(
    657                       input.athlete,
    658                       input.heat,
    659               );
    660               if (startlist.length < 1) {
    661                 throw new Error("Startlist not updated")
    662               }
    663 
    664               res.end(JSON.stringify({
    665                 message: 'Athlete added to startlist',
    666                 data: startlist[0],
    667               }));
    668             } else {
    669               notAuthorized(res)
    670             }
    671           } catch (error) {
    672             serverError(res, error);
    673           }
    674         })
    675         break
    676       case '/v1/leaderboard/removeAthleteFromHeat':
    677         req.on('data', chunk => {
    678           body.push(chunk);
    679         }).on('end', async () => {
    680           const b = Buffer.concat(body);
    681           try {
    682             const user = await verifyToken(req, token)
    683 
    684             if (user !== false) {
    685               if (b.length < 1) {
    686                 throw new Error("Empty request body")
    687               }
    688               input = JSON.parse(b);
    689               console.log('  removeAthleteFromHeat request for:', input);
    690 
    691               const startlist = await db.removeAthleteFromHeat(input.startlist_id)
    692               if (startlist.length < 1) {
    693                 throw new Error("Startlist not updated")
    694               }
    695 
    696               res.end(JSON.stringify({
    697                 message: 'Athlete removed from startlist',
    698                 data: startlist[0],
    699               }));
    700             } else {
    701               notAuthorized(res)
    702             }
    703           } catch (error) {
    704             serverError(res, error);
    705           }
    706         })
    707         break
    708       case '/v1/leaderboard/addAthlete':
    709         req.on('data', chunk => {
    710           body.push(chunk);
    711         }).on('end', async () => {
    712           const b = Buffer.concat(body);
    713           try {
    714             const user = await verifyToken(req, token)
    715 
    716             if (user !== false) {
    717               if (b.length < 1) {
    718                 throw new Error("Empty request body")
    719               }
    720               input = JSON.parse(b);
    721               console.log('  addAthlete request for:', input);
    722 
    723               const athlete = await db.addAthlete(
    724                       input.nr,
    725                       input.firstname,
    726                       input.lastname,
    727                       input.birthday,
    728                       input.school,
    729               );
    730               if (athlete.length < 1) {
    731                 throw new Error("Athlete not removed")
    732               }
    733 
    734               res.end(JSON.stringify({
    735                 message: 'Athlete created',
    736                 data: athlete[0]
    737               }));
    738             } else {
    739               notAuthorized(res)
    740             }
    741           } catch (error) {
    742             serverError(res, error);
    743           }
    744         })
    745         break
    746       case '/v1/leaderboard/removeAthlete':
    747         req.on('data', chunk => {
    748           body.push(chunk);
    749         }).on('end', async () => {
    750           const b = Buffer.concat(body);
    751           try {
    752             const user = await verifyToken(req, token)
    753 
    754             if (user !== false) {
    755               if (b.length < 1) {
    756                 throw new Error("Empty request body")
    757               }
    758               input = JSON.parse(b);
    759               console.log('  removeAthlete request for:', input);
    760 
    761               const athlete = await db.removeAthlete(input.athlete_id)
    762               if (athlete.length < 1) {
    763                 throw new Error("Athlete not removed")
    764               }
    765               res.end(JSON.stringify({
    766                 message: 'Athlete removed',
    767                 data: athlete[0],
    768               }));
    769             } else {
    770               notAuthorized(res)
    771             }
    772           } catch (error) {
    773             serverError(res, error);
    774           }
    775         })
    776         break
    777       case '/v1/leaderboard/addJudge':
    778         req.on('data', chunk => {
    779           body.push(chunk);
    780         }).on('end', async () => {
    781           const b = Buffer.concat(body);
    782           try {
    783             const user = await verifyToken(req, token)
    784 
    785             if (user !== false) {
    786               if (b.length < 1) {
    787                 throw new Error("Empty request body")
    788               }
    789               input = JSON.parse(b);
    790               console.log('  addJudge request for:', input);
    791 
    792               const judge = await db.addJudge(
    793                       input.email,
    794                       input.firstname,
    795                       input.lastname,
    796               );
    797               if (judge.length < 1) {
    798                 throw new Error("Judge not added")
    799               }
    800 
    801               res.end(JSON.stringify({
    802                 message: 'Judge created',
    803                 data: judge[0]
    804               }));
    805             } else {
    806               notAuthorized(res)
    807             }
    808           } catch (error) {
    809             serverError(res, error);
    810           }
    811         })
    812         break
    813       case '/v1/leaderboard/removeJudge':
    814         req.on('data', chunk => {
    815           body.push(chunk);
    816         }).on('end', async () => {
    817           const b = Buffer.concat(body);
    818           try {
    819             const user = await verifyToken(req, token)
    820 
    821             if (user !== false) {
    822               if (b.length < 1) {
    823                 throw new Error("Empty request body")
    824               }
    825               input = JSON.parse(b);
    826               console.log('  removeJudge request for:', input);
    827 
    828               const judge = await db.removeJudge(input.judge_id)
    829               if (judge.length < 1) {
    830                 throw new Error("Judge not removed")
    831               }
    832               res.end(JSON.stringify({
    833                 message: 'Judge removed',
    834                 data: judge[0],
    835               }));
    836             } else {
    837               notAuthorized(res)
    838             }
    839           } catch (error) {
    840             serverError(res, error);
    841           }
    842         })
    843         break
    844       case '/v1/leaderboard/updateSetting':
    845         req.on('data', chunk => {
    846           body.push(chunk);
    847         }).on('end', async () => {
    848           const b = Buffer.concat(body);
    849           try {
    850             const user = await verifyToken(req, token)
    851 
    852             if (user !== false) {
    853               if (b.length < 1) {
    854                 throw new Error("Empty request body")
    855               }
    856               input = JSON.parse(b);
    857               console.log('  updateSetting request for:', input);
    858 
    859               const setting = await db.updateSetting(input.name, input.value)
    860               if (setting.length < 1) {
    861                 throw new Error("Setting not updated")
    862               }
    863               res.end(JSON.stringify({
    864                 message: 'Setting updated',
    865                 data: setting[0],
    866               }));
    867             } else {
    868               notAuthorized(res)
    869             }
    870           } catch (error) {
    871             serverError(res, error);
    872           }
    873         })
    874         break
    875       case '/v1/leaderboard/removeSetting':
    876         req.on('data', chunk => {
    877           body.push(chunk);
    878         }).on('end', async () => {
    879           const b = Buffer.concat(body);
    880           try {
    881             const user = await verifyToken(req, token)
    882 
    883             if (user !== false) {
    884               if (b.length < 1) {
    885                 throw new Error("Empty request body")
    886               }
    887               input = JSON.parse(b);
    888               console.log('  removeSetting request for:', input);
    889 
    890               const settings = await db.removeSetting(input.name)
    891               if (settings.length < 1) {
    892                 throw new Error("Setting not removed")
    893               }
    894               res.end(JSON.stringify({
    895                 message: 'Setting removed',
    896                 data: settings[0],
    897               }));
    898             } else {
    899               notAuthorized(res)
    900             }
    901           } catch (error) {
    902             serverError(res, error);
    903           }
    904         })
    905         break
    906       default:
    907         const pathExists = paths.find((i) => i === url.pathname);
    908         if (pathExists) {
    909           // wrong method for this path
    910           notAllowed(res, req.method);
    911         } else {
    912           notFound(res, url.pathname);
    913         }
    914     } // end switch
    915   } else {
    916     notAllowed(res, req.method);
    917   }
    918 });
    919 
    920 // returns user or false for valid/invalid tokens
    921 async function verifyToken(req, token) {
    922   if (!token){
    923     console.warn("  No token found in verifyToken");
    924     return false
    925   } else if (token.length === 0) {
    926     console.warn("  Token empty in verifyToken");
    927     return false
    928   }
    929 
    930   // lookup token in the database
    931   const users = await db.lookupToken(token);
    932   if (users.length < 1) {
    933     console.warn("  User not found in verifyToken");
    934     return false
    935   }
    936 
    937   const user = users[0]
    938 
    939   console.warn("  Token expires_at:", users[0].expires_at);
    940   console.log("  Time now, lah:", new Date());
    941 
    942   // check expiration date of token in the database
    943   if (new Date(users[0].expires_at) < new Date()) {
    944     console.warn("Token expired.. tthink about yu saad live 😿");
    945     return false
    946   }
    947 
    948   // verify token signature
    949   const v = jwt.verify(token, jwt_secret);
    950   //await db.invalidateToken(token);
    951   console.log("  Verified token", v);
    952 
    953   if (v.email !== user.email) {
    954     console.warn("Token signature invalid");
    955     return false
    956   }
    957   return user
    958 }
    959 
    960 function noContent(res) {
    961   console.warn('x Warning: 204 no rice 😱');
    962   res.statusCode = 204;
    963   res.end();
    964 }
    965 function notFound(res, path) {
    966   console.error('x Error: 404 not found');
    967   res.statusCode = 404;
    968   res.end(JSON.stringify({
    969     message: 'no tea cup 🍡 use fingaa πŸ‘‡ haaiiyaa!',
    970     error: `404 path ${path} not found`,
    971   }));
    972 }
    973 function notAllowed(res, method) {
    974   console.error('x Error: 403 method', method, 'not allowed');
    975   res.statusCode = 403;
    976   res.end(JSON.stringify({
    977     message: 'why english tea πŸ«– cup to measure rice? Use fingaa πŸ‘‡ haaiiyaa!',
    978     error: `403 method ${method} not allowed`,
    979   }));
    980 }
    981 function notAuthorized(res) {
    982   console.error('x Error: 401 not authorized');
    983   res.statusCode = 401;
    984   res.end(JSON.stringify({
    985     message: 'what is bbc let me see πŸ€” ohh it\'s a british broadcasting corporation!',
    986     error: `401 not authorized`,
    987   }));
    988 }
    989 function serverError(res, err) {
    990   console.error('x Error:', err);
    991   res.statusCode = 500;
    992   res.end(JSON.stringify({
    993     message: 'no colander, haaiiyaa, rice 🍚 wet you fucked up!',
    994     error: err.message,
    995   }));
    996 }
    997 
    998 // Create websocket listeners
    999 // https://github.com/websockets/ws?tab=readme-ov-file#multiple-servers-sharing-a-single-https-server
   1000 const wss1 = new ws.WebSocketServer({ noServer: true});
   1001 
   1002 // Listen for websocket connections
   1003 wss1.on('connection', function connection(sock) {
   1004   sock.on('message', async function message(m) {
   1005     try {
   1006       msg = JSON.parse(m)
   1007       console.log('  Uncle roger hears:', msg);
   1008 
   1009       if (msg.method === 'watchScores') {
   1010         console.log(`~ Client requesting new scores`);
   1011         db.watchScores(sock)
   1012       }
   1013     } catch (error) {
   1014       console.error('x Error:', error.message);
   1015     }
   1016   });
   1017 
   1018   sock.on('error', console.error);
   1019 
   1020   // Client closes the connection
   1021   sock.on('close', function(event) {
   1022     console.log("  Close event:", event.code);
   1023     console.log("  Close reason:", event.reason);
   1024     db.removeClient(sock);
   1025   });
   1026 
   1027   console.log(`~ Received a new websocket connection`);
   1028   db.addClient(sock)
   1029 });
   1030 
   1031 // Listen to upgrade event
   1032 server.on('upgrade', function upgrade(req, sock, head) {
   1033   const url = new URL(`${req.protocol}://${req.host}${req.url}`);
   1034 
   1035   console.log('> WS path', url.pathname);
   1036 
   1037   if (url.pathname === '/v1/leaderboard') {
   1038     wss1.handleUpgrade(req, sock, head, function done(sock) {
   1039       wss1.emit('connection', sock, req);
   1040     });
   1041   } else {
   1042     sock.destroy();
   1043   }
   1044 });
   1045 
   1046 // Listen and serve
   1047 server.listen(api_port);