├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── LICENSE ├── README.md ├── api ├── add-question.js ├── check-answer.js ├── clear-cache.js ├── get-config.js ├── get-leaderboard.js ├── get-logs.js ├── get-player-details.js ├── get-player-rank.js ├── get-question.js ├── get-stats.js ├── update-config.js ├── update-player-level.js ├── update-player-type.js └── update-question.js ├── index.html ├── models ├── config.js ├── log.js ├── player.js └── question.js ├── mongo.js ├── package-lock.json ├── package.json ├── rollup.config.js ├── routers ├── api.js └── auth.js ├── src ├── cache.js ├── client.js ├── components │ ├── AdminForm.svelte │ ├── Chevron.svelte │ ├── Countdown.svelte │ ├── Navigation.svelte │ └── play │ │ ├── Footer.svelte │ │ ├── GameArea.svelte │ │ ├── Navigation.svelte │ │ ├── QuestionAnswer.svelte │ │ └── SidePanel.svelte ├── constants.js ├── routes │ ├── _error.svelte │ ├── _layout.svelte │ ├── admin.svelte │ ├── index.svelte │ ├── johnwickr.html │ ├── leaderboard.svelte │ ├── login.svelte │ ├── logs.svelte │ ├── register.svelte │ ├── stats.svelte │ ├── thatsonesmallstepformanonegiantleapformankind.html │ └── thetis.svelte ├── server.js ├── template.html └── utils.js ├── static ├── Illiad logo.png ├── loading.svg ├── logo.svg ├── meta │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── browserconfig.xml │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── manifest.json │ ├── mstile-150x150.png │ ├── safari-pinned-tab.svg │ ├── site-card-old.png │ └── site-card.png ├── paradoxf.png └── robots.txt ├── styles ├── admin-forms.scss ├── auth-forms.scss ├── common.scss ├── global.scss └── theme.scss └── svelte.config.js /.eslintignore: -------------------------------------------------------------------------------- 1 | __sapper__ -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2020: true, 5 | node: true, 6 | serviceworker: true, 7 | }, 8 | extends: ["standard"], 9 | parserOptions: { 10 | ecmaVersion: 11, 11 | sourceType: "module", 12 | }, 13 | plugins: ["svelte3"], 14 | overrides: [ 15 | { 16 | files: ["*.svelte"], 17 | processor: "svelte3/svelte3", 18 | rules: { 19 | "import/first": 0, 20 | "import/no-duplicates": 0, 21 | "import/no-mutable-exports": 0, 22 | "no-return-assign": 0, 23 | "no-multiple-empty-lines": [ 24 | "error", 25 | { 26 | max: 1, 27 | maxBOF: 2, 28 | maxEOF: 0, 29 | }, 30 | ], 31 | }, 32 | }, 33 | ], 34 | rules: {}, 35 | }; 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /node_modules/ 3 | /src/node_modules/@sapper/ 4 | yarn-error.log 5 | /__sapper__/ 6 | .env 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020, IEEE IIIT Delhi and authors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Slash 2 | 3 | > Platform for the Slash cryptic hunt 4 | 5 | ## License 6 | 7 | [MIT](LICENSE) 8 | -------------------------------------------------------------------------------- /api/add-question.js: -------------------------------------------------------------------------------- 1 | import * as constants from '../src/constants' 2 | import { log, hash } from '../src/utils' 3 | import Question from '../models/question' 4 | 5 | export default async (req, res) => { 6 | if (!req.user || !req.user.admin) { 7 | return res.json({ 8 | success: false, 9 | message: constants.ERR_NOAUTH 10 | }) 11 | } 12 | 13 | const { level, question, img, answer } = req.body 14 | const existingQuestion = await Question.findOne({ level }) 15 | 16 | if (existingQuestion) { 17 | return res.json({ 18 | success: false, 19 | message: constants.ERR_LEVEL_EXISTS 20 | }) 21 | } 22 | 23 | const hashedAnswer = hash(answer.replace(/[\s]+/g, '').toLowerCase()) 24 | await Question.create({ 25 | level, 26 | question, 27 | img, 28 | answer: hashedAnswer 29 | }) 30 | 31 | log('ADMIN', `[${req.user.username}] Question added`, `Level ${level}`) 32 | 33 | return res.json({ 34 | success: true, 35 | message: constants.GENERIC_SUCC 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /api/check-answer.js: -------------------------------------------------------------------------------- 1 | import * as constants from '../src/constants' 2 | import { clearCache } from '../src/cache' 3 | import { log, hash } from '../src/utils' 4 | import Question from '../models/question' 5 | import Player from '../models/player' 6 | import Config from '../models/config' 7 | 8 | export default async (req, res) => { 9 | if (!req.user) { 10 | return res.status(403).json({ 11 | success: false, 12 | message: constants.ERR_NOAUTH 13 | }) 14 | } 15 | 16 | // check whether game has ended 17 | const config = await Config.findOne() 18 | if (config.ended) { 19 | return res.json({ 20 | success: false, 21 | reload: true, 22 | message: constants.HUNT_END 23 | }) 24 | } 25 | 26 | const { username } = req.user 27 | const { answer } = req.body 28 | const invalid = !constants.ANSWER_REGEX.test(answer) 29 | 30 | const player = await Player.findOne({ username }) 31 | const { level } = player 32 | 33 | if (!player) { 34 | return res.json({ 35 | success: false, 36 | message: constants.ERR_NO_PLAYER 37 | }) 38 | } 39 | 40 | if (player.disqualified) { 41 | return res.json({ 42 | success: false, 43 | reload: true, 44 | message: constants.ERR_PLAYER_DQ 45 | }) 46 | } 47 | 48 | log('ANSWER', 'Attempt', `${username} @ L${level}: ${answer}`) 49 | 50 | if (invalid) { 51 | return res.json({ 52 | success: false, 53 | message: constants.ERR_INVALID_ANS 54 | }) 55 | } 56 | 57 | // // Exceptional level 58 | // if (level === 4) { 59 | // let playerName 60 | 61 | // const leaderboard = await Player.find( 62 | // { 63 | // admin: false, 64 | // phantom: false, 65 | // disqualified: false 66 | // }, 67 | // { 68 | // _id: 0, 69 | // username: 1, 70 | // level: 1 71 | // }, 72 | // { 73 | // sort: { 74 | // level: -1, 75 | // lastLevelOn: 1 76 | // } 77 | // } 78 | // ).cache({ key: 'leaderboard' }) 79 | 80 | // if (leaderboard.length < 24) { 81 | // playerName = 'nobody' 82 | // } else { 83 | // playerName = leaderboard[23].username.replace(/[\s]+/g, '').toLowerCase() 84 | // } 85 | // if (!(playerName === answer)) { 86 | // return res.json({ 87 | // success: false, 88 | // message: constants.ERR_WRONG_ANS 89 | // }) 90 | // } 91 | // } else { 92 | // // Regular level 93 | // const { answer: correctAnswer } = await Question.findOne({ level }) 94 | // const hashedAttempt = hash(answer.toLowerCase()) 95 | 96 | // if (hashedAttempt !== correctAnswer) { 97 | // return res.json({ 98 | // success: false, 99 | // message: constants.ERR_WRONG_ANS 100 | // }) 101 | // } 102 | // } 103 | // Regular level 104 | const { answer: correctAnswer } = await Question.findOne({ level }) 105 | const hashedAttempt = hash(answer.toLowerCase()) 106 | 107 | if (hashedAttempt !== correctAnswer) { 108 | return res.json({ 109 | success: false, 110 | message: constants.ERR_WRONG_ANS 111 | }) 112 | } 113 | 114 | // It's correct 115 | player.level = level + 1 116 | player.lastLevelOn = new Date() 117 | await player.save() 118 | 119 | log('ANSWER', 'Correct', `${username} @ L${level}: ${answer}`) 120 | clearCache('leaderboard') 121 | 122 | return res.json({ 123 | success: true, 124 | message: constants.CORRECT_ANS 125 | }) 126 | } 127 | -------------------------------------------------------------------------------- /api/clear-cache.js: -------------------------------------------------------------------------------- 1 | import * as constants from '../src/constants' 2 | import { clearCache } from '../src/cache' 3 | import { log } from '../src/utils' 4 | 5 | export default async (req, res) => { 6 | if (!req.user || !req.user.admin) { 7 | return res.json({ 8 | success: false, 9 | message: constants.ERR_NOAUTH 10 | }) 11 | } 12 | 13 | const { key } = req.body 14 | clearCache(key) 15 | 16 | log('ADMIN', `[${req.user.username}] Cache cleared`, key || 'FLUSH') 17 | 18 | return res.json({ 19 | success: true, 20 | message: constants.GENERIC_SUCC 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /api/get-config.js: -------------------------------------------------------------------------------- 1 | import * as constants from '../src/constants' 2 | import Config from '../models/config' 3 | 4 | // Config.create({ 5 | // started: false, 6 | // showLeaderboard: false, 7 | // ended: false, 8 | // startDate: new Date(2022, 5, 28) 9 | // }) 10 | 11 | export default async (req, res) => { 12 | const config = await Config.findOne({}, { _id: 0 }).cache({ 13 | key: 'config' 14 | }) 15 | 16 | return res.json({ 17 | success: true, 18 | message: constants.GENERIC_SUCC, 19 | data: { config } 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /api/get-leaderboard.js: -------------------------------------------------------------------------------- 1 | import * as constants from '../src/constants' 2 | import Player from '../models/player' 3 | import Config from '../models/config' 4 | 5 | export default async (req, res) => { 6 | const isAdmin = req.user && req.user.admin 7 | 8 | const config = await Config.findOne().cache({ key: 'config' }) 9 | if (!config.showLeaderboard && !isAdmin) { 10 | return res.json({ 11 | success: true, 12 | message: constants.GENERIC_SUCC, 13 | data: { leaderboard: [] } 14 | }) 15 | } 16 | 17 | let leaderboard = await Player.find( 18 | { 19 | admin: false, 20 | phantom: false, 21 | disqualified: false 22 | }, 23 | { 24 | _id: 0, 25 | username: 1, 26 | level: 1 27 | }, 28 | { 29 | sort: { 30 | level: -1, 31 | lastLevelOn: 1 32 | } 33 | } 34 | ).cache({ key: 'leaderboard' }) 35 | 36 | if (!config.ended && !isAdmin) { 37 | leaderboard = leaderboard.slice(0, constants.MAX_LEADERBOARD_PLAYERS) 38 | } 39 | 40 | return res.json({ 41 | success: true, 42 | message: constants.GENERIC_SUCC, 43 | data: { leaderboard } 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /api/get-logs.js: -------------------------------------------------------------------------------- 1 | import * as constants from "../src/constants"; 2 | import Log from "../models/log"; 3 | 4 | export default async (req, res) => { 5 | if (!req.user || !req.user.admin) { 6 | return res.json({ 7 | success: false, 8 | message: constants.ERR_NOAUTH, 9 | }); 10 | } 11 | 12 | const logs = await Log.find({}, { _id: 0, __v: 0 }) 13 | .sort({ time: -1 }) 14 | .limit(100); 15 | 16 | return res.json({ 17 | success: true, 18 | message: constants.GENERIC_SUCC, 19 | data: { logs }, 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /api/get-player-details.js: -------------------------------------------------------------------------------- 1 | import * as constants from "../src/constants"; 2 | import Player from "../models/player"; 3 | import Log from "../models/log"; 4 | 5 | export default async (req, res) => { 6 | if (!req.user || !req.user.admin) { 7 | return res.json({ 8 | success: false, 9 | message: constants.ERR_NOAUTH, 10 | }); 11 | } 12 | 13 | const { username } = req.body; 14 | const player = await Player.findOne({ username }).lean(); 15 | 16 | if (!player) { 17 | return res.json({ 18 | success: false, 19 | message: constants.ERR_NO_PLAYER, 20 | }); 21 | } 22 | 23 | const { time } = await Log.findOne({ key: "Registered", value: username }); 24 | player.registrationDate = time; 25 | 26 | return res.json({ 27 | success: true, 28 | message: constants.GENERIC_SUCC, 29 | data: { player }, 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /api/get-player-rank.js: -------------------------------------------------------------------------------- 1 | import * as constants from '../src/constants' 2 | import Player from '../models/player' 3 | 4 | export default async (req, res) => { 5 | if (!req.user) { 6 | return res.json({ 7 | success: false, 8 | message: constants.ERR_NOAUTH 9 | }) 10 | } 11 | const { username } = req.user 12 | 13 | const leaderboard = await Player.find( 14 | { 15 | admin: false, 16 | phantom: false, 17 | disqualified: false 18 | }, 19 | { 20 | _id: 0, 21 | username: 1, 22 | level: 1 23 | }, 24 | { 25 | sort: { 26 | level: -1, 27 | lastLevelOn: 1 28 | } 29 | } 30 | ).cache({ key: 'leaderboard' }) 31 | 32 | const rank = leaderboard.findIndex((p) => p.username === username) + 1 33 | 34 | if (rank === 0) { 35 | return res.json({ 36 | success: false, 37 | message: constants.ERR_NO_PLAYER, 38 | data: { rank: -1 } 39 | }) 40 | } 41 | 42 | return res.json({ 43 | success: true, 44 | message: constants.GENERIC_SUCC, 45 | data: { rank } 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /api/get-question.js: -------------------------------------------------------------------------------- 1 | import * as constants from "../src/constants"; 2 | import Question from "../models/question"; 3 | import { clearCache } from "../src/cache"; 4 | 5 | export default async (req, res) => { 6 | if (!req.user) { 7 | return res.json({ 8 | success: false, 9 | message: constants.ERR_NOAUTH, 10 | }); 11 | } 12 | 13 | const level = req.user.admin ? req.body.level : req.user.level; 14 | const question = await Question.findOne({ level }).cache({ 15 | key: `question_${level}`, 16 | }); 17 | 18 | if (!question) { 19 | clearCache(`question_${level}`); 20 | 21 | if (req.user.admin) { 22 | return res.json({ 23 | success: false, 24 | message: constants.ERR_NO_QUESTION, 25 | }); 26 | } 27 | 28 | return res.json({ 29 | success: true, 30 | message: constants.GENERIC_SUCC, 31 | data: { win: true }, 32 | }); 33 | } 34 | 35 | return res.json({ 36 | success: true, 37 | message: constants.GENERIC_SUCC, 38 | data: { 39 | win: false, 40 | question: req.user.admin ? question : question.question, 41 | img: req.user.admin ? question : question.img 42 | }, 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /api/get-stats.js: -------------------------------------------------------------------------------- 1 | import * as constants from "../src/constants"; 2 | import Player from "../models/player"; 3 | import Question from "../models/question"; 4 | import Log from "../models/log"; 5 | 6 | export default async (req, res) => { 7 | if (!req.user || !req.user.admin) { 8 | return res.json({ 9 | success: false, 10 | message: constants.ERR_NOAUTH, 11 | }); 12 | } 13 | 14 | const playerCount = await Player.countDocuments({ admin: false }); 15 | const adminCount = await Player.countDocuments({ admin: true }); 16 | const answerAttempts = await Log.countDocuments({ 17 | type: "ANSWER", 18 | key: "Attempt", 19 | }); 20 | const lastTenRegistrants = await Log.find( 21 | { type: "AUTH", key: "Registered" }, 22 | { _id: 0, value: 1, time: 1 } 23 | ) 24 | .sort("-time") 25 | .limit(10); 26 | 27 | const playersPerLevel = []; 28 | const numberOfLevels = await Question.countDocuments(); 29 | for (let level = 0; level < numberOfLevels; level++) { 30 | playersPerLevel.push(await Player.countDocuments({ level })); 31 | } 32 | 33 | const countries = await Player.find().distinct("geo.country"); 34 | const cities = await Player.find().distinct("geo.city"); 35 | 36 | return res.json({ 37 | success: true, 38 | message: constants.GENERIC_SUCC, 39 | data: { 40 | playerCount, 41 | adminCount, 42 | answerAttempts, 43 | lastTenRegistrants, 44 | playersPerLevel, 45 | geo: { countries, cities }, 46 | }, 47 | }); 48 | }; 49 | -------------------------------------------------------------------------------- /api/update-config.js: -------------------------------------------------------------------------------- 1 | import Config from "../models/config"; 2 | import { clearCache } from "../src/cache"; 3 | import { log } from "../src/utils"; 4 | 5 | export default async (req, res) => { 6 | if (!req.user || !req.user.admin) { 7 | return res.redirect("/"); 8 | } 9 | 10 | const config = await Config.findOne(); 11 | const { action } = req.body; 12 | 13 | switch (action) { 14 | case "begin": 15 | config.started = true; 16 | break; 17 | case "end": 18 | config.ended = true; 19 | break; 20 | case "show-leaderboard": 21 | config.showLeaderboard = true; 22 | break; 23 | case "hide-leaderboard": 24 | config.showLeaderboard = false; 25 | break; 26 | } 27 | 28 | await config.save(); 29 | 30 | clearCache("config"); 31 | log("ADMIN", `[${req.user.username}] Danger action`, action); 32 | 33 | res.redirect("/"); 34 | }; 35 | -------------------------------------------------------------------------------- /api/update-player-level.js: -------------------------------------------------------------------------------- 1 | import * as constants from '../src/constants' 2 | import { clearCache } from '../src/cache' 3 | import { log } from '../src/utils' 4 | import Player from '../models/player' 5 | 6 | export default async (req, res) => { 7 | if (!req.user || !req.user.admin) { 8 | return res.json({ 9 | success: false, 10 | message: constants.ERR_NOAUTH 11 | }) 12 | } 13 | 14 | const { username, newLevel } = req.body 15 | const player = await Player.findOne({ username }) 16 | const { level } = player 17 | 18 | if (!player) { 19 | return res.json({ 20 | success: false, 21 | message: constants.ERR_NO_PLAYER 22 | }) 23 | } 24 | 25 | player.level = newLevel 26 | player.lastLevelOn = new Date() 27 | await player.save() 28 | 29 | clearCache('leaderboard') 30 | log( 31 | 'ADMIN', 32 | `[${req.user.username}] Player level updated`, 33 | `${username}: ${level} -> ${newLevel}` 34 | ) 35 | 36 | return res.json({ 37 | success: true, 38 | message: constants.GENERIC_SUCC 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /api/update-player-type.js: -------------------------------------------------------------------------------- 1 | import * as constants from "../src/constants"; 2 | import { clearCache } from "../src/cache"; 3 | import { log } from "../src/utils"; 4 | import Player from "../models/player"; 5 | 6 | const types = ["admin", "superadmin", "phantom", "disqualified"]; 7 | 8 | export default async (req, res) => { 9 | if (!req.user || !req.user.admin) { 10 | return res.json({ 11 | success: false, 12 | message: constants.ERR_NOAUTH, 13 | }); 14 | } 15 | 16 | const { username, type, value } = req.body; 17 | const player = await Player.findOne({ username }); 18 | 19 | if (!player) { 20 | return res.json({ 21 | success: false, 22 | message: constants.ERR_NO_PLAYER, 23 | }); 24 | } 25 | 26 | if (!types.includes(type)) { 27 | return res.json({ 28 | success: false, 29 | message: constants.ERR_MISC, 30 | }); 31 | } 32 | 33 | player[type] = Number(value); 34 | await player.save(); 35 | 36 | clearCache("leaderboard"); 37 | log( 38 | "ADMIN", 39 | `[${req.user.username}] Player type changed`, 40 | `${username}: ${type} -> ${Boolean(Number(value))}` 41 | ); 42 | 43 | return res.json({ 44 | success: true, 45 | message: constants.GENERIC_SUCC, 46 | }); 47 | }; 48 | -------------------------------------------------------------------------------- /api/update-question.js: -------------------------------------------------------------------------------- 1 | import * as constants from "../src/constants"; 2 | import { clearCache } from "../src/cache"; 3 | import { log, hash } from "../src/utils"; 4 | import Question from "../models/question"; 5 | 6 | export default async (req, res) => { 7 | if (!req.user || !req.user.admin) { 8 | return res.json({ 9 | success: false, 10 | message: constants.ERR_NOAUTH, 11 | }); 12 | } 13 | 14 | const { level, question: questionText, img, answer } = req.body; 15 | const question = await Question.findOne({ level }); 16 | 17 | if (!question) { 18 | return res.json({ 19 | success: false, 20 | message: constants.ERR_NO_QUESTION, 21 | }); 22 | } 23 | 24 | question.question = questionText; 25 | question.img = img; 26 | question.answer = hash(answer.replace(/[\s]+/g, "").toLowerCase()); 27 | await question.save(); 28 | 29 | clearCache(`question_${level}`); 30 | log("ADMIN", `[${req.user.username}] Question updated`, `Level ${level}`); 31 | 32 | return res.json({ 33 | success: true, 34 | message: constants.GENERIC_SUCC, 35 | }); 36 | }; 37 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | Slash Register // Slash 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
29 |
30 | 31 | 32 | 33 |

