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