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);