Register

34 |

Create an account to start playing. Login if you already 35 | have. 36 |

37 |
38 |
39 |
40 |
41 |
42 |
43 |
-------------------------------------------------------------------------------- /models/config.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const ConfigSchema = new mongoose.Schema({ 4 | started: Boolean, 5 | showLeaderboard: Boolean, 6 | ended: Boolean, 7 | startDate: Date, 8 | }); 9 | 10 | export default mongoose.model("Config", ConfigSchema, "config"); 11 | -------------------------------------------------------------------------------- /models/log.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const LogSchema = new mongoose.Schema({ 4 | type: String, 5 | key: String, 6 | value: String, 7 | time: { 8 | type: Date, 9 | default: Date.now, 10 | }, 11 | }); 12 | 13 | export default mongoose.model("Log", LogSchema); 14 | -------------------------------------------------------------------------------- /models/player.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose' 2 | import passportLocalMongoose from 'passport-local-mongoose' 3 | 4 | const PlayerSchema = new mongoose.Schema({ 5 | username: { 6 | type: String, 7 | unique: true 8 | }, 9 | disqualified: { 10 | type: Boolean, 11 | default: false 12 | }, 13 | level: { 14 | type: Number, 15 | default: 0 16 | }, 17 | lastLevelOn: { 18 | type: Date, 19 | default: Date.now 20 | }, 21 | phantom: { 22 | type: Boolean, 23 | default: false 24 | }, 25 | admin: { 26 | type: Boolean, 27 | default: false 28 | }, 29 | superadmin: { 30 | type: Boolean, 31 | default: false 32 | }, 33 | geo: { 34 | country: String, 35 | city: String 36 | } 37 | }) 38 | 39 | PlayerSchema.plugin(passportLocalMongoose) 40 | 41 | export default mongoose.model('Player', PlayerSchema) 42 | -------------------------------------------------------------------------------- /models/question.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const QuestionSchema = new mongoose.Schema({ 4 | level: { 5 | type: Number, 6 | unique: true, 7 | }, 8 | question: String, 9 | img: String, 10 | answer: String, 11 | }); 12 | 13 | export default mongoose.model("Question", QuestionSchema); 14 | -------------------------------------------------------------------------------- /mongo.js: -------------------------------------------------------------------------------- 1 | // Clear and Restart DB 2 | db.dropDatabase() 3 | use slash 4 | 5 | // CLear and Reset Slash config 6 | db.config.drop() 7 | db.config.insert({ 8 | started: false, 9 | showLeaderboard: false, 10 | ended: false, 11 | startDate: new Date(2023, 0, 8).setHours(12, 30) 12 | }) 13 | 14 | //Set Admin and super admin details 15 | db.players.update({"username" : "superadmin"}, {$set: {"superadmin" : true,"admin": true}}) 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "slash", 3 | "description": "Platform for Slash Hunt", 4 | "version": "0.0.1", 5 | "private": true, 6 | "scripts": { 7 | "dev": "sapper dev", 8 | "build": "sapper build --legacy", 9 | "start": "node __sapper__/build", 10 | "lint": "eslint . --ext .js,.svelte", 11 | "lint:fix": "eslint . --ext .js,.svelte --fix" 12 | }, 13 | "dependencies": { 14 | "body-parser": "^1.19.0", 15 | "connect-mongo": "^3.2.0", 16 | "email-validator": "^2.0.4", 17 | "express": "^4.17.1", 18 | "express-rate-limit": "^5.1.3", 19 | "express-session": "^1.17.1", 20 | "mongoose": "^5.10.9", 21 | "node-fetch": "^2.6.1", 22 | "passport": "^0.4.1", 23 | "passport-local": "^1.0.0", 24 | "passport-local-mongoose": "^6.0.1", 25 | "redis": "^3.0.2", 26 | "timeago.js": "^4.0.2" 27 | }, 28 | "devDependencies": { 29 | "@rollup/plugin-commonjs": "^15.1.0", 30 | "@rollup/plugin-node-resolve": "^9.0.0", 31 | "@rollup/plugin-replace": "^2.3.3", 32 | "autoprefixer": "^9.8.6", 33 | "cssnano": "^4.1.10", 34 | "dotenv": "^8.2.0", 35 | "eslint": "^7.11.0", 36 | "eslint-config-standard": "^14.1.1", 37 | "eslint-plugin-import": "^2.22.1", 38 | "eslint-plugin-node": "^11.1.0", 39 | "eslint-plugin-promise": "^4.2.1", 40 | "eslint-plugin-standard": "^4.0.1", 41 | "eslint-plugin-svelte3": "^2.7.3", 42 | "node-sass": "^4.14.1", 43 | "rollup": "^2.29.0", 44 | "rollup-plugin-svelte": "^6.0.1", 45 | "rollup-plugin-terser": "^7.0.2", 46 | "sapper": "^0.28.10", 47 | "sapper-page-loading-bar": "^1.0.0", 48 | "svelte": "^3.29.0", 49 | "svelte-preprocess": "^4.5.1" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from "@rollup/plugin-node-resolve"; 2 | import replace from "@rollup/plugin-replace"; 3 | import commonjs from "@rollup/plugin-commonjs"; 4 | import svelte from "rollup-plugin-svelte"; 5 | import { terser } from "rollup-plugin-terser"; 6 | import config from "sapper/config/rollup.js"; 7 | import pkg from "./package.json"; 8 | import sveltePreprocess from "svelte-preprocess"; 9 | import autoprefixer from "autoprefixer"; 10 | import cssnano from "cssnano"; 11 | 12 | const mode = process.env.NODE_ENV; 13 | const dev = mode === "development"; 14 | 15 | const preprocess = sveltePreprocess({ 16 | scss: { 17 | includePaths: ["src"], 18 | }, 19 | postcss: { 20 | plugins: [autoprefixer, cssnano], 21 | }, 22 | }); 23 | 24 | const onwarn = (warning, onwarn) => { 25 | if (warning.code === "CIRCULAR_DEPENDENCY") return; 26 | return ( 27 | (warning.code === "MISSING_EXPORT" && /'preload'/.test(warning.message)) || 28 | (warning.code === "CIRCULAR_DEPENDENCY" && 29 | /[/\\]@sapper[/\\]/.test(warning.message)) || 30 | onwarn(warning) 31 | ); 32 | }; 33 | 34 | export default { 35 | client: { 36 | input: config.client.input(), 37 | output: config.client.output(), 38 | plugins: [ 39 | replace({ 40 | "process.browser": true, 41 | "process.env.NODE_ENV": JSON.stringify(mode), 42 | }), 43 | svelte({ 44 | dev, 45 | hydratable: true, 46 | preprocess, 47 | emitCss: true, 48 | }), 49 | resolve({ 50 | browser: true, 51 | dedupe: ["svelte"], 52 | }), 53 | commonjs(), 54 | 55 | !dev && 56 | terser({ 57 | module: true, 58 | }), 59 | ], 60 | preserveEntrySignatures: "strict", 61 | onwarn, 62 | }, 63 | 64 | server: { 65 | input: config.server.input(), 66 | output: config.server.output(), 67 | plugins: [ 68 | replace({ 69 | "process.browser": false, 70 | "process.env.NODE_ENV": JSON.stringify(mode), 71 | }), 72 | svelte({ 73 | generate: "ssr", 74 | hydratable: true, 75 | preprocess, 76 | dev, 77 | }), 78 | resolve({ 79 | dedupe: ["svelte"], 80 | }), 81 | commonjs(), 82 | ], 83 | external: Object.keys(pkg.dependencies).concat( 84 | // eslint-disable-next-line node/no-deprecated-api 85 | require("module").builtinModules || 86 | Object.keys(process.binding("natives")) 87 | ), 88 | preserveEntrySignatures: "strict", 89 | onwarn, 90 | }, 91 | }; 92 | -------------------------------------------------------------------------------- /routers/api.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | 3 | import addQuestion from '../api/add-question' 4 | import checkAnswer from '../api/check-answer' 5 | import clearCache from '../api/clear-cache' 6 | import getConfig from '../api/get-config' 7 | import getLeaderboard from '../api/get-leaderboard' 8 | import getPlayerDetails from '../api/get-player-details' 9 | import getPlayerRank from '../api/get-player-rank' 10 | import getQuestion from '../api/get-question' 11 | import getLogs from '../api/get-logs' 12 | import getStats from '../api/get-stats' 13 | import updateConfig from '../api/update-config' 14 | import updatePlayerLevel from '../api/update-player-level' 15 | import updatePlayerType from '../api/update-player-type' 16 | import updateQuestion from '../api/update-question' 17 | 18 | const router = express.Router() 19 | 20 | router.get('/get-config', getConfig) 21 | router.get('/get-leaderboard', getLeaderboard) 22 | router.get('/get-player-rank', getPlayerRank) 23 | router.get('/get-question', getQuestion) 24 | router.get('/get-logs', getLogs) 25 | router.get('/get-stats', getStats) 26 | router.post('/add-question', addQuestion) 27 | router.post('/check-answer', checkAnswer) 28 | router.post('/clear-cache', clearCache) 29 | router.post('/get-player-details', getPlayerDetails) 30 | router.post('/get-question', getQuestion) 31 | router.post('/update-config', updateConfig) 32 | router.post('/update-player-level', updatePlayerLevel) 33 | router.post('/update-player-type', updatePlayerType) 34 | router.post('/update-question', updateQuestion) 35 | 36 | export default router 37 | -------------------------------------------------------------------------------- /routers/auth.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import passport from 'passport' 3 | 4 | import Player from '../models/player' 5 | import * as constants from '../src/constants' 6 | import { clearCache } from '../src/cache' 7 | import { log, getGeoInfo } from '../src/utils' 8 | 9 | function login (player, req) { 10 | const response = {} 11 | 12 | req.logIn(player, (err) => { 13 | if (err) { 14 | response.success = false 15 | response.message = constants.ERR_MISC 16 | return 17 | } 18 | 19 | log('AUTH', 'Logged in', player.username) 20 | 21 | response.success = true 22 | response.message = constants.LOGIN_SUCCESS 23 | }) 24 | 25 | return response 26 | } 27 | 28 | const router = express.Router() 29 | 30 | // User logout at /auth/logout 31 | router.get('/logout', (req, res) => { 32 | log('AUTH', 'Logged out', req.user.username) 33 | 34 | req.logout() 35 | res.redirect('/') 36 | }) 37 | 38 | // User login at /auth/login 39 | router.post('/login', (req, res) => { 40 | let response = { 41 | success: false, 42 | message: undefined 43 | } 44 | 45 | passport.authenticate('local', (err, player) => { 46 | if (err) { 47 | response.message = err.message 48 | return res.json(response) 49 | } 50 | 51 | if (!player) { 52 | response.message = constants.ERR_INVALID_CREDS 53 | return res.json(response) 54 | } 55 | 56 | if (player.disqualified) { 57 | response.message = constants.ERR_PLAYER_DQ 58 | return res.json(response) 59 | } 60 | 61 | response = login(player, req) 62 | return res.json(response) 63 | })(req, res) 64 | }) 65 | 66 | // User register at /auth/register 67 | router.post('/register', async (req, res) => { 68 | let response = { 69 | success: false, 70 | message: undefined 71 | } 72 | 73 | const { username, password } = req.body 74 | 75 | if ( 76 | !constants.ANSWER_REGEX.test(username) || 77 | !constants.USERNAME_LENGTH_REGEX.test(username) 78 | ) { 79 | response.message = constants.ERR_INVALID_EMAIL_USERNAME 80 | return res.json(response) 81 | } 82 | 83 | const existingUser = await Player.findOne({ username }) 84 | if (existingUser) { 85 | response.message = constants.ERR_USERNAME_EXISTS 86 | return res.json(response) 87 | } 88 | 89 | const geo = await getGeoInfo(req) 90 | const player = await Player.register({ username, geo }, password) 91 | 92 | log('AUTH', 'Registered', username) 93 | clearCache('leaderboard') 94 | 95 | response = login(player, req) 96 | return res.json(response) 97 | }) 98 | 99 | export default router 100 | -------------------------------------------------------------------------------- /src/cache.js: -------------------------------------------------------------------------------- 1 | import dotenv from "dotenv"; 2 | import { promisify } from "util"; 3 | import redis from "redis"; 4 | import mongoose from "mongoose"; 5 | import { log } from "./utils"; 6 | 7 | dotenv.config(); 8 | 9 | const { REDIS_HOST, REDIS_PORT } = process.env; 10 | 11 | // redis 12 | const redisClient = redis.createClient({ 13 | host: REDIS_HOST, 14 | port: REDIS_PORT, 15 | retry_strategy: () => 1000, 16 | }); 17 | 18 | redisClient.get = promisify(redisClient.get); 19 | const mongoExec = mongoose.Query.prototype.exec; 20 | 21 | mongoose.Query.prototype.cache = function (options) { 22 | this.useCache = true; 23 | this.time = options.time; 24 | this.key = options.key; 25 | this.createDocument = options.createDocument; 26 | return this; 27 | }; 28 | 29 | mongoose.Query.prototype.exec = async function () { 30 | if (!this.useCache) { 31 | return await mongoExec.apply(this, arguments); 32 | } 33 | 34 | const { key } = this; 35 | const cacheValue = await redisClient.get(key); 36 | 37 | if (cacheValue) { 38 | const doc = JSON.parse(cacheValue); 39 | 40 | /* eslint-disable new-cap */ 41 | if (this.createDocument) { 42 | return Array.isArray(doc) 43 | ? doc.map((d) => new this.model(d)) 44 | : new this.model(doc); 45 | } 46 | 47 | return doc; 48 | } 49 | 50 | const result = await mongoExec.apply(this, arguments); 51 | 52 | redisClient.set(key, JSON.stringify(result)); 53 | log("CACHE", "Set", key); 54 | 55 | if (this.time) { 56 | redisClient.expire(key, this.time, () => {}); 57 | } 58 | 59 | return result; 60 | }; 61 | 62 | export function clearCache(key) { 63 | if (key) { 64 | redisClient.del(key); 65 | log("CACHE", "Cleared", key); 66 | } else { 67 | const success = redisClient.flushall(); 68 | log("CACHE", "Flushed all keys", success); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/client.js: -------------------------------------------------------------------------------- 1 | import * as sapper from "@sapper/app"; 2 | 3 | sapper.start({ 4 | target: document.body, 5 | }); 6 | 7 | // Unregister all service workers from client's devices 8 | // Kuch nahi kar rahe vo apart from caching which I don't 9 | // really need and the browsers are already doing their bit 10 | if ("serviceWorker" in navigator) { 11 | navigator.serviceWorker.getRegistrations().then(function (registrations) { 12 | for (const registration of registrations) { 13 | registration.unregister(); 14 | } 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /src/components/AdminForm.svelte: -------------------------------------------------------------------------------- 1 | 34 | 35 |
41 |

{heading}

42 | 43 | {#if response} 44 |

49 | {response.message} 50 |

51 | {/if} 52 | 53 | 54 | 55 | {#if response && response.success && displayResponse} 56 | 57 | {#each displayResponseFormatter(response.data) as [key, value]} 58 | 59 | 60 | 61 | 62 | {/each} 63 |
{key}{value}
64 | {/if} 65 | 66 |
67 | 68 |
69 | 70 | 71 | 99 | -------------------------------------------------------------------------------- /src/components/Chevron.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 18 | 19 | 28 | -------------------------------------------------------------------------------- /src/components/Countdown.svelte: -------------------------------------------------------------------------------- 1 | 40 | 41 |
42 | {days} 43 | d 44 | 45 | {hours < 10 ? "0" + hours : hours} 46 | h 47 | 48 | {minutes < 10 ? "0" + minutes : minutes} 49 | m 50 | 51 | {seconds < 10 ? "0" + seconds : seconds} 52 | s 53 |
54 | 55 | 73 | -------------------------------------------------------------------------------- /src/components/Navigation.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 |
13 | 76 |
77 | 78 | 174 | -------------------------------------------------------------------------------- /src/components/play/Footer.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 |
8 |

Level {level}

9 |

{username}

10 |

Rank: {rank}

11 |
12 | 13 | 47 | -------------------------------------------------------------------------------- /src/components/play/GameArea.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
14 |
15 | {#if !config.started} 16 |

Hang tight!

17 |

Enigma will begin in

18 | 19 |

20 | Till then, join our 21 | Discord server. 22 |

23 | {:else if win} 24 |

Congratulations!

25 |

26 | You have successfully completed Slash with a rank of {rank}! 29 |

30 |

31 | Go to the leaderboard to check the standings. 32 |

33 | {:else if config.ended} 34 |

The End!

35 |

36 | Enigma has come to an end! You have finished with a rank of {rank}. 39 |

40 |

41 | Go to the leaderboard to check the standings. 42 |

43 | {:else} 44 |

Question

45 | 46 | {/if} 47 |
48 |
49 | 50 | 94 | -------------------------------------------------------------------------------- /src/components/play/Navigation.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 |
11 | 49 |
50 | 51 | 150 | -------------------------------------------------------------------------------- /src/components/play/QuestionAnswer.svelte: -------------------------------------------------------------------------------- 1 | 58 | 59 | 60 |

{@html question}

61 | 62 |
63 |
64 | 71 |
72 | 81 |
82 | 83 | 187 | -------------------------------------------------------------------------------- /src/components/play/SidePanel.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 17 | 18 | 74 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | export const ANSWER_REGEX = /^\S+$/; 2 | export const USERNAME_LENGTH_REGEX = /^.{2,25}$/; 3 | export const COOKIE_MAX_AGE = 2592000000; 4 | export const MAX_LEADERBOARD_PLAYERS = 50; 5 | 6 | // Response success 7 | export const CORRECT_ANS = "Correct answer"; 8 | export const GENERIC_SUCC = "Successful request"; 9 | export const LOGIN_SUCCESS = "Successful, redirecting..."; 10 | 11 | // Response failure 12 | export const ERR_NOAUTH = "Request unauthenticated"; 13 | export const ERR_INVALID_ANS = 14 | "No whitespace allowed. Further invalid attempts will lead to disqualification."; 15 | export const ERR_WRONG_ANS = "Wrong answer"; 16 | export const ERR_MISC = "An error occured while processing the query"; 17 | export const ERR_NO_PLAYER = "Player with requested ID not found"; 18 | export const ERR_NO_QUESTION = "Question could not be found"; 19 | export const ERR_LEVEL_EXISTS = "Question for specified level exists"; 20 | export const ERR_INVALID_CREDS = "Wrong player username or password"; 21 | export const ERR_INVALID_EMAIL_USERNAME = "Invalid username"; 22 | export const ERR_USERNAME_EXISTS = "Username taken"; 23 | export const ERR_EMAIL_EXISTS = 24 | "Email is already in use. Please try another email or login using existing account"; 25 | export const ERR_PLAYER_DQ = "You have been disqualified"; 26 | export const HUNT_END = "The hunt has ended"; 27 | -------------------------------------------------------------------------------- /src/routes/_error.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | {status} 10 | 11 | 12 |

{status}

13 | 14 |

{error.message}

15 | 16 | {#if dev && error.stack} 17 |
{error.stack}
18 | {/if} 19 | 20 | 42 | -------------------------------------------------------------------------------- /src/routes/_layout.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 18 | 19 | 20 | {#if !$session.user || !$session.user.admin} 21 | 29 | 34 | {/if} 35 | 36 | 37 | 38 | 39 | {#if segment} 40 |