├── .gitignore ├── README.md ├── backend ├── .env.example ├── api │ ├── auth │ │ ├── index.js │ │ ├── register-schema.js │ │ ├── register.js │ │ └── verification.js │ ├── index.js │ ├── leaderboard.js │ ├── levels.js │ ├── logs.js │ ├── play │ │ ├── gate.js │ │ ├── guard.js │ │ ├── index.js │ │ ├── jail.js │ │ ├── riddle.js │ │ └── story.js │ └── shop.js ├── app.js ├── assets │ ├── WYkPG9E.png │ └── sme.png ├── backlinks.js ├── bin │ └── serve ├── claudia.json ├── config │ └── passport.js ├── killshot │ ├── 404 │ │ └── index.html │ ├── 1bfc9850-32de6903d90ba13d6813.js │ ├── 1bfc9850-32de6903d90ba13d6813.js.map │ ├── 404.html │ ├── 95b64a6e-e953716b84c4d8bd38ef.js │ ├── 95b64a6e-e953716b84c4d8bd38ef.js.map │ ├── CNAME │ ├── about │ │ └── index.html │ ├── app-2971bdda35af8fc65532.js │ ├── app-2971bdda35af8fc65532.js.map │ ├── chunk-map.json │ ├── commons-93a2c044275dc9da3e4a.js │ ├── commons-93a2c044275dc9da3e4a.js.map │ ├── component---src-pages-404-js-fd850f06d3b7f74c62e0.js │ ├── component---src-pages-404-js-fd850f06d3b7f74c62e0.js.map │ ├── component---src-pages-about-js-3bb07278d80038d36196.js │ ├── component---src-pages-about-js-3bb07278d80038d36196.js.map │ ├── component---src-pages-index-js-2a49de6e7a758ff68c8e.js │ ├── component---src-pages-index-js-2a49de6e7a758ff68c8e.js.map │ ├── favicon-32x32.png │ ├── framework-75da9754c2a76bbaf08a.js │ ├── framework-75da9754c2a76bbaf08a.js.map │ ├── icons │ │ ├── icon-144x144.png │ │ ├── icon-192x192.png │ │ ├── icon-256x256.png │ │ ├── icon-384x384.png │ │ ├── icon-48x48.png │ │ ├── icon-512x512.png │ │ ├── icon-72x72.png │ │ └── icon-96x96.png │ ├── index.html │ ├── manifest.webmanifest │ ├── page-data │ │ ├── 404 │ │ │ └── page-data.json │ │ ├── 404.html │ │ │ └── page-data.json │ │ ├── about │ │ │ └── page-data.json │ │ ├── app-data.json │ │ └── index │ │ │ └── page-data.json │ ├── static │ │ ├── 6d91c86c0fde632ba4cd01062fd9ccfa │ │ │ ├── 2a4de │ │ │ │ └── gatsby-astronaut.png │ │ │ ├── 5db04 │ │ │ │ └── gatsby-astronaut.png │ │ │ ├── 62b1f │ │ │ │ └── gatsby-astronaut.png │ │ │ ├── 630fb │ │ │ │ └── gatsby-astronaut.png │ │ │ ├── 6d161 │ │ │ │ └── gatsby-astronaut.png │ │ │ └── ee604 │ │ │ │ └── gatsby-astronaut.png │ │ └── d │ │ │ ├── 2417117884.json │ │ │ ├── 2969191536.json │ │ │ └── 856328897.json │ ├── styles-dd3841a4888192e20843.js │ ├── styles-dd3841a4888192e20843.js.map │ ├── styles.4a6afaa3d5790f28f7bd.css │ ├── webpack-runtime-611b74ddb81015b1fa4d.js │ ├── webpack-runtime-611b74ddb81015b1fa4d.js.map │ └── webpack.stats.json ├── lambda.js ├── lib │ ├── action-from-user-state.js │ ├── auth.js │ ├── bot.js │ ├── helpers.js │ ├── logs.js │ ├── prisma.js │ ├── recaptcha.js │ ├── tiles.js │ └── validation.js ├── migrations │ ├── 20200624195700-init │ │ ├── README.md │ │ ├── schema.prisma │ │ └── steps.json │ ├── 20200625162125-replace-json-types-with-strins │ │ ├── README.md │ │ ├── schema.prisma │ │ └── steps.json │ └── migrate.lock ├── package-lock.json ├── package.json ├── schema.prisma └── seed │ ├── levels.js │ ├── riddles.js │ ├── run.js │ └── tiles.js └── frontend ├── .env.example ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.png ├── index.html ├── manifest.json └── robots.txt └── src ├── App.js ├── assets ├── favicon.png ├── ivyachievement.png ├── logo.png └── svg │ └── logo.svg ├── components ├── Layout │ ├── AuthCheck.js │ ├── Navbar.js │ └── index.js ├── Loading.js ├── Login │ └── form.js ├── Play │ ├── Grid │ │ ├── index.js │ │ ├── middle.js │ │ ├── tile-data.js │ │ └── tile.js │ ├── States │ │ ├── Finished.js │ │ ├── Gate.js │ │ ├── GateI.js │ │ ├── Guard.js │ │ ├── Jail.js │ │ ├── MinRiddle.js │ │ ├── Move.js │ │ ├── MoveWithStory.js │ │ ├── Mystery.js │ │ ├── Riddle.js │ │ ├── SideQuest.js │ │ ├── SkipRiddle.js │ │ └── index.js │ └── Widgets │ │ ├── level.js │ │ └── story.js ├── Register │ ├── form.js │ └── register-schema.js └── forms.js ├── index.css ├── index.js ├── lib ├── api.js └── auth-context.js ├── pages ├── 404.js ├── index.js ├── leaderboard.js ├── logs.js ├── play.js ├── register.js ├── shop.js ├── signin.js └── verify │ └── email.js └── serviceWorker.js /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | 8 | # testing 9 | coverage 10 | 11 | # production 12 | build 13 | react_build 14 | 15 | # misc 16 | .DS_Store 17 | .env 18 | .env.local 19 | .env.development.local 20 | .env.test.local 21 | .env.production.local 22 | 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cryptocracy Platform 2 | 3 | ## Setup 4 | 5 | 0. Install and setup: `node`, `redis`, `mysql`. 6 | 7 | 1. Clone the repository and install dependencies. 8 | 9 | ```sh 10 | git clone https://github.com/cryptichunt/platform cryptocracy-platform 11 | cd cryptocracy-platform/backend 12 | npm i 13 | cd ../frontend 14 | npm i 15 | ``` 16 | 17 | 2. Copy and populate config files 18 | 19 | ```sh 20 | cd ../backend 21 | cp .env.example .env 22 | cp config/config.example.json config/config.json 23 | ``` 24 | 25 | 3. Create a database and run migrations on it 26 | 27 | ```sh 28 | MYSQL_URL= prisma migrate up 29 | node seed/run all 30 | ``` 31 | 32 | 4. Start a redis server and check if it's running 33 | 34 | ```sh 35 | redis-server --daemonize yes 36 | ps aux | grep redis-server 37 | ``` 38 | 39 | 4. Run the app 40 | 41 | ```sh 42 | npm run serve:dev 43 | # In another shell 44 | cd ../frontend && npm run start 45 | ``` 46 | -------------------------------------------------------------------------------- /backend/.env.example: -------------------------------------------------------------------------------- 1 | PORT=62442 2 | SECRET= 3 | MYSQL_URL= 4 | REDIS_URL= 5 | RECAPTCHA_SECRET_KEY= 6 | SENTRY_DSN= 7 | -------------------------------------------------------------------------------- /backend/api/auth/index.js: -------------------------------------------------------------------------------- 1 | const router = require("express").Router(); 2 | const passport = require("passport"); 3 | const recaptcha = require("../../lib/recaptcha"); 4 | const { client } = require("../../lib/prisma"); 5 | 6 | router.use("/verification", require("./verification")); 7 | router.post("/register", ...require("./register")); 8 | // TODO: password resets 9 | 10 | router.post("/login", recaptcha.verify(), (req, res, next) => 11 | passport.authenticate("local", (error, user, info) => { 12 | if (error) { 13 | return res.json({ success: false, message: error }); 14 | } 15 | 16 | if (user) { 17 | return req.logIn(user, (err) => { 18 | if (err) { 19 | return res.json({ success: false, message: err }); 20 | } 21 | 22 | return res.status(200).json({ 23 | success: true, 24 | message: "User authenticated", 25 | user, 26 | }); 27 | }); 28 | } 29 | 30 | return res.json({ success: false, message: info.message }); 31 | })(req, res, next) 32 | ); 33 | 34 | router.post("/me", async (req, res, next) => 35 | res.json({ 36 | success: true, 37 | authenticated: req.isAuthenticated(), 38 | user: req.user, 39 | }) 40 | ); 41 | 42 | router.get("/logout", (req, res) => { 43 | req.logOut(); 44 | res.json({ 45 | success: true, 46 | message: "Logged out", 47 | }); 48 | }); 49 | 50 | module.exports = router; 51 | -------------------------------------------------------------------------------- /backend/api/auth/register-schema.js: -------------------------------------------------------------------------------- 1 | const yup = require('yup') 2 | 3 | module.exports = yup.object().shape({ 4 | email: yup 5 | .string('Invalid email') 6 | .email('Invalid email') 7 | .required('Email is required'), 8 | username: yup 9 | .string('Invalid username') 10 | .required('Username is required') 11 | .min(3, 'Username must be longer than 3 characters') 12 | .max(20, "Username can't be longer than 20 characters") 13 | .matches( 14 | /^[a-zA-Z0-9_]*$/g, 15 | 'Username can only contain letters, digits and underscores' 16 | ), 17 | password: yup 18 | .string('Invalid password') 19 | .required('Password is required') 20 | .min(3, 'Password must be longer than 3 characters') 21 | .max(20, "Password can't be longer than 20 characters"), 22 | discord: yup 23 | .string('Invalid discord username') 24 | .required('Discord username is required') 25 | .matches(/^[^@#:]*#\d{4}$/gu, 'Invalid discord username') 26 | .matches(/^((?!```).)*$/gu, 'Invalid discord username') 27 | .notOneOf(['everyone', 'discordtag', 'here'], 'Invalid discord username'), 28 | edu: yup.string('Invalid educational innstitution'), 29 | name: yup 30 | .string('Invalid name') 31 | .min(3, 'Name must be over 3 characters') 32 | .max(255, 'Name must be less than 255 characters'), 33 | }) 34 | -------------------------------------------------------------------------------- /backend/api/auth/register.js: -------------------------------------------------------------------------------- 1 | const bcrypt = require("bcrypt"); 2 | const nodemailer = require("nodemailer"); 3 | const { against, generateVerificationURL } = require("../../lib/validation"); 4 | const recaptcha = require("../../lib/recaptcha"); 5 | const logs = require("../../lib/logs"); 6 | const { client } = require("../../lib/prisma"); 7 | 8 | async function sendMail(email, url) { 9 | const transporter = nodemailer.createTransport({ 10 | host: "smtp.zoho.in", 11 | port: 465, 12 | secure: true, 13 | auth: { 14 | user: process.env.EMAIL, 15 | pass: process.env.PASSWORD, 16 | }, 17 | }); 18 | 19 | // send mail with defined transport object 20 | const info = await transporter.sendMail({ 21 | from: '"Team Cryptocracy" ', 22 | to: email, // list of receivers 23 | subject: "Please verify your email", // Subject line 24 | html: `Click the following link - ${url}`, // html body 25 | }); 26 | 27 | return info; 28 | } 29 | 30 | module.exports = [ 31 | recaptcha.verify(), 32 | // against(require("./register-schema")), 33 | async (req, res, next) => { 34 | try { 35 | const { email, username, password, discord, edu, name } = req.body; 36 | 37 | const user = await client.user.create({ 38 | data: { 39 | email, 40 | username, 41 | password: await bcrypt.hash(password, 14), 42 | discord, 43 | edu, 44 | name, 45 | points: 200, 46 | emailVerified: true, 47 | discordVerified: true, 48 | currentTile: { connect: { id: 1 } }, 49 | }, 50 | }); 51 | 52 | await client.visitedTile.create({ 53 | data: { 54 | tile: { connect: { id: 1 } }, 55 | user: { connect: { id: user.id } }, 56 | }, 57 | }); 58 | 59 | // const emailUrl = await generateVerificationURL('email', user) 60 | // await sendMail(user.email, emailUrl) 61 | 62 | await logs.add(user.id, `${user.username} accepted their mission`); 63 | 64 | res.json({ 65 | success: true, 66 | message: "User created", 67 | id: user.id, 68 | }); 69 | } catch (e) { 70 | if (e.message.match(/Unique constraint/g)) { 71 | return res.json({ 72 | success: false, 73 | message: `An account already exists with this ${e.meta.target}`, 74 | }); 75 | } else { 76 | e.statusCode = 500; 77 | next(e); 78 | } 79 | } 80 | }, 81 | ]; 82 | -------------------------------------------------------------------------------- /backend/api/auth/verification.js: -------------------------------------------------------------------------------- 1 | const router = require("express").Router(); 2 | const jwt = require("jsonwebtoken"); 3 | const { client } = require("../../lib/prisma"); 4 | 5 | router.post("/email", async (req, res, next) => { 6 | try { 7 | const { token } = req.body; 8 | 9 | const payload = jwt.verify(token, process.env.SECRET); 10 | 11 | await client.user.update({ 12 | where: { id: payload.userId }, 13 | data: { emailVerified: true }, 14 | }); 15 | 16 | return res.json({ success: true, message: "Email verified" }); 17 | } catch (e) { 18 | return next(e); 19 | } 20 | }); 21 | 22 | module.exports = router; 23 | -------------------------------------------------------------------------------- /backend/api/index.js: -------------------------------------------------------------------------------- 1 | const router = require("express").Router(); 2 | 3 | router.get("/", (req, res) => 4 | res.json({ success: true, message: "cryptoooo" }) 5 | ); 6 | router.use("/auth", require("./auth")); 7 | // router.use("/shop", require("./shop")); 8 | router.use("/leaderboard", require("./leaderboard")); 9 | router.use("/logs", require("./logs")); 10 | router.use("/play", require("./play")); 11 | router.use("/levels", require("./levels")); 12 | 13 | router.use("*", (req, res) => 14 | res.json({ success: false, message: "Resource not found" }) 15 | ); 16 | 17 | module.exports = router; 18 | -------------------------------------------------------------------------------- /backend/api/leaderboard.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router() 2 | const { client } = require('../lib/prisma') 3 | 4 | router.get('/', async (req, res, next) => { 5 | try { 6 | const users = await client.user.findMany({ 7 | where: { 8 | admin: false, 9 | emailVerified: true, 10 | discordVerified: true, 11 | bountyBanned: false, 12 | dqed: false, 13 | }, 14 | orderBy: { points: 'desc' }, 15 | select: { username: true, points: true }, 16 | }) 17 | 18 | res.json({ 19 | success: true, 20 | message: `${users.length} users on leaderboard`, 21 | users, 22 | }) 23 | } catch (e) { 24 | e.status = 500 25 | return next(e) 26 | } 27 | }) 28 | 29 | module.exports = router 30 | -------------------------------------------------------------------------------- /backend/api/levels.js: -------------------------------------------------------------------------------- 1 | const router = require("express").Router(); 2 | const { check, canPlay } = require("../lib/auth"); 3 | const yup = require("yup"); 4 | const { against } = require("../lib/validation"); 5 | const logs = require("../lib/logs"); 6 | 7 | const { client } = require("../lib/prisma"); 8 | 9 | router.use(check); 10 | router.use(canPlay); 11 | 12 | router.get("/", async (req, res, next) => { 13 | try { 14 | const [lvl] = await client.userLevel.findMany({ 15 | where: { completed: false, userId: req.user.id }, 16 | orderBy: { createdAt: "desc" }, 17 | take: 1, 18 | include: { level: true }, 19 | }); 20 | 21 | lvl.level.answer = "nicetry"; 22 | lvl.level.createdAt = new Date(); 23 | lvl.level.updatedAt = new Date(); 24 | 25 | res.json({ success: true, lvl }); 26 | } catch (e) { 27 | return next(e); 28 | } 29 | }); 30 | 31 | router.post( 32 | "/answer", 33 | against( 34 | yup.object().shape({ 35 | answer: yup 36 | .string() 37 | .required() 38 | .matches(/[a-z0-9-_.{}]+/) 39 | .max(45), 40 | }) 41 | ), 42 | async (req, res, next) => { 43 | try { 44 | const [lvl] = await client.userLevel.findMany({ 45 | where: { completed: false, userId: req.user.id }, 46 | orderBy: { createdAt: "desc" }, 47 | take: 1, 48 | include: { level: true }, 49 | }); 50 | 51 | if (!lvl) { 52 | return res.json({ 53 | success: false, 54 | message: "You're not currently on a level.", 55 | user: req.user, 56 | }); 57 | } 58 | 59 | await client.levelAttempt.create({ 60 | data: { 61 | level: { connect: { id: lvl.level.id } }, 62 | user: { connect: { id: req.user.id } }, 63 | attempt: req.body.answer, 64 | }, 65 | }); 66 | 67 | if (req.body.answer === lvl.level.answer) { 68 | await client.user.update({ 69 | where: { id: req.user.id }, 70 | data: { points: req.user.points + lvl.level.points }, 71 | }); 72 | 73 | await logs.add( 74 | req.user.id, 75 | `${req.user.username} solved level ${lvl.levelId}` 76 | ); 77 | 78 | await client.userLevel.update({ 79 | where: { id: lvl.id }, 80 | data: { completed: true, completedAt: new Date() }, 81 | }); 82 | 83 | return res.json({ 84 | success: true, 85 | message: `You gave the correct answer to level ${lvl.levelId}!`, 86 | user: req.user, 87 | }); 88 | } 89 | 90 | return res.json({ 91 | success: false, 92 | message: "Wrong answer, please try again.", 93 | user: req.user, 94 | }); 95 | } catch (e) { 96 | return next(e); 97 | } 98 | } 99 | ); 100 | 101 | router.post("/skip", async (req, res, next) => { 102 | try { 103 | if (req.user.points < 351) { 104 | return res.json({ success: false, user: req.user }); 105 | } 106 | 107 | const [lvl] = await client.userLevel.findMany({ 108 | where: { completed: false, userId: req.user.id }, 109 | orderBy: { createdAt: "desc" }, 110 | take: 1, 111 | include: { level: true }, 112 | }); 113 | 114 | if (lvl.level.id === 47) { 115 | return res.json({ success: false, message: "lulz" }); 116 | } 117 | 118 | if (!lvl) { 119 | return res.json({ 120 | success: false, 121 | message: "You're not currently on a level.", 122 | user: req.user, 123 | }); 124 | } 125 | 126 | await logs.add( 127 | req.user.id, 128 | `${req.user.username} skipped level ${lvl.level.id}` 129 | ); 130 | 131 | await client.user.update({ 132 | where: { id: req.user.id }, 133 | data: { points: req.user.points - 350 }, 134 | }); 135 | 136 | await client.userLevel.update({ 137 | where: { id: lvl.id }, 138 | data: { completed: true, completedAt: new Date() }, 139 | }); 140 | 141 | res.json({ 142 | success: true, 143 | message: "You skipped this level", 144 | user: req.user, 145 | }); 146 | } catch (e) { 147 | return next(e); 148 | } 149 | }); 150 | 151 | module.exports = router; 152 | -------------------------------------------------------------------------------- /backend/api/logs.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router() 2 | const auth = require('../lib/auth') 3 | const { client } = require('../lib/prisma') 4 | 5 | router.use(auth.check) 6 | router.use(auth.canPlay) 7 | 8 | router.get('/', auth.admin, async (req, res, next) => { 9 | try { 10 | const logs = await client.log.findMany({ 11 | where: {}, 12 | orderBy: { createdAt: 'desc' }, 13 | select: { createdAt: true, log: true }, 14 | }) 15 | 16 | res.json({ 17 | success: true, 18 | logs, 19 | }) 20 | } catch (e) { 21 | e.status = 500 22 | return next(e) 23 | } 24 | }) 25 | 26 | router.get('/self', async (req, res, next) => { 27 | try { 28 | const logs = await client.log.findMany({ 29 | where: { userId: req.user.id }, 30 | orderBy: { createdAt: 'desc' }, 31 | select: { createdAt: true, log: true }, 32 | }) 33 | 34 | res.json({ 35 | success: true, 36 | logs, 37 | }) 38 | } catch (e) { 39 | e.status = 500 40 | return next(e) 41 | } 42 | }) 43 | 44 | router.get('/:userId', auth.admin, async (req, res, next) => { 45 | try { 46 | const logs = await client.log.findMany({ 47 | where: { userId: parseInt(req.params.userId, 10) }, 48 | orderBy: { createdAt: 'desc' }, 49 | select: { createdAt: true, log: true }, 50 | }) 51 | 52 | res.json({ 53 | success: true, 54 | logs, 55 | }) 56 | } catch (e) { 57 | e.status = 500 58 | return next(e) 59 | } 60 | }) 61 | 62 | module.exports = router 63 | -------------------------------------------------------------------------------- /backend/api/play/gate.js: -------------------------------------------------------------------------------- 1 | const router = require("express").Router(); 2 | const { client } = require("../../lib/prisma"); 3 | 4 | router.get("/in", async (req, res, next) => { 5 | try { 6 | const levels = await client.userLevel.count({ 7 | where: { completed: true, userId: req.user.id }, 8 | }); 9 | 10 | res.json({ success: true, levels, allowed: levels > 23 }); 11 | } catch (e) { 12 | return next(e); 13 | } 14 | }); 15 | 16 | module.exports = router; 17 | -------------------------------------------------------------------------------- /backend/api/play/guard.js: -------------------------------------------------------------------------------- 1 | const router = require("express").Router(); 2 | const { rollDice } = require("../../lib/helpers"); 3 | const logs = require("../../lib/logs"); 4 | const { client } = require("../../lib/prisma"); 5 | 6 | router.use(async (req, res, next) => { 7 | try { 8 | const [currentTile] = await client.visitedTile.findMany({ 9 | where: { userId: req.user.id }, 10 | orderBy: { createdAt: "desc" }, 11 | take: 1, 12 | include: { tile: true }, 13 | }); 14 | 15 | if ( 16 | currentTile.tile.type === "RAND_PERSON" && 17 | currentTile.randomPersonType === "GUARD" 18 | ) { 19 | return next(); 20 | } 21 | 22 | return res.json({ success: false, message: "Not available" }); 23 | } catch (e) { 24 | return next(e); 25 | } 26 | }); 27 | 28 | router.post("/bribe", async (req, res, next) => { 29 | try { 30 | if (req.user.points < 126) { 31 | return res.json({ success: true, message: "not enough points" }); 32 | } 33 | 34 | await client.user.update({ 35 | where: { id: req.user.id }, 36 | data: { points: req.user.points - 125 }, 37 | }); 38 | 39 | await logs.add(req.user.id, `${req.user.username} bribed a guard`); 40 | 41 | const [currentTile] = await client.visitedTile.findMany({ 42 | where: { userId: req.user.id }, 43 | orderBy: { createdAt: "desc" }, 44 | take: 1, 45 | include: { tile: true }, 46 | }); 47 | 48 | await client.visitedTile.update({ 49 | where: { id: currentTile.id }, 50 | data: { guardPassed: true }, 51 | }); 52 | 53 | return res.json({ 54 | success: true, 55 | message: "You bribed the guard", 56 | user: req.user, 57 | }); 58 | } catch (e) { 59 | return next(e); 60 | } 61 | }); 62 | 63 | router.post("/fight", async (req, res, next) => { 64 | try { 65 | const roll = rollDice(); 66 | console.log({ roll }); 67 | 68 | if (roll < 3) { 69 | const newUser = await client.user.update({ 70 | where: { id: req.user.id }, 71 | data: { 72 | incarcerated: true, 73 | incarceratedAt: new Date(), 74 | currentTile: { connect: { id: 12 } }, 75 | }, 76 | }); 77 | 78 | await logs.add( 79 | req.user.id, 80 | `${req.user.username} fought a guard and lost` 81 | ); 82 | 83 | await client.visitedTile.create({ 84 | data: { 85 | user: { connect: { id: req.user.id } }, 86 | tile: { connect: { id: 12 } }, 87 | }, 88 | }); 89 | 90 | return res.json({ 91 | success: true, 92 | won: false, 93 | message: "You fought the guard and lost", 94 | user: newUser, 95 | }); 96 | } 97 | 98 | const [currentTile] = await client.visitedTile.findMany({ 99 | where: { userId: req.user.id }, 100 | orderBy: { createdAt: "desc" }, 101 | take: 1, 102 | include: { tile: true }, 103 | }); 104 | 105 | await client.visitedTile.update({ 106 | where: { id: currentTile.id }, 107 | data: { guardPassed: true }, 108 | }); 109 | 110 | await logs.add(req.user.id, `${req.user.username} fought a guard and won`); 111 | 112 | return res.json({ 113 | success: true, 114 | won: true, 115 | message: "You fought the guard and won", 116 | user: req.user, 117 | }); 118 | } catch (e) { 119 | return next(e); 120 | } 121 | }); 122 | 123 | module.exports = router; 124 | -------------------------------------------------------------------------------- /backend/api/play/jail.js: -------------------------------------------------------------------------------- 1 | const router = require("express").Router(); 2 | const logs = require("../../lib/logs"); 3 | const { client } = require("../../lib/prisma"); 4 | 5 | router.post("/bribe", async (req, res, next) => { 6 | try { 7 | await client.user.update({ 8 | where: { id: req.user.id }, 9 | data: { 10 | incarcerated: false, 11 | incarceratedAt: null, 12 | points: req.user.points - 150, 13 | }, 14 | }); 15 | 16 | await logs.add(req.user.id, `${req.user.username} escaped from jail`); 17 | 18 | res.json({ 19 | success: true, 20 | user: req.user, 21 | message: "You escaped from jail", 22 | }); 23 | } catch (e) { 24 | return next(e); 25 | } 26 | }); 27 | 28 | module.exports = router; 29 | -------------------------------------------------------------------------------- /backend/api/play/riddle.js: -------------------------------------------------------------------------------- 1 | const router = require("express").Router(); 2 | const { client } = require("../../lib/prisma"); 3 | const logs = require("../../lib/logs"); 4 | 5 | router.get("/", async (req, res, next) => { 6 | try { 7 | const [riddle] = await client.userRiddle.findMany({ 8 | where: { completed: false, userId: req.user.id }, 9 | include: { riddle: true }, 10 | }); 11 | 12 | riddle.riddle.answer = "nicetry"; 13 | riddle.riddle.createdAt = new Date(); 14 | riddle.riddle.updatedAt = new Date(); 15 | 16 | res.json({ success: true, riddle }); 17 | } catch (e) { 18 | return next(e); 19 | } 20 | }); 21 | 22 | router.post("/accept", async (req, res, next) => { 23 | try { 24 | if (req.session.userState !== "rp-riddle-skippable") { 25 | return res.json({ success: false, message: "E H" }); 26 | } 27 | 28 | await logs.add( 29 | req.user.id, 30 | `${req.user.username} accepted the sphinx's challenge` 31 | ); 32 | 33 | // Get a riddle the user hasn't seen 34 | const riddleCount = await client.riddle.count(); 35 | const getRandId = () => Math.ceil(Math.random() * riddleCount); 36 | const userRiddles = await client.userRiddle.findMany({ 37 | include: { riddle: true }, 38 | }); 39 | 40 | let found = false; 41 | let riddleId = 0; 42 | 43 | while (!found) { 44 | const id = getRandId(); 45 | const r = userRiddles.find((u) => u.riddleId === id); 46 | found = !!!r; 47 | riddleId = id; 48 | } 49 | 50 | const [currentTile] = await client.visitedTile.findMany({ 51 | where: { userId: req.user.id }, 52 | orderBy: { createdAt: "desc" }, 53 | take: 1, 54 | include: { tile: true }, 55 | }); 56 | 57 | await client.userRiddle.create({ 58 | data: { 59 | riddle: { connect: { id: riddleId } }, 60 | user: { connect: { id: req.user.id } }, 61 | tile: { connect: { id: currentTile.id } }, 62 | }, 63 | }); 64 | 65 | return res.json({ 66 | success: true, 67 | user: req.user, 68 | message: "You accepted the riddle", 69 | }); 70 | } catch (e) { 71 | return next(e); 72 | } 73 | }); 74 | 75 | router.post("/answer", async (req, res, next) => { 76 | try { 77 | const [riddle] = await client.userRiddle.findMany({ 78 | where: { completed: false, userId: req.user.id }, 79 | orderBy: { createdAt: "desc" }, 80 | take: 1, 81 | include: { riddle: true }, 82 | }); 83 | 84 | if (!riddle) { 85 | return res.json({ 86 | success: false, 87 | message: "E H", 88 | user: req.user, 89 | }); 90 | } 91 | 92 | await client.riddleAttempt.create({ 93 | data: { 94 | riddle: { connect: { id: riddle.riddle.id } }, 95 | user: { connect: { id: req.user.id } }, 96 | attempt: req.body.answer, 97 | }, 98 | }); 99 | 100 | if (req.body.answer === riddle.riddle.answer) { 101 | await client.user.update({ 102 | where: { id: req.user.id }, 103 | data: { points: req.user.points + 300 }, 104 | }); 105 | 106 | await logs.add(req.user.id, `${req.user.username} solved the riddle`); 107 | 108 | await client.userRiddle.update({ 109 | where: { id: riddle.id }, 110 | data: { completed: true, completedAt: new Date() }, 111 | }); 112 | 113 | return res.json({ 114 | success: true, 115 | message: `You solved the riddle!`, 116 | user: req.user, 117 | }); 118 | } 119 | 120 | return res.json({ 121 | success: false, 122 | message: "Wrong answer, please try again.", 123 | user: req.user, 124 | }); 125 | } catch (e) { 126 | return next(e); 127 | } 128 | }); 129 | 130 | module.exports = router; 131 | -------------------------------------------------------------------------------- /backend/api/play/story.js: -------------------------------------------------------------------------------- 1 | const router = require("express").Router(); 2 | const { client } = require("../../lib/prisma"); 3 | 4 | router.get("/:tileId", async (req, res, next) => { 5 | try { 6 | const tiles = await client.visitedTile.findMany({ 7 | where: { 8 | tileId: parseInt(req.params.tileId), 9 | userId: req.user.id, 10 | tile: { type: "STORY" }, 11 | }, 12 | include: { tile: true }, 13 | }); 14 | 15 | res.json({ success: true, tile: tiles.length > 0 ? tiles[0].tile : null }); 16 | } catch (e) { 17 | return next(e); 18 | } 19 | }); 20 | 21 | module.exports = router; 22 | -------------------------------------------------------------------------------- /backend/api/shop.js: -------------------------------------------------------------------------------- 1 | const router = require("express").Router(); 2 | const auth = require("../lib/auth"); 3 | const logs = require("../lib/logs"); 4 | const { client } = require("../lib/prisma"); 5 | 6 | router.use(auth.check); 7 | router.use(auth.canPlay); 8 | 9 | router.post("/buy", async (req, res, next) => { 10 | try { 11 | if (req.user.points < 126 || req.user.hasHintCard) { 12 | res.json({ success: true, message: "No" }); 13 | } 14 | 15 | const newUser = await client.user.update({ 16 | where: { id: req.user.id }, 17 | data: { hasHintCard: true, points: req.user.points - 125 }, 18 | }); 19 | 20 | await logs.add(newUser.id, `${newUser.username} bought a hint card`); 21 | 22 | res.json({ success: true, message: "Hint card purchased", user: newUser }); 23 | } catch (e) { 24 | return next(e); 25 | } 26 | }); 27 | 28 | module.exports = router; 29 | -------------------------------------------------------------------------------- /backend/app.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const pino = require("pino-http"); 3 | const logger = require("morgan"); 4 | const session = require("express-session"); 5 | const passport = require("passport"); 6 | const helmet = require("helmet"); 7 | const compression = require("compression"); 8 | const redis = require("redis"); 9 | const connectRedis = require("connect-redis"); 10 | const crypto = require("crypto"); 11 | const path = require("path"); 12 | const cors = require("cors"); 13 | const rateLimit = require("express-rate-limit"); 14 | const cookieParser = require("cookie-parser"); 15 | const Sentry = require("@sentry/node"); 16 | const app = express(); 17 | 18 | Sentry.init({ dsn: process.env.SENTRY_DSN }); 19 | const RedisStore = connectRedis(session); 20 | const client = redis.createClient(process.env.REDIS_URL); 21 | 22 | app.use( 23 | process.env.NODE_ENV === "production" 24 | ? pino({ prettyPrint: process.env.NODE_ENV !== "production" }) 25 | : logger("dev") 26 | ); 27 | app.use((req, res, next) => { 28 | if (process.env.NODE_ENV !== "production") { 29 | req.log = {}; 30 | req.log.info = console.log; 31 | } 32 | 33 | next(); 34 | }); 35 | 36 | if (process.env.NODE_ENV === "production") { 37 | app.use(Sentry.Handlers.requestHandler()); 38 | app.use(Sentry.Handlers.errorHandler()); 39 | } 40 | app.use(helmet()); 41 | app.use(cors()); 42 | app.use(compression()); 43 | app.use(express.json()); 44 | app.use(express.urlencoded()); 45 | app.use(express.static(path.join(__dirname, "react_build"))); 46 | app.use(cookieParser()); 47 | // app.use( 48 | // rateLimit({ 49 | // windowMs: 1 * 60 * 1000, // 15 minutes 50 | // max: 30, // limit each IP to 100 requests per windowMs 51 | // handler: (req, res) => 52 | // res.json({ status: "error", message: "Your IP has been rate limited" }), 53 | // }) 54 | // ); 55 | app.use( 56 | session({ 57 | store: new RedisStore({ client }), 58 | secret: process.env.SECRET || crypto.randomBytes(20).toString("hex"), 59 | cookie: { 60 | secure: false, 61 | httpOnly: true, 62 | }, 63 | }) 64 | ); 65 | 66 | require("./config/passport"); 67 | app.use(passport.initialize()); 68 | app.use(passport.session()); 69 | 70 | app.use("/api", require("./api")); 71 | app.use("/", require("./backlinks")); 72 | 73 | app.get("*", (req, res) => 74 | res.sendFile(path.join(__dirname + "/react_build/index.html")) 75 | ); 76 | 77 | app.use((err, req, res, next) => { 78 | req.log.info(err); 79 | 80 | // Set statusCode to 500 if it isn't already there 81 | err.statusCode = err.statusCode || err.status || 500; 82 | err.message = err.message || err.name || "Internal Server Error"; 83 | err.code = err.code || err.name || "500_INTERNAL_SERVER_ERR"; 84 | 85 | res.locals.authenticated = req.isAuthenticated() || false; 86 | res.locals.user = req.user; 87 | res.json({ success: false, code: err.statusCode, message: err.message }); 88 | return; 89 | }); 90 | 91 | module.exports = app; 92 | -------------------------------------------------------------------------------- /backend/assets/WYkPG9E.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cryptichunt/platform/17b04d384aee97fe0f6745c9b4396d7a4f0ba1b9/backend/assets/WYkPG9E.png -------------------------------------------------------------------------------- /backend/assets/sme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cryptichunt/platform/17b04d384aee97fe0f6745c9b4396d7a4f0ba1b9/backend/assets/sme.png -------------------------------------------------------------------------------- /backend/backlinks.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const fs = require("fs"); 4 | 5 | router.get("/lethifold", (_, res) => 6 | res.redirect("https://i.imgur.com/VyK9oDn.png") 7 | ); 8 | 9 | router.get("/discord.gg", (_, res) => { 10 | res.writeHead(200, { 11 | "Content-Type": "image/png", 12 | "Content-Disposition": 'attachements; filename="WYkPG9E.png"', 13 | "X-PAY-ATTENTION": "WYkPG9E", 14 | }); 15 | fs.createReadStream("./assets/WYkPG9E.png").pipe(res); 16 | }); 17 | 18 | router.get("/slash", (_, res) => { 19 | res.writeHead(200, { 20 | "Content-Type": "image/png", 21 | }); 22 | fs.createReadStream("./assets/sme.png").pipe(res); 23 | }); 24 | 25 | router.get("/dbz", (_, res) => 26 | res.redirect("http://chall.cryptichunt.com:8080/fivethousand.tar.gz") 27 | ); 28 | 29 | router.use("/KILLSHOT", express.static("./killshot")); 30 | 31 | module.exports = router; 32 | -------------------------------------------------------------------------------- /backend/bin/serve: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | if (process.env.NODE_ENV !== "production") { 4 | require("dotenv").config(); 5 | } 6 | const app = require("../app"); 7 | const port = process.env.PORT || 3000; 8 | 9 | app.listen(port, () => console.log(`> Listening on http://localhost:${port}`)); 10 | -------------------------------------------------------------------------------- /backend/claudia.json: -------------------------------------------------------------------------------- 1 | { 2 | "lambda": { 3 | "role": "backend-executor", 4 | "name": "backend", 5 | "region": "ap-south-1" 6 | }, 7 | "api": { 8 | "id": "a92op9y6dj" 9 | } 10 | } -------------------------------------------------------------------------------- /backend/config/passport.js: -------------------------------------------------------------------------------- 1 | const passport = require("passport"); 2 | const bcrypt = require("bcrypt"); 3 | const { Strategy: LocalStrategy } = require("passport-local"); 4 | const { client } = require("../lib/prisma"); 5 | 6 | passport.use( 7 | new LocalStrategy( 8 | { usernameField: "email" }, 9 | async (email, password, done) => { 10 | try { 11 | const user = await client.user.findOne({ 12 | where: { email }, 13 | }); 14 | 15 | if (!user) { 16 | return done("Your account could not be found", null); 17 | } 18 | 19 | if (await bcrypt.compare(password, user.password)) { 20 | return done(null, user); 21 | } 22 | 23 | return done("Incorrect password"); 24 | } catch (e) { 25 | done(e); 26 | } 27 | } 28 | ) 29 | ); 30 | 31 | passport.serializeUser((user, done) => done(null, user.id)); 32 | passport.deserializeUser((token, done) => 33 | client.user 34 | .findOne({ where: { id: token } }) 35 | .then((u) => (u ? done(null, u) : done("User not found"))) 36 | .catch(done) 37 | ); 38 | -------------------------------------------------------------------------------- /backend/killshot/1bfc9850-32de6903d90ba13d6813.js: -------------------------------------------------------------------------------- 1 | (window.webpackJsonp=window.webpackJsonp||[]).push([[0],{ma3e:function(c,t,a){"use strict";a.d(t,"a",(function(){return s}));var n=a("Lnxd"),s=function(c){return Object(n.a)({tag:"svg",attr:{viewBox:"0 0 448 512"},child:[{tag:"path",attr:{d:"M297.216 243.2c0 15.616-11.52 28.416-26.112 28.416-14.336 0-26.112-12.8-26.112-28.416s11.52-28.416 26.112-28.416c14.592 0 26.112 12.8 26.112 28.416zm-119.552-28.416c-14.592 0-26.112 12.8-26.112 28.416s11.776 28.416 26.112 28.416c14.592 0 26.112-12.8 26.112-28.416.256-15.616-11.52-28.416-26.112-28.416zM448 52.736V512c-64.494-56.994-43.868-38.128-118.784-107.776l13.568 47.36H52.48C23.552 451.584 0 428.032 0 398.848V52.736C0 23.552 23.552 0 52.48 0h343.04C424.448 0 448 23.552 448 52.736zm-72.96 242.688c0-82.432-36.864-149.248-36.864-149.248-36.864-27.648-71.936-26.88-71.936-26.88l-3.584 4.096c43.52 13.312 63.744 32.512 63.744 32.512-60.811-33.329-132.244-33.335-191.232-7.424-9.472 4.352-15.104 7.424-15.104 7.424s21.248-20.224 67.328-33.536l-2.56-3.072s-35.072-.768-71.936 26.88c0 0-36.864 66.816-36.864 149.248 0 0 21.504 37.12 78.08 38.912 0 0 9.472-11.52 17.152-21.248-32.512-9.728-44.8-30.208-44.8-30.208 3.766 2.636 9.976 6.053 10.496 6.4 43.21 24.198 104.588 32.126 159.744 8.96 8.96-3.328 18.944-8.192 29.44-15.104 0 0-12.8 20.992-46.336 30.464 7.68 9.728 16.896 20.736 16.896 20.736 56.576-1.792 78.336-38.912 78.336-38.912z"}}]})(c)};s.displayName="FaDiscord"}}]); 2 | //# sourceMappingURL=1bfc9850-32de6903d90ba13d6813.js.map -------------------------------------------------------------------------------- /backend/killshot/95b64a6e-e953716b84c4d8bd38ef.js: -------------------------------------------------------------------------------- 1 | (window.webpackJsonp=window.webpackJsonp||[]).push([[1],{IdFE:function(t,c,a){"use strict";a.d(c,"a",(function(){return n})),a.d(c,"b",(function(){return o})),a.d(c,"c",(function(){return i})),a.d(c,"d",(function(){return e})),a.d(c,"e",(function(){return d}));var r=a("Lnxd"),n=function(t){return Object(r.a)({tag:"svg",attr:{viewBox:"0 0 512 512"},child:[{tag:"path",attr:{d:"M460.6 147.3L353 256.9c-.8.8-.8 2 0 2.8l75.3 80.2c5.1 5.1 5.1 13.3 0 18.4-2.5 2.5-5.9 3.8-9.2 3.8s-6.7-1.3-9.2-3.8l-75-79.9c-.8-.8-2.1-.8-2.9 0L313.7 297c-15.3 15.5-35.6 24.1-57.4 24.2-22.1.1-43.1-9.2-58.6-24.9l-17.6-17.9c-.8-.8-2.1-.8-2.9 0l-75 79.9c-2.5 2.5-5.9 3.8-9.2 3.8s-6.7-1.3-9.2-3.8c-5.1-5.1-5.1-13.3 0-18.4l75.3-80.2c.7-.8.7-2 0-2.8L51.4 147.3c-1.3-1.3-3.4-.4-3.4 1.4V368c0 17.6 14.4 32 32 32h352c17.6 0 32-14.4 32-32V148.7c0-1.8-2.2-2.6-3.4-1.4z"}},{tag:"path",attr:{d:"M256 295.1c14.8 0 28.7-5.8 39.1-16.4L452 119c-5.5-4.4-12.3-7-19.8-7H79.9c-7.5 0-14.4 2.6-19.8 7L217 278.7c10.3 10.5 24.2 16.4 39 16.4z"}}]})(t)};n.displayName="IoIosMail";var o=function(t){return Object(r.a)({tag:"svg",attr:{viewBox:"0 0 512 512"},child:[{tag:"path",attr:{d:"M426.8 64H85.2C73.5 64 64 73.5 64 85.2v341.6c0 11.7 9.5 21.2 21.2 21.2H256V296h-45.9v-56H256v-41.4c0-49.6 34.4-76.6 78.7-76.6 21.2 0 44 1.6 49.3 2.3v51.8h-35.3c-24.1 0-28.7 11.4-28.7 28.2V240h57.4l-7.5 56H320v152h106.8c11.7 0 21.2-9.5 21.2-21.2V85.2c0-11.7-9.5-21.2-21.2-21.2z"}}]})(t)};o.displayName="IoLogoFacebook";var i=function(t){return Object(r.a)({tag:"svg",attr:{viewBox:"0 0 512 512"},child:[{tag:"path",attr:{d:"M256 32C132.3 32 32 134.9 32 261.7c0 101.5 64.2 187.5 153.2 217.9 1.4.3 2.6.4 3.8.4 8.3 0 11.5-6.1 11.5-11.4 0-5.5-.2-19.9-.3-39.1-8.4 1.9-15.9 2.7-22.6 2.7-43.1 0-52.9-33.5-52.9-33.5-10.2-26.5-24.9-33.6-24.9-33.6-19.5-13.7-.1-14.1 1.4-14.1h.1c22.5 2 34.3 23.8 34.3 23.8 11.2 19.6 26.2 25.1 39.6 25.1 10.5 0 20-3.4 25.6-6 2-14.8 7.8-24.9 14.2-30.7-49.7-5.8-102-25.5-102-113.5 0-25.1 8.7-45.6 23-61.6-2.3-5.8-10-29.2 2.2-60.8 0 0 1.6-.5 5-.5 8.1 0 26.4 3.1 56.6 24.1 17.9-5.1 37-7.6 56.1-7.7 19 .1 38.2 2.6 56.1 7.7 30.2-21 48.5-24.1 56.6-24.1 3.4 0 5 .5 5 .5 12.2 31.6 4.5 55 2.2 60.8 14.3 16.1 23 36.6 23 61.6 0 88.2-52.4 107.6-102.3 113.3 8 7.1 15.2 21.1 15.2 42.5 0 30.7-.3 55.5-.3 63 0 5.4 3.1 11.5 11.4 11.5 1.2 0 2.6-.1 4-.4C415.9 449.2 480 363.1 480 261.7 480 134.9 379.7 32 256 32z"}}]})(t)};i.displayName="IoLogoGithub";var e=function(t){return Object(r.a)({tag:"svg",attr:{viewBox:"0 0 512 512"},child:[{tag:"path",attr:{d:"M336 96c21.2 0 41.3 8.4 56.5 23.5S416 154.8 416 176v160c0 21.2-8.4 41.3-23.5 56.5S357.2 416 336 416H176c-21.2 0-41.3-8.4-56.5-23.5S96 357.2 96 336V176c0-21.2 8.4-41.3 23.5-56.5S154.8 96 176 96h160m0-32H176c-61.6 0-112 50.4-112 112v160c0 61.6 50.4 112 112 112h160c61.6 0 112-50.4 112-112V176c0-61.6-50.4-112-112-112z"}},{tag:"path",attr:{d:"M360 176c-13.3 0-24-10.7-24-24s10.7-24 24-24c13.2 0 24 10.7 24 24s-10.8 24-24 24zM256 192c35.3 0 64 28.7 64 64s-28.7 64-64 64-64-28.7-64-64 28.7-64 64-64m0-32c-53 0-96 43-96 96s43 96 96 96 96-43 96-96-43-96-96-96z"}}]})(t)};e.displayName="IoLogoInstagram";var d=function(t){return Object(r.a)({tag:"svg",attr:{viewBox:"0 0 512 512"},child:[{tag:"path",attr:{d:"M492 109.5c-17.4 7.7-36 12.9-55.6 15.3 20-12 35.4-31 42.6-53.6-18.7 11.1-39.4 19.2-61.5 23.5C399.8 75.8 374.6 64 346.8 64c-53.5 0-96.8 43.4-96.8 96.9 0 7.6.8 15 2.5 22.1-80.5-4-151.9-42.6-199.6-101.3-8.3 14.3-13.1 31-13.1 48.7 0 33.6 17.2 63.3 43.2 80.7-16-.4-31-4.8-44-12.1v1.2c0 47 33.4 86.1 77.7 95-8.1 2.2-16.7 3.4-25.5 3.4-6.2 0-12.3-.6-18.2-1.8 12.3 38.5 48.1 66.5 90.5 67.3-33.1 26-74.9 41.5-120.3 41.5-7.8 0-15.5-.5-23.1-1.4C62.8 432 113.7 448 168.3 448 346.6 448 444 300.3 444 172.2c0-4.2-.1-8.4-.3-12.5C462.6 146 479 129 492 109.5z"}}]})(t)};d.displayName="IoLogoTwitter"}}]); 2 | //# sourceMappingURL=95b64a6e-e953716b84c4d8bd38ef.js.map -------------------------------------------------------------------------------- /backend/killshot/CNAME: -------------------------------------------------------------------------------- 1 | cryptichunt.com 2 | -------------------------------------------------------------------------------- /backend/killshot/chunk-map.json: -------------------------------------------------------------------------------- 1 | {"app":["/app-2971bdda35af8fc65532.js"],"component---src-pages-404-js":["/component---src-pages-404-js-fd850f06d3b7f74c62e0.js"],"component---src-pages-about-js":["/component---src-pages-about-js-3bb07278d80038d36196.js"],"component---src-pages-index-js":["/component---src-pages-index-js-2a49de6e7a758ff68c8e.js"]} -------------------------------------------------------------------------------- /backend/killshot/component---src-pages-404-js-fd850f06d3b7f74c62e0.js: -------------------------------------------------------------------------------- 1 | (window.webpackJsonp=window.webpackJsonp||[]).push([[5],{w2l6:function(e,n,t){"use strict";t.r(n);var a=t("q1tI"),l=t.n(a),r=t("Bl7J"),u=t("vrFN");n.default=function(){return l.a.createElement(r.a,null,l.a.createElement(u.a,{title:"404"}),l.a.createElement("h1",null,"404"),l.a.createElement("p",null,"Page Not Found"))}}}]); 2 | //# sourceMappingURL=component---src-pages-404-js-fd850f06d3b7f74c62e0.js.map -------------------------------------------------------------------------------- /backend/killshot/component---src-pages-404-js-fd850f06d3b7f74c62e0.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["webpack:///./src/pages/404.js"],"names":["NotFoundPage","title"],"mappings":"2FAAA,wDAaeA,UARM,kBACnB,kBAAC,IAAD,KACE,kBAAC,IAAD,CAAKC,MAAM,QACX,mCACA","file":"component---src-pages-404-js-fd850f06d3b7f74c62e0.js","sourcesContent":["import React from \"react\"\n\nimport Layout from \"../components/layout\"\nimport SEO from \"../components/seo\"\n\nconst NotFoundPage = () => (\n \n \n

404

\n

Page Not Found

\n
\n)\n\nexport default NotFoundPage\n"],"sourceRoot":""} -------------------------------------------------------------------------------- /backend/killshot/component---src-pages-about-js-3bb07278d80038d36196.js: -------------------------------------------------------------------------------- 1 | (window.webpackJsonp=window.webpackJsonp||[]).push([[6],{"3XHS":function(e,a,t){"use strict";t.r(a);t("f3/d");var n=t("q1tI"),l=t.n(n),o=t("Wbzz"),i=t("vOnD"),r=t("Bl7J"),m=t("vrFN"),c=[{name:"Angad Singh",post:"Priest",email:"mailto:mail@angad.dev"},{name:"Somesh Kar",post:"Dragon",email:"mailto:somesh.kar@gmail.com"},{name:"Inesh Tickoo",post:"Jester",email:"mailto:inesh.tickoo@gmail.com"},{name:"Ishir Bhardwaj",post:"Queen",email:"mailto:ishirbhardwaj@gmail.com"},{name:"Shashwat Mundra",post:"Executioner",email:"mailto:shashwat@cryptichunt.com"},{name:"Hitarth Khurana",post:"Knight",email:"mailto:hitarth2004@gmail.com"},{name:"Tarush Sonakya",post:"Herald",email:"mailto:tsonakya@protonmail.com"}],s=i.a.div.withConfig({displayName:"about__AboutContainer",componentId:"sc-1scvwxa-0"})(["display:flex;align-items:center;flex-wrap:wrap;"]),p=i.a.div.withConfig({displayName:"about__AboutBox",componentId:"sc-1scvwxa-1"})(["border-radius:5px;background-color:#2f3336;padding:20px 20px 30px 20px;margin:0 10px 10px 0;text-align:center;font-size:24px;position:relative;line-height:28px;color:#eee;flex:1;flex-basis:300px;&:hover{transform:translateY(-2px);}*{background-color:transparent;}main{font-size:19px;}i{font-size:18px;}main,i{display:inline;}"]),u=i.a.div.withConfig({displayName:"about__Title",componentId:"sc-1scvwxa-2"})(["font-size:24px;font-weight:600;margin:20px;color:#ee3769;background-color:transparent;"]),d=i.a.button.withConfig({displayName:"about__ButtonContact",componentId:"sc-1scvwxa-3"})(["padding:10px 20px;font-size:18px;margin-top:16px;border:0;border-radius:5px;font-weight:600;cursor:pointer;background:#ee3769;"]),h=i.a.div.withConfig({displayName:"about__About",componentId:"sc-1scvwxa-4"})(["padding:0;margin:0;"]);a.default=function(){return l.a.createElement(r.a,null,l.a.createElement(h,null,l.a.createElement(m.a,{title:"About"}),l.a.createElement("h1",null,"About"),l.a.createElement("p",null,"A cryptic hunt is an online multiplayer event that involves players having to follow a trail of leads and clues to get to a question's answer. These answers aren't directly available on the internet, and each question generally takes a great deal of patience and mental prowess to solve."),l.a.createElement("p",null,l.a.createElement("b",null,l.a.createElement(o.Link,{to:"/format"},"Click here"))," ","for more details reagrding the event."),l.a.createElement("h1",null,"Prizes"),l.a.createElement("p",null,l.a.createElement("b",null,"1st -")," ₹20,000",l.a.createElement("br",null),l.a.createElement("b",null,"2nd -")," ₹15,000",l.a.createElement("br",null),l.a.createElement("b",null,"3rd -")," ₹10,000",l.a.createElement("br",null),l.a.createElement("b",null,"4th to 15th -")," ₹1,000 each",l.a.createElement("br",null)),l.a.createElement("p",null,"In addition to this, everyone in the ",l.a.createElement("b",null,"top 100")," will receive a"," ",l.a.createElement("b",null,".co domain")," of their choosing. Everyone in the ",l.a.createElement("b",null,"top 15")," will also receive an ",l.a.createElement("b",null,"additional .us domain")," along with their .co domain. Premium domains are not permitted."),l.a.createElement("h1",null,"Members"),l.a.createElement(s,null,c.map((function(e){var a=e.name,t=e.post,n=e.email;return l.a.createElement(p,{key:a},l.a.createElement(u,null,a),l.a.createElement("div",null,l.a.createElement("main",{style:{fontSize:"22px"}},"Organiser"),l.a.createElement("br",null),l.a.createElement("i",null,t)," ",l.a.createElement("br",null),l.a.createElement("a",{href:n},l.a.createElement(d,null,"Contact"))))})))))}}}]); 2 | //# sourceMappingURL=component---src-pages-about-js-3bb07278d80038d36196.js.map -------------------------------------------------------------------------------- /backend/killshot/component---src-pages-about-js-3bb07278d80038d36196.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["webpack:///./data/memberInfo.js","webpack:///./src/pages/about.js"],"names":["name","post","email","AboutContainer","styled","div","AboutBox","Title","ButtonContact","button","About","AboutPage","title","to","Member","map","key","style","fontSize","href"],"mappings":"uLAAe,GACb,CACEA,KAAM,cACNC,KAAM,SACNC,MAAO,yBAET,CACEF,KAAM,aACNC,KAAM,SACNC,MAAO,+BAET,CACEF,KAAM,eACNC,KAAM,SACNC,MAAO,iCAET,CACEF,KAAM,iBACNC,KAAM,QACNC,MAAO,kCAET,CACEF,KAAM,kBACNC,KAAM,cACNC,MAAO,mCAET,CACEF,KAAM,kBACNC,KAAM,SACNC,MAAO,gCAET,CACEF,KAAM,iBACNC,KAAM,SACNC,MAAO,mCC1BLC,EAAiBC,IAAOC,IAAV,4EAAGD,CAAH,qDAMdE,EAAWF,IAAOC,IAAV,sEAAGD,CAAH,2UA8BRG,EAAQH,IAAOC,IAAV,mEAAGD,CAAH,4FAQLI,EAAgBJ,IAAOK,OAAV,2EAAGL,CAAH,oIAWbM,EAAQN,IAAOC,IAAV,mEAAGD,CAAH,yBA4DIO,UAvDG,kBAChB,kBAAC,IAAD,KACE,kBAACD,EAAD,KACE,kBAAC,IAAD,CAAKE,MAAM,UACX,qCACA,8TAOA,2BACE,2BACE,kBAAC,OAAD,CAAMC,GAAG,WAAT,eACG,IAHP,yCAMA,sCACA,2BACE,oCADF,WAEE,6BACA,oCAHF,WAIE,6BACA,oCALF,WAME,6BACA,4CAPF,eAQE,8BAEF,mEACuC,sCADvC,kBACqE,IACnE,yCAFF,uCAEuD,qCAFvD,yBAGkB,oDAHlB,oEAMA,uCACA,kBAACV,EAAD,KACGW,EAAOC,KAAI,gBAAGf,EAAH,EAAGA,KAAMC,EAAT,EAASA,KAAMC,EAAf,EAAeA,MAAf,OACV,kBAACI,EAAD,CAAUU,IAAKhB,GACb,kBAACO,EAAD,KAAQP,GACR,6BACE,0BAAMiB,MAAO,CAAEC,SAAU,SAAzB,aACA,6BACA,2BAAIjB,GAHN,IAGgB,6BACd,uBAAGkB,KAAMjB,GACP,kBAACM,EAAD","file":"component---src-pages-about-js-3bb07278d80038d36196.js","sourcesContent":["export default [\n {\n name: \"Angad Singh\",\n post: \"Priest\",\n email: \"mailto:mail@angad.dev\",\n },\n {\n name: \"Somesh Kar\",\n post: \"Dragon\",\n email: \"mailto:somesh.kar@gmail.com\",\n },\n {\n name: \"Inesh Tickoo\",\n post: \"Jester\",\n email: \"mailto:inesh.tickoo@gmail.com\",\n },\n {\n name: \"Ishir Bhardwaj\",\n post: \"Queen\",\n email: \"mailto:ishirbhardwaj@gmail.com\",\n },\n {\n name: \"Shashwat Mundra\",\n post: \"Executioner\",\n email: \"mailto:shashwat@cryptichunt.com\",\n },\n {\n name: \"Hitarth Khurana\",\n post: \"Knight\",\n email: \"mailto:hitarth2004@gmail.com\",\n },\n {\n name: \"Tarush Sonakya\",\n post: \"Herald\",\n email: \"mailto:tsonakya@protonmail.com\",\n },\n]\n","import React from 'react'\nimport { Link } from 'gatsby'\nimport styled from 'styled-components'\n\nimport Layout from '../components/layout'\nimport SEO from '../components/seo'\nimport Member from '../../data/memberInfo'\n\nconst AboutContainer = styled.div`\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n`\n\nconst AboutBox = styled.div`\n border-radius: 5px;\n background-color: #2f3336;\n padding: 20px 20px 30px 20px;\n margin: 0 10px 10px 0;\n text-align: center;\n font-size: 24px;\n position: relative;\n line-height: 28px;\n color: #eee;\n flex: 1;\n flex-basis: 300px;\n &:hover {\n transform: translateY(-2px);\n }\n * {\n background-color: transparent;\n }\n main {\n font-size: 19px;\n }\n i {\n font-size: 18px;\n }\n main,\n i {\n display: inline;\n }\n`\n\nconst Title = styled.div`\n font-size: 24px;\n font-weight: 600;\n margin: 20px;\n color: #ee3769;\n background-color: transparent;\n`\n\nconst ButtonContact = styled.button`\n padding: 10px 20px;\n font-size: 18px;\n margin-top: 16px;\n border: 0;\n border-radius: 5px;\n font-weight: 600;\n cursor: pointer;\n background: #ee3769;\n`\n\nconst About = styled.div`\n padding: 0;\n margin: 0;\n`\n\nconst AboutPage = () => (\n \n \n \n

About

\n

\n A cryptic hunt is an online multiplayer event that involves players\n having to follow a trail of leads and clues to get to a question's\n answer. These answers aren't directly available on the internet, and\n each question generally takes a great deal of patience and mental\n prowess to solve.\n

\n

\n \n Click here\n {' '}\n for more details reagrding the event.\n

\n

Prizes

\n

\n 1st - ₹20,000\n
\n 2nd - ₹15,000\n
\n 3rd - ₹10,000\n
\n 4th to 15th - ₹1,000 each\n
\n

\n

\n In addition to this, everyone in the top 100 will receive a{' '}\n .co domain of their choosing. Everyone in the top 15 will\n also receive an additional .us domain along with their .co\n domain. Premium domains are not permitted.\n

\n

Members

\n \n {Member.map(({ name, post, email }) => (\n \n {name}\n
\n
Organiser
\n
\n {post}
\n \n Contact\n \n
\n
\n ))}\n
\n
\n
\n)\n\nexport default AboutPage\n"],"sourceRoot":""} -------------------------------------------------------------------------------- /backend/killshot/component---src-pages-index-js-2a49de6e7a758ff68c8e.js: -------------------------------------------------------------------------------- 1 | (window.webpackJsonp=window.webpackJsonp||[]).push([[7],{RXBc:function(e,t,a){"use strict";a.r(t);var r=a("q1tI"),n=a.n(r),i=a("vOnD"),o=a("Bl7J"),l=a("vrFN"),c=i.a.div.withConfig({displayName:"pages__Discord",componentId:"sc-1r55gkc-0"})(["top:31vh;right:15%;position:absolute;background-color:#2f3336;width:420px;border-radius:5px;padding:0 0 4px 0;margin:0;p{margin:25px 30px 20px 30px;}@media screen and (max-width:1440px){top:33vh;width:350px;}@media screen and (max-width:1200px){right:10%;}@media screen and (max-width:1080px){right:5%;}@media screen and (max-width:960px){position:static;padding-top:8px;width:100%;height:50%;}"]),p=i.a.div.withConfig({displayName:"pages__Icon",componentId:"sc-1r55gkc-1"})(["background-color:#ee3769;border-radius:50%;margin:30px;height:75px;width:75px;"]),d=i.a.button.withConfig({displayName:"pages__ButtonDiscord",componentId:"sc-1r55gkc-2"})(["background:#ee3769;color:#eee;display:block;border-radius:5px;padding:2% 20%;margin:15px 30px 30px 30px;border:solid #ee3769;font-weight:600;box-shadow:0px 8px 32px #ee376920;white-space:nowrap;&:hover{cursor:pointer;box-shadow:0px 8px 32px #ee376970;transform:translateY(-2px);}@media screen and (max-width:1200px){right:10%;}@media screen and (max-width:960px){padding:2% 15%;}@media screen and (max-width:280px){padding:4% 10%;}"]),s=i.a.div.withConfig({displayName:"pages__MainText",componentId:"sc-1r55gkc-3"})(["width:40%;@media screen and (max-width:960px){width:100%;}"]);t.default=function(){return n.a.createElement(o.a,null,n.a.createElement(l.a,{title:"Home"}),n.a.createElement("h1",null,"Cryptocracy"),n.a.createElement(s,null,n.a.createElement("p",null,"The hunt is now live! Click"," ",n.a.createElement("b",null,n.a.createElement("a",{href:"https://play.cryptichunt.com",target:"_blank",rel:"noreferrer"},"here"))," ","to play!"),n.a.createElement("p",null,"A cryptic hunt organised by high school students from Delhi."),n.a.createElement("p",null,"Join our Discord to stay updated. Registrations are now live, register now!"),n.a.createElement("p",null,"Check out the"," ",n.a.createElement("b",null,n.a.createElement("a",{href:"https://www.instagram.com/p/CBWK36Hl995/?igshid=kws9jeec5w0j",target:"_blank",rel:"noreferrer"},"prizes here!"))," ","We are officialy sponsored by"," ",n.a.createElement("b",null,n.a.createElement("a",{href:"https://www.ivyachievement.com/",target:"_blank",rel:"noreferrer"},"IvyAchievement."))),n.a.createElement("p",null,n.a.createElement("b",null,"Dates:")," 26th June 12:00 AM IST - 27th June 11:59 PM IST 2020.")),n.a.createElement(c,null,n.a.createElement(p,null),n.a.createElement("p",null,"You're invited to join our Discord server!"),n.a.createElement("p",{style:{fontSize:"0.99em",opacity:"70%",marginTop:"0"}},"Join our Discord server! It's the place where we will be releasing hints, confirming leads, and posting all hunt related content. Some levels will require you to contact members of our team on Discord."),n.a.createElement("a",{href:"https://discord.com/invite/C5Y2CQ5",target:"_blank",rel:"noreferrer"},n.a.createElement(d,null,"Join Server"))))}}}]); 2 | //# sourceMappingURL=component---src-pages-index-js-2a49de6e7a758ff68c8e.js.map -------------------------------------------------------------------------------- /backend/killshot/component---src-pages-index-js-2a49de6e7a758ff68c8e.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["webpack:///./src/pages/index.js"],"names":["Discord","styled","div","Icon","ButtonDiscord","button","MainText","IndexPage","title","href","target","rel","style","fontSize","opacity","marginTop"],"mappings":"2FAAA,oEAMMA,EAAUC,IAAOC,IAAV,qEAAGD,CAAH,gZA8BPE,EAAOF,IAAOC,IAAV,kEAAGD,CAAH,oFAQJG,EAAgBH,IAAOI,OAAV,2EAAGJ,CAAH,qbA2BbK,EAAWL,IAAOC,IAAV,sEAAGD,CAAH,gEA2ECM,UApEG,kBAChB,kBAAC,IAAD,KACE,kBAAC,IAAD,CAAKC,MAAM,SACX,2CACA,kBAACF,EAAD,KACE,yDAC8B,IAC5B,2BACE,uBACEG,KAAK,+BACLC,OAAO,SACPC,IAAI,cAHN,SAOG,IAVP,YAaA,2FACA,0GAIA,2CACgB,IACd,2BACE,uBACEF,KAAK,+DACLC,OAAO,SACPC,IAAI,cAHN,iBAOG,IAVP,gCAWgC,IAC9B,2BACE,uBACEF,KAAK,kCACLC,OAAO,SACPC,IAAI,cAHN,qBASJ,2BACE,qCADF,2DAIF,kBAACX,EAAD,KACE,kBAACG,EAAD,MACA,yEACA,uBAAGS,MAAO,CAAEC,SAAU,SAAUC,QAAS,MAAOC,UAAW,MAA3D,6MAKA,uBACEN,KAAK,qCACLC,OAAO,SACPC,IAAI,cAEJ,kBAACP,EAAD","file":"component---src-pages-index-js-2a49de6e7a758ff68c8e.js","sourcesContent":["import React from 'react'\nimport styled from 'styled-components'\n\nimport Layout from '../components/layout'\nimport SEO from '../components/seo'\n\nconst Discord = styled.div`\n top: 31vh;\n right: 15%;\n position: absolute;\n background-color: #2f3336;\n width: 420px;\n border-radius: 5px;\n padding: 0 0 4px 0;\n margin: 0;\n p {\n margin: 25px 30px 20px 30px;\n }\n @media screen and (max-width: 1440px) {\n top: 33vh;\n width: 350px;\n }\n @media screen and (max-width: 1200px) {\n right: 10%;\n }\n @media screen and (max-width: 1080px) {\n right: 5%;\n }\n @media screen and (max-width: 960px) {\n position: static;\n padding-top: 8px;\n width: 100%;\n height: 50%;\n }\n`\n\nconst Icon = styled.div`\n background-color: #ee3769;\n border-radius: 50%;\n margin: 30px;\n height: 75px;\n width: 75px;\n`\n\nconst ButtonDiscord = styled.button`\n background: #ee3769;\n color: #eee;\n display: block;\n border-radius: 5px;\n padding: 2% 20%;\n margin: 15px 30px 30px 30px;\n border: solid #ee3769;\n font-weight: 600;\n box-shadow: 0px 8px 32px #ee376920;\n white-space: nowrap;\n &:hover {\n cursor: pointer;\n box-shadow: 0px 8px 32px #ee376970;\n transform: translateY(-2px);\n }\n @media screen and (max-width: 1200px) {\n right: 10%;\n }\n @media screen and (max-width: 960px) {\n padding: 2% 15%;\n }\n @media screen and (max-width: 280px) {\n padding: 4% 10%;\n }\n`\n\nconst MainText = styled.div`\n width: 40%;\n @media screen and (max-width: 960px) {\n width: 100%;\n }\n`\n\nconst IndexPage = () => (\n \n \n

Cryptocracy

\n \n

\n The hunt is now live! Click{' '}\n \n \n here\n \n {' '}\n to play!\n

\n

A cryptic hunt organised by high school students from Delhi.

\n

\n Join our Discord to stay updated. Registrations are now live, register\n now!\n

\n

\n Check out the{' '}\n \n \n prizes here!\n \n {' '}\n We are officialy sponsored by{' '}\n \n \n IvyAchievement.\n \n \n

\n

\n Dates: 26th June 12:00 AM IST - 27th June 11:59 PM IST 2020.\n

\n
\n \n \n

You're invited to join our Discord server!

\n

\n Join our Discord server! It's the place where we will be releasing\n hints, confirming leads, and posting all hunt related content. Some\n levels will require you to contact members of our team on Discord.\n

\n \n Join Server\n \n
\n
\n)\n\nexport default IndexPage\n"],"sourceRoot":""} -------------------------------------------------------------------------------- /backend/killshot/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cryptichunt/platform/17b04d384aee97fe0f6745c9b4396d7a4f0ba1b9/backend/killshot/favicon-32x32.png -------------------------------------------------------------------------------- /backend/killshot/icons/icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cryptichunt/platform/17b04d384aee97fe0f6745c9b4396d7a4f0ba1b9/backend/killshot/icons/icon-144x144.png -------------------------------------------------------------------------------- /backend/killshot/icons/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cryptichunt/platform/17b04d384aee97fe0f6745c9b4396d7a4f0ba1b9/backend/killshot/icons/icon-192x192.png -------------------------------------------------------------------------------- /backend/killshot/icons/icon-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cryptichunt/platform/17b04d384aee97fe0f6745c9b4396d7a4f0ba1b9/backend/killshot/icons/icon-256x256.png -------------------------------------------------------------------------------- /backend/killshot/icons/icon-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cryptichunt/platform/17b04d384aee97fe0f6745c9b4396d7a4f0ba1b9/backend/killshot/icons/icon-384x384.png -------------------------------------------------------------------------------- /backend/killshot/icons/icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cryptichunt/platform/17b04d384aee97fe0f6745c9b4396d7a4f0ba1b9/backend/killshot/icons/icon-48x48.png -------------------------------------------------------------------------------- /backend/killshot/icons/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cryptichunt/platform/17b04d384aee97fe0f6745c9b4396d7a4f0ba1b9/backend/killshot/icons/icon-512x512.png -------------------------------------------------------------------------------- /backend/killshot/icons/icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cryptichunt/platform/17b04d384aee97fe0f6745c9b4396d7a4f0ba1b9/backend/killshot/icons/icon-72x72.png -------------------------------------------------------------------------------- /backend/killshot/icons/icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cryptichunt/platform/17b04d384aee97fe0f6745c9b4396d7a4f0ba1b9/backend/killshot/icons/icon-96x96.png -------------------------------------------------------------------------------- /backend/killshot/manifest.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"gatsby-starter-default","short_name":"starter","start_url":"/KILLSHOT/","background_color":"#23272A","theme_color":"#23272A","display":"minimal-ui","cacheDigest":"0e3de589b23119301420abdadcd80e5c","icons":[{"src":"/KILLSHOT/icons/icon-48x48.png?v=0e3de589b23119301420abdadcd80e5c","sizes":"48x48","type":"image/png"},{"src":"/KILLSHOT/icons/icon-72x72.png?v=0e3de589b23119301420abdadcd80e5c","sizes":"72x72","type":"image/png"},{"src":"/KILLSHOT/icons/icon-96x96.png?v=0e3de589b23119301420abdadcd80e5c","sizes":"96x96","type":"image/png"},{"src":"/KILLSHOT/icons/icon-144x144.png?v=0e3de589b23119301420abdadcd80e5c","sizes":"144x144","type":"image/png"},{"src":"/KILLSHOT/icons/icon-192x192.png?v=0e3de589b23119301420abdadcd80e5c","sizes":"192x192","type":"image/png"},{"src":"/KILLSHOT/icons/icon-256x256.png?v=0e3de589b23119301420abdadcd80e5c","sizes":"256x256","type":"image/png"},{"src":"/KILLSHOT/icons/icon-384x384.png?v=0e3de589b23119301420abdadcd80e5c","sizes":"384x384","type":"image/png"},{"src":"/KILLSHOT/icons/icon-512x512.png?v=0e3de589b23119301420abdadcd80e5c","sizes":"512x512","type":"image/png"}]} -------------------------------------------------------------------------------- /backend/killshot/page-data/404.html/page-data.json: -------------------------------------------------------------------------------- 1 | {"componentChunkName":"component---src-pages-404-js","path":"/404.html","result":{"pageContext":{}}} -------------------------------------------------------------------------------- /backend/killshot/page-data/404/page-data.json: -------------------------------------------------------------------------------- 1 | {"componentChunkName":"component---src-pages-404-js","path":"/404/","result":{"pageContext":{}}} -------------------------------------------------------------------------------- /backend/killshot/page-data/about/page-data.json: -------------------------------------------------------------------------------- 1 | {"componentChunkName":"component---src-pages-about-js","path":"/about/","result":{"pageContext":{}}} -------------------------------------------------------------------------------- /backend/killshot/page-data/app-data.json: -------------------------------------------------------------------------------- 1 | {"webpackCompilationHash":"132e3cdfab34334df274"} 2 | -------------------------------------------------------------------------------- /backend/killshot/page-data/index/page-data.json: -------------------------------------------------------------------------------- 1 | {"componentChunkName":"component---src-pages-index-js","path":"/","result":{"pageContext":{}}} -------------------------------------------------------------------------------- /backend/killshot/static/6d91c86c0fde632ba4cd01062fd9ccfa/2a4de/gatsby-astronaut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cryptichunt/platform/17b04d384aee97fe0f6745c9b4396d7a4f0ba1b9/backend/killshot/static/6d91c86c0fde632ba4cd01062fd9ccfa/2a4de/gatsby-astronaut.png -------------------------------------------------------------------------------- /backend/killshot/static/6d91c86c0fde632ba4cd01062fd9ccfa/5db04/gatsby-astronaut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cryptichunt/platform/17b04d384aee97fe0f6745c9b4396d7a4f0ba1b9/backend/killshot/static/6d91c86c0fde632ba4cd01062fd9ccfa/5db04/gatsby-astronaut.png -------------------------------------------------------------------------------- /backend/killshot/static/6d91c86c0fde632ba4cd01062fd9ccfa/62b1f/gatsby-astronaut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cryptichunt/platform/17b04d384aee97fe0f6745c9b4396d7a4f0ba1b9/backend/killshot/static/6d91c86c0fde632ba4cd01062fd9ccfa/62b1f/gatsby-astronaut.png -------------------------------------------------------------------------------- /backend/killshot/static/6d91c86c0fde632ba4cd01062fd9ccfa/630fb/gatsby-astronaut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cryptichunt/platform/17b04d384aee97fe0f6745c9b4396d7a4f0ba1b9/backend/killshot/static/6d91c86c0fde632ba4cd01062fd9ccfa/630fb/gatsby-astronaut.png -------------------------------------------------------------------------------- /backend/killshot/static/6d91c86c0fde632ba4cd01062fd9ccfa/6d161/gatsby-astronaut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cryptichunt/platform/17b04d384aee97fe0f6745c9b4396d7a4f0ba1b9/backend/killshot/static/6d91c86c0fde632ba4cd01062fd9ccfa/6d161/gatsby-astronaut.png -------------------------------------------------------------------------------- /backend/killshot/static/6d91c86c0fde632ba4cd01062fd9ccfa/ee604/gatsby-astronaut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cryptichunt/platform/17b04d384aee97fe0f6745c9b4396d7a4f0ba1b9/backend/killshot/static/6d91c86c0fde632ba4cd01062fd9ccfa/ee604/gatsby-astronaut.png -------------------------------------------------------------------------------- /backend/killshot/static/d/2417117884.json: -------------------------------------------------------------------------------- 1 | {"data":{"site":{"siteMetadata":{"title":"Cryptocracy","description":"An online cryptic hunt organised by high-school students from New Delhi, India.","author":"@cryptocracy"}}}} -------------------------------------------------------------------------------- /backend/killshot/static/d/2969191536.json: -------------------------------------------------------------------------------- 1 | {"data":{"placeholderImage":{"childImageSharp":{"fluid":{"base64":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64paAAAACXBIWXMAAAsSAAALEgHS3X78AAACYklEQVQ4y42Uy28SQRjA+dM8efDmwYN6qF6qiSY+Y/WgQRMibY00TaWNBSRSCraYQtHl/bR0KyxQWCgWWAqU8izl/Sq7rLNsRHlVJpvJtzPfb77nDIOcZHSoqZSrp4+KtXIziubaLRysMCZiCYqOoVnhjNEi8RcztdxxeTzc6VBfT+5O2Vhpb+vw4wMdZ0ppWvP9xzLeJoDNThf2W+Nz1+XzNxQubSToSKKW+BDc+WOnkshhSVgeCiGpViZMEg1oxc26Knt+ae3bEtJTZwzE1kXLccG0+sOOlrcvZXvsczPkITfsa20vwIKnhsh+BnjUarT74Gb13CY7KBVJMv3z4N1NszQYsMWM62HNrCis/GxXn0iYls23uz5LPBcv0bH8hUH2XRoM85ySXv7JBtO87jMIvWq+H5GoYIHCLA1ZxD6Qap3Ak8IKfW7TJ50lK6uP9E6RgndHaODtCJ6Z5RyHfnE7j6gRbcKlCYNSt+rtETHTpUGgEP8FYmdNqd/Mo7aiVWTfuH2L9xASvfxxlqr01EYkrJszvNkgW9bH0OuFr+99m+y9IOeyU6zIp/Hubp/yMEztlzFPwOhdvq+nIoS1JNn4t2sugCmVsDvPe2KKolnZLCxhOcAKQRDDXTQaVi46lqYhIBwHTrl3oWqhMRDtaJge37lOBMKo4tfbqhVX0J7snTsWps8uZWuoOQY6CcjpSIF55UvmqNgr5wUwtV1IVdnXtnSfPEB2qjDNqnvczRl0m+j6Jn5lXb6nAQJqinmN0ZEBj03YLzghY8PnTRz80o/GRJZpOLCb0PM9BN7pvUEjx28V00WUg9jIVwAAAABJRU5ErkJggg==","aspectRatio":1,"src":"/KILLSHOT/static/6d91c86c0fde632ba4cd01062fd9ccfa/630fb/gatsby-astronaut.png","srcSet":"/KILLSHOT/static/6d91c86c0fde632ba4cd01062fd9ccfa/5db04/gatsby-astronaut.png 75w,\n/KILLSHOT/static/6d91c86c0fde632ba4cd01062fd9ccfa/6d161/gatsby-astronaut.png 150w,\n/KILLSHOT/static/6d91c86c0fde632ba4cd01062fd9ccfa/630fb/gatsby-astronaut.png 300w,\n/KILLSHOT/static/6d91c86c0fde632ba4cd01062fd9ccfa/62b1f/gatsby-astronaut.png 450w,\n/KILLSHOT/static/6d91c86c0fde632ba4cd01062fd9ccfa/2a4de/gatsby-astronaut.png 600w,\n/KILLSHOT/static/6d91c86c0fde632ba4cd01062fd9ccfa/ee604/gatsby-astronaut.png 800w","sizes":"(max-width: 300px) 100vw, 300px"}}}}} -------------------------------------------------------------------------------- /backend/killshot/static/d/856328897.json: -------------------------------------------------------------------------------- 1 | {"data":{"site":{"siteMetadata":{"title":"Cryptocracy"}}}} -------------------------------------------------------------------------------- /backend/killshot/styles-dd3841a4888192e20843.js: -------------------------------------------------------------------------------- 1 | (window.webpackJsonp=window.webpackJsonp||[]).push([[3],[]]); 2 | //# sourceMappingURL=styles-dd3841a4888192e20843.js.map -------------------------------------------------------------------------------- /backend/killshot/styles-dd3841a4888192e20843.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":[],"names":[],"mappings":"","file":"styles-dd3841a4888192e20843.js","sourceRoot":""} -------------------------------------------------------------------------------- /backend/killshot/styles.4a6afaa3d5790f28f7bd.css: -------------------------------------------------------------------------------- 1 | @import url(https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap);*{background-color:#23272a;color:#eee;font-family:Montserrat,sans-serif;transition:all .5s ease}html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}article,aside,details,figcaption,figure,footer,header,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block}audio:not([controls]){display:none;height:0}progress{vertical-align:baseline}[hidden],template{display:none}a{background-color:transparent;-webkit-text-decoration-skip:objects}a:active,a:hover{outline-width:0}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:inherit;font-weight:bolder}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background-color:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}svg:not(:root){overflow:hidden}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}figure{margin:1em 40px}hr{box-sizing:content-box;height:0;overflow:visible}button,input,optgroup,select,textarea{font:inherit;margin:0}optgroup{font-weight:700}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-input-placeholder{color:inherit;opacity:.54}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}html{font:112.5%/1.45em georgia,serif;box-sizing:border-box;overflow-y:scroll}*,:after,:before{box-sizing:inherit}body{color:rgba(0,0,0,.8);font-weight:400;word-wrap:break-word;-webkit-font-kerning:normal;font-kerning:normal;-ms-font-feature-settings:"kern","liga","clig","calt";font-feature-settings:"kern","liga","clig","calt"}img{max-width:100%;padding:0;margin:0 0 1.45rem}h1{font-size:2.25rem}h1,h2{padding:0;margin:0 0 1.45rem;color:inherit;font-weight:700;text-rendering:optimizeLegibility;line-height:1.1}h2{font-size:1.62671rem}h3{font-size:1.38316rem}h3,h4{padding:0;margin:0 0 1.45rem;color:inherit;font-weight:700;text-rendering:optimizeLegibility;line-height:1.1}h4{font-size:1rem}h5{font-size:.85028rem}h5,h6{padding:0;margin:0 0 1.45rem;color:inherit;font-weight:700;text-rendering:optimizeLegibility;line-height:1.1}h6{font-size:.78405rem}hgroup{padding:0;margin:0 0 1.45rem}ol,ul{padding:0;margin:0 0 1.45rem 1.45rem;list-style-position:outside;list-style-image:none}dd,dl,figure,p{padding:0;margin:0 0 1.45rem}pre{margin:0 0 1.45rem;font-size:.85rem;line-height:1.42;background:rgba(0,0,0,.04);border-radius:3px;overflow:auto;word-wrap:normal;padding:1.45rem}table{font-size:1rem;line-height:1.45rem;border-collapse:collapse;width:100%}fieldset,table{padding:0;margin:0 0 1.45rem}blockquote{padding:0;margin:0 1.45rem 1.45rem}form,iframe,noscript{padding:0;margin:0 0 1.45rem}hr{padding:0;margin:0 0 calc(1.45rem - 1px);background:rgba(0,0,0,.2);border:none;height:1px}address{padding:0;margin:0 0 1.45rem}b,dt,strong,th{font-weight:700}li{margin-bottom:.725rem}ol li,ul li{padding-left:0}li>ol,li>ul{margin-left:1.45rem;margin-bottom:.725rem;margin-top:.725rem}blockquote :last-child,li :last-child,p :last-child{margin-bottom:0}li>p{margin-bottom:.725rem}code,kbd,samp{font-size:.85rem;line-height:1.45rem}abbr,abbr[title],acronym{border-bottom:1px dotted rgba(0,0,0,.5);cursor:help}abbr[title]{text-decoration:none}td,th,thead{text-align:left}td,th{border-bottom:1px solid rgba(0,0,0,.12);font-feature-settings:"tnum";-moz-font-feature-settings:"tnum";-ms-font-feature-settings:"tnum";-webkit-font-feature-settings:"tnum";padding:.725rem .96667rem calc(.725rem - 1px)}td:first-child,th:first-child{padding-left:0}td:last-child,th:last-child{padding-right:0}code,tt{background-color:rgba(0,0,0,.04);border-radius:3px;padding:.2em 0}pre code{background:none;line-height:1.42}code:after,code:before,tt:after,tt:before{letter-spacing:-.2em;content:" "}pre code:after,pre code:before,pre tt:after,pre tt:before{content:""}@media only screen and (max-width:480px){html{font-size:100%}} -------------------------------------------------------------------------------- /backend/killshot/webpack-runtime-611b74ddb81015b1fa4d.js: -------------------------------------------------------------------------------- 1 | !function(e){function t(t){for(var n,o,u=t[0],s=t[1],d=t[2],f=0,l=[];f awsServerlessExpress.proxy(server, event, context) 15 | -------------------------------------------------------------------------------- /backend/lib/action-from-user-state.js: -------------------------------------------------------------------------------- 1 | const { client } = require("./prisma"); 2 | const logs = require("./logs"); 3 | 4 | module.exports = { 5 | "go-moveable": (user) => async (req, res, next) => 6 | res.json({ success: true, user, message: "You landed on the Go tile!" }), 7 | "story-moveable": (user, ct) => async (req, res, next) => 8 | res.json({ success: true, message: "You landed on a story tile! ", user }), 9 | "levelsolved-moveable": (user, ct) => async (req, res, next) => { 10 | try { 11 | const lvl = ct.tile.levelId 12 | ? await client.userLevel.findOne({ 13 | where: { id: ct.tile.levelId, userId: req.user.id }, 14 | }) 15 | : null; 16 | 17 | if (!lvl) { 18 | return res.json({ success: false, message: "No such level", user }); 19 | } 20 | 21 | res.json({ 22 | success: true, 23 | message: "You've already solved this level", 24 | user, 25 | }); 26 | } catch (e) { 27 | return next(e); 28 | } 29 | }, 30 | "randchance-moveable": (user, ct) => async (req, res, next) => { 31 | try { 32 | const types = { 33 | JAIL: async () => { 34 | await client.user.update({ 35 | where: { id: req.user.id }, 36 | data: { 37 | incarcerated: true, 38 | incarceratedAt: new Date(), 39 | currentTile: { connect: { id: 12 } }, 40 | }, 41 | }); 42 | 43 | await client.visitedTile.create({ 44 | data: { 45 | tile: { connect: { id: 12 } }, 46 | user: { connect: { id: req.user.id } }, 47 | }, 48 | }); 49 | 50 | return res.json({ 51 | success: true, 52 | message: 53 | "You were caught forging your job contracts and are now in jail", 54 | user, 55 | }); 56 | }, 57 | BRIBE: async () => { 58 | await client.user.update({ 59 | where: { id: user.id }, 60 | data: { points: user.points - 50 }, 61 | }); 62 | 63 | return res.json({ 64 | success: true, 65 | message: 66 | "You had to bribe a toll guard to let you cross into enemy territory without travel documents", 67 | user, 68 | }); 69 | }, 70 | BOUNTY: async () => { 71 | await client.user.update({ 72 | where: { id: user.id }, 73 | data: { points: user.points + 50 }, 74 | }); 75 | 76 | return res.json({ 77 | success: true, 78 | message: "You were given a bounty by your employer", 79 | user, 80 | }); 81 | }, 82 | }; 83 | 84 | console.log({ rc: ct.randomChanceType }); 85 | types[ct.randomChanceType](); 86 | } catch (e) { 87 | next(e); 88 | } 89 | }, 90 | "jailvisiting-moveable": (user) => async (req, res, next) => 91 | res.json({ success: true, message: "Visiting jail", user }), 92 | "rp-moveable": (user, ct) => async (req, res, next) => { 93 | if (ct.randomPersonType === "ALLY") { 94 | await client.user.update({ 95 | where: { id: req.user.id }, 96 | data: { points: req.user.points + 50 }, 97 | }); 98 | 99 | return res.json({ 100 | success: true, 101 | message: "An ally gave you 50 points", 102 | user, 103 | }); 104 | } 105 | 106 | if (ct.randomPersonType === "PICKPOCKET") { 107 | await client.user.update({ 108 | where: { id: req.user.id }, 109 | data: { points: req.user.points - 50 }, 110 | }); 111 | 112 | return res.json({ 113 | success: true, 114 | message: "A pickpocket stole 50 points", 115 | user, 116 | }); 117 | } 118 | 119 | return res.json({ 120 | success: false, 121 | message: "Invalid randomPersonType", 122 | user, 123 | }); 124 | }, 125 | "gate-moveable": (user, ct) => async (req, res, next) => 126 | res.json({ success: true, user, message: "You're on a Gate tile" }), 127 | "gatei-moveable": (user, ct) => async (req, res, next) => 128 | res.json({ success: true, user, message: "You're on a Gate tile" }), 129 | "visited-moveable": (user, ct) => async (req, res, next) => 130 | res.json({ 131 | success: true, 132 | user, 133 | message: "You've already visited this tile", 134 | }), 135 | "mystery-moveable": (user, ct) => async (req, res, next) => 136 | res.json({ success: true, user, message: "You're on the mystery tile" }), 137 | "mystery-completed-moveable": (user, ct) => async (req, res, next) => 138 | res.json({ success: true, user, message: "You're on the mystery tile" }), 139 | "rp-riddle": (user, ct) => async (req, res, next) => 140 | res.json({ success: true, user, message: "You encountered a sphinx" }), 141 | level: (user, ct) => async (req, res, next) => 142 | res.json({ success: true, user, message: "You are on a level tile" }), 143 | "rp-guard": (user, ct) => async (req, res, next) => 144 | res.json({ 145 | success: true, 146 | user, 147 | message: "A guard has stopped you for questioning", 148 | }), 149 | "rp-sidequest-skippable": (user, ct) => async (req, res, next) => 150 | res.json({ 151 | success: true, 152 | user, 153 | message: "A villager approached you with a sidequest", 154 | }), 155 | "rp-riddle-skippable": (user, ct) => async (req, res, next) => 156 | res.json({ success: true, user, message: "You encountered a sphinx" }), 157 | jail: (user, ct) => async (req, res, next) => 158 | res.json({ success: true, user, message: "You are in jail" }), 159 | center: (user, ct) => async (req, res, next) => { 160 | try { 161 | let [lvl] = await client.userLevel.findMany({ 162 | where: { levelId: ct.tile.levelId, userId: user.id }, 163 | }); 164 | 165 | console.log({ lvl }); 166 | 167 | if (!lvl) { 168 | lvl = await client.userLevel.create({ 169 | data: { 170 | level: { connect: { id: ct.tile.levelId } }, 171 | user: { connect: { id: user.id } }, 172 | }, 173 | }); 174 | } 175 | 176 | return res.json({ 177 | success: true, 178 | user, 179 | message: "Congratulations for making it to the center tile!", 180 | }); 181 | } catch (e) { 182 | return next(e); 183 | } 184 | }, 185 | "gate-inner-moveable": (user, ct) => async (req, res, next) => 186 | res.json({ 187 | success: true, 188 | user, 189 | message: "You're on a Gate tile", 190 | }), 191 | }; 192 | -------------------------------------------------------------------------------- /backend/lib/auth.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | check: (req, res, next) => { 3 | if (!req.isAuthenticated()) { 4 | return res.json({ 5 | success: false, 6 | message: "Unauthorized", 7 | user: req.user, 8 | }); 9 | } 10 | 11 | return next(); 12 | }, 13 | 14 | canPlay: (req, res, next) => 15 | req.user 16 | ? !req.user.emailVerified || 17 | !req.user.discordVerified || 18 | req.user.dqed || 19 | req.user.bountyBanned 20 | ? res.json({ 21 | success: false, 22 | message: "Please verify your email", 23 | user: req.user, 24 | }) 25 | : req.user.points < 0 26 | ? res.json({ 27 | success: false, 28 | message: "Insufficient funds", 29 | user: req.user, 30 | }) 31 | : next() 32 | : res.json({ success: false, message: "auth pls", user: req.user }), 33 | 34 | admin: (req, res, next) => 35 | req.user.admin 36 | ? next() 37 | : res.json({ 38 | success: false, 39 | message: "This functionality is only available to admins", 40 | user: req.user, 41 | }), 42 | }; 43 | -------------------------------------------------------------------------------- /backend/lib/bot.js: -------------------------------------------------------------------------------- 1 | const Discord = require("discord.js"); 2 | const { client } = require("./prisma"); 3 | 4 | const bot = new Discord.Client(); 5 | 6 | bot.on("ready", () => { 7 | console.log(`Logged in as ${bot.user.tag}!`); 8 | }); 9 | 10 | bot.on("message", async (msg) => { 11 | // if (msg.member.roles.find((r) => r.name === 'Admin')) { 12 | // if (msg.member.roles.includes('Admin')) { 13 | if (msg.content.startsWith("crypto ")) { 14 | const command = msg.content.split(" ")[1]; 15 | 16 | // Get logs for a user 17 | if (command === "logs") { 18 | const username = msg.content.split(" ")[2]; 19 | 20 | const logs = await client.log.findMany({ 21 | where: { user: { username } }, 22 | orderBy: { 23 | createdAt: "desc", 24 | }, 25 | take: 30, 26 | }); 27 | 28 | const res = logs 29 | .map( 30 | (log) => `${log.createdAt.toLocaleTimeString("en-US")}: ${log.log}` 31 | ) 32 | .join("\n"); 33 | 34 | console.log(`Logs were requested for ${username}`); 35 | await msg.reply(res); 36 | } 37 | 38 | if (command === "tile") { 39 | const username = msg.content.split(" ")[2]; 40 | console.log(username); 41 | 42 | const { currentTileId } = await client.user.findOne({ 43 | where: { username }, 44 | }); 45 | const { number, type } = await client.tile.findOne({ 46 | where: { id: currentTileId }, 47 | }); 48 | 49 | const res = `${username} is currently on tile number ${number} with id ${currentTileId} and type '${type}'.`; 50 | console.log(res); 51 | await msg.reply(res); 52 | } 53 | 54 | if (command === "unhint") { 55 | const username = msg.content.split(" ")[2]; 56 | 57 | await client.user.update({ 58 | where: { username }, 59 | data: { hasHintCard: false }, 60 | }); 61 | 62 | await msg.reply(`Removed hint card for ${username}`); 63 | } 64 | 65 | if (command === "hint") { 66 | const username = msg.content.split(" ")[2]; 67 | 68 | const { hasHintCard } = await client.user.findOne({ 69 | where: { username }, 70 | }); 71 | 72 | const res = hasHintCard 73 | ? `${username} has a hint card` 74 | : `${username} does not have a hint card`; 75 | 76 | console.log(res); 77 | await msg.reply(res); 78 | } 79 | 80 | // Show suspicious activity 81 | if (command === "flagged") { 82 | // TODO: Get flagged logs with timestamps and usernames 83 | 84 | const flaggedLogs = await client.log.findMany({ 85 | where: { flagged: true }, 86 | orderBy: { 87 | createdAt: "desc", 88 | }, 89 | take: 30, 90 | }); 91 | 92 | const res = flaggedLogs 93 | .map( 94 | (log) => 95 | `${log.createdAt.toLocaleTimeString("en-US")}: (${log.userId}) ${ 96 | log.log 97 | }` 98 | ) 99 | .join("\n"); 100 | console.log(res); 101 | await msg.reply(res); 102 | } 103 | } 104 | }); 105 | 106 | // bot.login(process.env.DISCORD_TOKEN); 107 | bot.login("NzIzMjQ1OTg3OTA4NDE5NjU2.XvR1sg.q8Ha_9GI4dLGxzGAI8kgloRAdQ4"); 108 | -------------------------------------------------------------------------------- /backend/lib/helpers.js: -------------------------------------------------------------------------------- 1 | const { client } = require("./prisma"); 2 | 3 | const userStateFromTileType = { 4 | GO: async () => "go-moveable", 5 | STORY: async () => "story-moveable", 6 | LEVEL: async (ct, user) => { 7 | let [lvl] = await client.userLevel.findMany({ 8 | where: { levelId: ct.tile.levelId, userId: user.id }, 9 | }); 10 | 11 | if (!lvl) { 12 | lvl = await client.userLevel.create({ 13 | data: { 14 | level: { connect: { id: ct.tile.levelId } }, 15 | user: { connect: { id: user.id } }, 16 | }, 17 | }); 18 | } 19 | 20 | if (lvl.completed) { 21 | return "levelsolved-moveable"; 22 | } 23 | 24 | return "level"; 25 | }, 26 | RAND_CHANCE: async () => "randchance-moveable", 27 | RAND_PERSON: async (ct, user) => { 28 | if (ct.randomPersonType === "SPHINX") { 29 | const [riddle] = await client.userRiddle.findMany({ 30 | where: { userId: user.id, tileId: ct.id }, 31 | }); 32 | 33 | return !riddle 34 | ? "rp-riddle-skippable" 35 | : riddle.completed 36 | ? "rp-moveable" 37 | : "rp-riddle"; 38 | } 39 | 40 | if (ct.randomPersonType === "GUARD") { 41 | return ct.guardPassed ? "rp-guard-moveable" : "rp-guard"; 42 | } 43 | 44 | return { 45 | ALLY: "rp-moveable", 46 | PICKPOCKET: "rp-moveable", 47 | VILLAGER: "rp-sidequest-skippable", 48 | }[ct.randomPersonType]; 49 | }, 50 | JAIL: async (_, user) => { 51 | return user.incarcerated ? "jail" : "jailvisiting-moveable"; 52 | }, 53 | GATE: async () => "gate-moveable", 54 | GATEI: async () => "gatei-moveable", 55 | MYSTERY: async (ct, user) => { 56 | if (!ct.tile.mysteryTileOpen) { 57 | return "mystery-moveable"; 58 | } 59 | 60 | let [level] = await client.userLevel.findMany({ 61 | where: { level: { id: 47 } }, 62 | }); 63 | 64 | if (!level) { 65 | level = await client.userLevel.create({ 66 | data: { 67 | level: { connect: { id: 47 } }, 68 | user: { connect: { id: user.id } }, 69 | completed: false, 70 | }, 71 | }); 72 | } 73 | 74 | if (level.completed) { 75 | return "mystery-completed-moveable"; 76 | } 77 | 78 | return "mystery"; 79 | }, 80 | CENTER: async (ct, user) => { 81 | let [lvl] = await client.userLevel.findMany({ 82 | where: { userId: user.id, levelId: ct.tile.levelId }, 83 | include: { level: true }, 84 | }); 85 | 86 | if (!lvl) { 87 | lvl = await client.userLevel.create({ 88 | data: { 89 | level: { connect: { id: ct.tile.levelId } }, 90 | user: { connect: { id: user.id } }, 91 | completed: false, 92 | }, 93 | }); 94 | } 95 | 96 | if (lvl.completed) { 97 | return "finished"; 98 | } 99 | 100 | return "center"; 101 | }, 102 | }; 103 | 104 | async function findUserState(user) { 105 | const [currentTile] = await client.visitedTile.findMany({ 106 | where: { userId: user.id }, 107 | orderBy: { createdAt: "desc" }, 108 | take: 1, 109 | include: { tile: true }, 110 | }); 111 | 112 | console.log({ currentTile }); 113 | 114 | const tiles = await client.visitedTile.findMany({ 115 | where: { tileId: currentTile.tileId, userId: user.id }, 116 | }); 117 | 118 | if (tiles.length > 1) { 119 | console.log(currentTile.tile.type); 120 | const moveable = ["STORY", "LEVEL", "RAND_PERSON", "RAND_CHANCE"]; 121 | 122 | if (moveable.indexOf(currentTile.tile.type) !== -1) { 123 | console.log("visited-moveable"); 124 | return "visited-moveable"; 125 | } 126 | } 127 | 128 | const state = await userStateFromTileType[currentTile.tile.type]( 129 | currentTile, 130 | user 131 | ); 132 | console.log(state); 133 | return state; 134 | } 135 | 136 | const rollDice = () => Math.ceil(Math.random() * 6); 137 | 138 | // Dice roll special cases: 139 | // currentTileId === 44: 0 + dice roll 140 | // currentTileId === 76: 40 + dice roll 141 | const calculateNextTileId = ( 142 | currentTileId, 143 | diceRollV, 144 | userState, 145 | goIn, 146 | goOut 147 | ) => { 148 | const next = currentTileId + diceRollV; 149 | 150 | if (userState === "gate-moveable" && goIn) { 151 | console.log("movein", 44 + diceRollV); 152 | return 44 + diceRollV; 153 | } 154 | 155 | if (userState === "gatei-moveable" && goOut) { 156 | console.log("moveout", 23 + diceRollV); 157 | return 23 + diceRollV; 158 | } 159 | if (currentTileId <= 44 && next > 44) { 160 | return 0 + diceRollV - (44 - currentTileId); 161 | } 162 | 163 | if (currentTileId <= 80 && next > 80) { 164 | console.log("over 80"); 165 | return 44 + diceRollV - (80 - currentTileId); 166 | } 167 | 168 | return next; 169 | }; 170 | 171 | const chooseRandomChance = () => { 172 | const r = Math.random(); 173 | 174 | if (r < 0.45) { 175 | return "BRIBE"; 176 | } 177 | 178 | if (r < 0.9) { 179 | return "BOUNTY"; 180 | } 181 | 182 | return "JAIL"; 183 | }; 184 | 185 | const chooseRandomPerson = () => { 186 | const rp = ["ALLY", "PICKPOCKET", "SPHINX", "GUARD", "VILLAGER"]; 187 | const random = Math.floor(Math.random() * rp.length); 188 | 189 | return rp[random]; 190 | }; 191 | 192 | const checkCenter = async (user) => { 193 | const tiles = await client.visitedTile.findMany({ 194 | where: { userId: user.id }, 195 | }); 196 | const levels = await client.userLevel.findMany({ 197 | where: { userId: user.id }, 198 | }); 199 | 200 | const vTiles = []; 201 | 202 | for (let tile of tiles) { 203 | if (vTiles.indexOf(tile.tileId) === -1) { 204 | vTiles.push(tile.tileId); 205 | } 206 | } 207 | 208 | if (levels.length > 44 && vTiles.length > 79) { 209 | return true; 210 | } 211 | 212 | return false; 213 | }; 214 | 215 | module.exports = { 216 | findUserState, 217 | actionFromUserState: require("./action-from-user-state"), 218 | calculateNextTileId, 219 | rollDice, 220 | chooseRandomChance, 221 | chooseRandomPerson, 222 | checkCenter, 223 | }; 224 | -------------------------------------------------------------------------------- /backend/lib/logs.js: -------------------------------------------------------------------------------- 1 | const { client } = require('./prisma') 2 | 3 | /** 4 | * @param {number} userId 5 | * @param {string} log 6 | * @param {boolean} adminOnly 7 | */ 8 | async function add(userId, log, adminOnly) { 9 | const logRecord = await client.log.create({ 10 | data: { user: { connect: { id: userId } }, log, adminOnly }, 11 | }) 12 | 13 | return logRecord 14 | } 15 | 16 | module.exports = { add } 17 | -------------------------------------------------------------------------------- /backend/lib/prisma.js: -------------------------------------------------------------------------------- 1 | const { PrismaClient } = require("@prisma/client"); 2 | 3 | const client = new PrismaClient({ 4 | log: [ 5 | { 6 | emit: "event", 7 | level: "query", 8 | }, 9 | { 10 | emit: "event", 11 | level: "info", 12 | }, 13 | { 14 | emit: "event", 15 | level: "warn", 16 | }, 17 | { 18 | emit: "event", 19 | level: "error", 20 | }, 21 | ], 22 | }); 23 | 24 | client.on("query", (prismaEvent) => { 25 | console.log({ prismaEvent }); 26 | }); 27 | 28 | module.exports = { client }; 29 | -------------------------------------------------------------------------------- /backend/lib/recaptcha.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios').default 2 | 3 | module.exports = { 4 | verify: (secret = process.env.RECAPTCHA_SECRET_KEY) => async ( 5 | req, 6 | res, 7 | next 8 | ) => { 9 | try { 10 | if (!req.body.recaptcha) { 11 | return res.json({ success: false, message: 'Recaptcha token missing' }) 12 | } 13 | 14 | const { data } = await axios.get( 15 | `https://www.google.com/recaptcha/api/siteverify`, 16 | { 17 | params: { 18 | secret, 19 | response: req.body.recaptcha, 20 | }, 21 | } 22 | ) 23 | 24 | const { success, score } = data 25 | 26 | if (success && score > 0.5) { 27 | delete req.body.recaptcha 28 | next() 29 | } 30 | 31 | if (success && score < 0.5) { 32 | return res.json({ success: false, message: 'Stop spamming robot' }) 33 | } 34 | 35 | if (!success) { 36 | return res.json({ success: false, message: 'Recaptcha error' }) 37 | } 38 | } catch (err) { 39 | err.statusCode = 500 40 | next(err) 41 | } 42 | }, 43 | } 44 | -------------------------------------------------------------------------------- /backend/lib/validation.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken') 2 | 3 | module.exports = { 4 | against: (schema) => async (req, res, next) => { 5 | try { 6 | await schema.validate(req.body) 7 | return next() 8 | } catch (err) { 9 | if (err.toString().match(/^ValidationError:/)) { 10 | return res.json({ success: false, message: err.errors[0] }) 11 | } 12 | 13 | err.statusCode = 500 14 | next(err) 15 | } 16 | }, 17 | generateVerificationURL: async (target, user) => { 18 | const jot = jwt.sign({ userId: user.id, target }, process.env.SECRET) 19 | 20 | return `https://play.cryptichunt.com/verify/${target}?token=${jot}` 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /backend/migrations/20200624195700-init/schema.prisma: -------------------------------------------------------------------------------- 1 | datasource db { 2 | provider = "mysql" 3 | url = "***" 4 | } 5 | 6 | generator client { 7 | provider = "prisma-client-js" 8 | } 9 | 10 | enum RandomPerson { 11 | ALLY 12 | PICKPOCKET 13 | SPHINX 14 | GUARD 15 | VILLAGER 16 | } 17 | 18 | enum RandomChance { 19 | JAIL 20 | BRIBE 21 | BOUNTY 22 | } 23 | 24 | enum TileType { 25 | STORY 26 | LEVEL 27 | RAND_PERSON 28 | RAND_CHANCE 29 | GO 30 | JAIL 31 | GATE 32 | GATEI 33 | MYSTERY 34 | CENTER 35 | } 36 | 37 | model User { 38 | id Int @id @default(autoincrement()) 39 | createdAt DateTime @default(now()) 40 | updatedAt DateTime @updatedAt @default(now()) 41 | email String @unique 42 | emailVerified Boolean @default(false) 43 | username String @unique 44 | timesPassedGo Int @default(1) 45 | name String? 46 | password String? 47 | discord String @unique 48 | discordVerified Boolean @default(false) 49 | edu String? 50 | points Int @default(0) 51 | admin Boolean @default(false) 52 | incarcerated Boolean @default(false) 53 | incarceratedAt DateTime? 54 | bountyBanned Boolean @default(false) 55 | dqed Boolean @default(false) 56 | hasHintCard Boolean @default(false) 57 | currentTileId Int 58 | currentTile Tile @relation(fields: [currentTileId], references: [id]) 59 | 60 | logs Log[] 61 | levels UserLevel[] 62 | riddles UserRiddle[] 63 | visitedTiles VisitedTile[] 64 | levelAttempt LevelAttempt[] 65 | riddleAttempt RiddleAttempt[] 66 | } 67 | 68 | model Tile { 69 | id Int @id @default(autoincrement()) 70 | createdAt DateTime @default(now()) 71 | updatedAt DateTime @updatedAt @default(now()) 72 | type TileType 73 | number Int? @unique 74 | 75 | // Level tile 76 | levelId Int? 77 | level Level? @relation(fields: [levelId], references: [id]) 78 | 79 | // Story tile 80 | story Json? 81 | 82 | // Mystery tile 83 | mysteryTileOpen Boolean @default(false) 84 | 85 | visits VisitedTile[] 86 | usersOnTile User[] 87 | } 88 | 89 | model VisitedTile { 90 | id Int @id @default(autoincrement()) 91 | createdAt DateTime @default(now()) 92 | updatedAt DateTime @updatedAt @default(now()) 93 | userId Int 94 | user User @relation(fields: [userId], references: [id]) 95 | tileId Int 96 | tile Tile @relation(fields: [tileId], references: [id]) 97 | guardPassed Boolean? 98 | randomPersonType RandomPerson? 99 | riddleId Int? 100 | riddle Riddle? @relation(fields: [riddleId], references: [id]) 101 | randomChanceType RandomChance? 102 | UserRiddle UserRiddle[] 103 | } 104 | 105 | model Level { 106 | id Int @id @default(autoincrement()) 107 | createdAt DateTime @default(now()) 108 | updatedAt DateTime @updatedAt @default(now()) 109 | level Json 110 | points Int 111 | answer String 112 | userLvls UserLevel[] 113 | tiles Tile[] 114 | LevelAttempt LevelAttempt[] 115 | } 116 | 117 | model UserLevel { 118 | id Int @id @default(autoincrement()) 119 | createdAt DateTime @default(now()) 120 | updatedAt DateTime @updatedAt @default(now()) 121 | userId Int 122 | user User @relation(fields: [userId], references: [id]) 123 | levelId Int 124 | level Level @relation(fields: [levelId], references: [id]) 125 | completed Boolean @default(false) 126 | completedAt DateTime? 127 | attempts LevelAttempt[] 128 | } 129 | 130 | model Riddle { 131 | id Int @id @default(autoincrement()) 132 | createdAt DateTime @default(now()) 133 | updatedAt DateTime @updatedAt @default(now()) 134 | riddle Json 135 | answer String 136 | points Int 137 | completed UserRiddle[] 138 | tiles VisitedTile[] 139 | RiddleAttempt RiddleAttempt[] 140 | } 141 | 142 | model LevelAttempt { 143 | id Int @id @default(autoincrement()) 144 | createdAt DateTime @default(now()) 145 | updatedAt DateTime @updatedAt @default(now()) 146 | levelId Int 147 | level Level @relation(fields: [levelId], references: [id]) 148 | userId Int 149 | user User @relation(fields: [userId], references: [id]) 150 | attempt String 151 | UserLevel UserLevel? @relation(fields: [userLevelId], references: [id]) 152 | userLevelId Int? 153 | } 154 | 155 | model UserRiddle { 156 | id Int @id @default(autoincrement()) 157 | createdAt DateTime @default(now()) 158 | updatedAt DateTime @updatedAt @default(now()) 159 | userId Int 160 | user User @relation(fields: [userId], references: [id]) 161 | riddleId Int 162 | riddle Riddle @relation(fields: [riddleId], references: [id]) 163 | completed Boolean @default(false) 164 | completedAt DateTime? 165 | tileId Int 166 | tile VisitedTile @relation(fields: [tileId], references: [id]) 167 | } 168 | 169 | model RiddleAttempt { 170 | id Int @id @default(autoincrement()) 171 | createdAt DateTime @default(now()) 172 | updatedAt DateTime @updatedAt @default(now()) 173 | riddleId Int 174 | riddle Riddle @relation(fields: [riddleId], references: [id]) 175 | attempt String 176 | userId Int 177 | user User @relation(fields: [userId], references: [id]) 178 | } 179 | 180 | model Log { 181 | id Int @id @default(autoincrement()) 182 | createdAt DateTime @default(now()) 183 | updatedAt DateTime @updatedAt @default(now()) 184 | userId Int 185 | user User @relation(fields: [userId], references: [id]) 186 | log Json 187 | adminOnly Boolean @default(false) 188 | flagged Boolean @default(false) 189 | } 190 | -------------------------------------------------------------------------------- /backend/migrations/20200625162125-replace-json-types-with-strins/README.md: -------------------------------------------------------------------------------- 1 | # Migration `20200625162125-replace-json-types-with-strins` 2 | 3 | This migration has been generated by Angad Singh at 6/25/2020, 4:21:25 PM. 4 | You can check out the [state of the schema](./schema.prisma) after the migration. 5 | 6 | ## Database Steps 7 | 8 | ```sql 9 | ALTER TABLE `cryptocracy`.`Level` DROP COLUMN `level`, 10 | ADD COLUMN `level` varchar(191) NOT NULL ; 11 | 12 | ALTER TABLE `cryptocracy`.`Log` DROP COLUMN `log`, 13 | ADD COLUMN `log` varchar(191) NOT NULL ; 14 | 15 | ALTER TABLE `cryptocracy`.`Riddle` DROP COLUMN `riddle`, 16 | ADD COLUMN `riddle` varchar(191) NOT NULL ; 17 | 18 | ALTER TABLE `cryptocracy`.`Tile` DROP COLUMN `story`, 19 | ADD COLUMN `story` varchar(191) ; 20 | ``` 21 | 22 | ## Changes 23 | 24 | ```diff 25 | diff --git schema.prisma schema.prisma 26 | migration 20200624195700-init..20200625162125-replace-json-types-with-strins 27 | --- datamodel.dml 28 | +++ datamodel.dml 29 | @@ -1,7 +1,7 @@ 30 | datasource db { 31 | provider = "mysql" 32 | - url = "***" 33 | + url = env("MYSQL_URL") 34 | } 35 | generator client { 36 | provider = "prisma-client-js" 37 | @@ -76,9 +76,9 @@ 38 | levelId Int? 39 | level Level? @relation(fields: [levelId], references: [id]) 40 | // Story tile 41 | - story Json? 42 | + story String? 43 | // Mystery tile 44 | mysteryTileOpen Boolean @default(false) 45 | @@ -105,9 +105,9 @@ 46 | model Level { 47 | id Int @id @default(autoincrement()) 48 | createdAt DateTime @default(now()) 49 | updatedAt DateTime @updatedAt @default(now()) 50 | - level Json 51 | + level String 52 | points Int 53 | answer String 54 | userLvls UserLevel[] 55 | tiles Tile[] 56 | @@ -130,9 +130,9 @@ 57 | model Riddle { 58 | id Int @id @default(autoincrement()) 59 | createdAt DateTime @default(now()) 60 | updatedAt DateTime @updatedAt @default(now()) 61 | - riddle Json 62 | + riddle String 63 | answer String 64 | points Int 65 | completed UserRiddle[] 66 | tiles VisitedTile[] 67 | @@ -182,8 +182,8 @@ 68 | createdAt DateTime @default(now()) 69 | updatedAt DateTime @updatedAt @default(now()) 70 | userId Int 71 | user User @relation(fields: [userId], references: [id]) 72 | - log Json 73 | + log String 74 | adminOnly Boolean @default(false) 75 | flagged Boolean @default(false) 76 | } 77 | ``` 78 | 79 | 80 | -------------------------------------------------------------------------------- /backend/migrations/20200625162125-replace-json-types-with-strins/schema.prisma: -------------------------------------------------------------------------------- 1 | datasource db { 2 | provider = "mysql" 3 | url = "***" 4 | } 5 | 6 | generator client { 7 | provider = "prisma-client-js" 8 | } 9 | 10 | enum RandomPerson { 11 | ALLY 12 | PICKPOCKET 13 | SPHINX 14 | GUARD 15 | VILLAGER 16 | } 17 | 18 | enum RandomChance { 19 | JAIL 20 | BRIBE 21 | BOUNTY 22 | } 23 | 24 | enum TileType { 25 | STORY 26 | LEVEL 27 | RAND_PERSON 28 | RAND_CHANCE 29 | GO 30 | JAIL 31 | GATE 32 | GATEI 33 | MYSTERY 34 | CENTER 35 | } 36 | 37 | model User { 38 | id Int @id @default(autoincrement()) 39 | createdAt DateTime @default(now()) 40 | updatedAt DateTime @updatedAt @default(now()) 41 | email String @unique 42 | emailVerified Boolean @default(false) 43 | username String @unique 44 | timesPassedGo Int @default(1) 45 | name String? 46 | password String? 47 | discord String @unique 48 | discordVerified Boolean @default(false) 49 | edu String? 50 | points Int @default(0) 51 | admin Boolean @default(false) 52 | incarcerated Boolean @default(false) 53 | incarceratedAt DateTime? 54 | bountyBanned Boolean @default(false) 55 | dqed Boolean @default(false) 56 | hasHintCard Boolean @default(false) 57 | currentTileId Int 58 | currentTile Tile @relation(fields: [currentTileId], references: [id]) 59 | 60 | logs Log[] 61 | levels UserLevel[] 62 | riddles UserRiddle[] 63 | visitedTiles VisitedTile[] 64 | levelAttempt LevelAttempt[] 65 | riddleAttempt RiddleAttempt[] 66 | } 67 | 68 | model Tile { 69 | id Int @id @default(autoincrement()) 70 | createdAt DateTime @default(now()) 71 | updatedAt DateTime @updatedAt @default(now()) 72 | type TileType 73 | number Int? @unique 74 | 75 | // Level tile 76 | levelId Int? 77 | level Level? @relation(fields: [levelId], references: [id]) 78 | 79 | // Story tile 80 | story String? 81 | 82 | // Mystery tile 83 | mysteryTileOpen Boolean @default(false) 84 | 85 | visits VisitedTile[] 86 | usersOnTile User[] 87 | } 88 | 89 | model VisitedTile { 90 | id Int @id @default(autoincrement()) 91 | createdAt DateTime @default(now()) 92 | updatedAt DateTime @updatedAt @default(now()) 93 | userId Int 94 | user User @relation(fields: [userId], references: [id]) 95 | tileId Int 96 | tile Tile @relation(fields: [tileId], references: [id]) 97 | guardPassed Boolean? 98 | randomPersonType RandomPerson? 99 | riddleId Int? 100 | riddle Riddle? @relation(fields: [riddleId], references: [id]) 101 | randomChanceType RandomChance? 102 | UserRiddle UserRiddle[] 103 | } 104 | 105 | model Level { 106 | id Int @id @default(autoincrement()) 107 | createdAt DateTime @default(now()) 108 | updatedAt DateTime @updatedAt @default(now()) 109 | level String 110 | points Int 111 | answer String 112 | userLvls UserLevel[] 113 | tiles Tile[] 114 | LevelAttempt LevelAttempt[] 115 | } 116 | 117 | model UserLevel { 118 | id Int @id @default(autoincrement()) 119 | createdAt DateTime @default(now()) 120 | updatedAt DateTime @updatedAt @default(now()) 121 | userId Int 122 | user User @relation(fields: [userId], references: [id]) 123 | levelId Int 124 | level Level @relation(fields: [levelId], references: [id]) 125 | completed Boolean @default(false) 126 | completedAt DateTime? 127 | attempts LevelAttempt[] 128 | } 129 | 130 | model Riddle { 131 | id Int @id @default(autoincrement()) 132 | createdAt DateTime @default(now()) 133 | updatedAt DateTime @updatedAt @default(now()) 134 | riddle String 135 | answer String 136 | points Int 137 | completed UserRiddle[] 138 | tiles VisitedTile[] 139 | RiddleAttempt RiddleAttempt[] 140 | } 141 | 142 | model LevelAttempt { 143 | id Int @id @default(autoincrement()) 144 | createdAt DateTime @default(now()) 145 | updatedAt DateTime @updatedAt @default(now()) 146 | levelId Int 147 | level Level @relation(fields: [levelId], references: [id]) 148 | userId Int 149 | user User @relation(fields: [userId], references: [id]) 150 | attempt String 151 | UserLevel UserLevel? @relation(fields: [userLevelId], references: [id]) 152 | userLevelId Int? 153 | } 154 | 155 | model UserRiddle { 156 | id Int @id @default(autoincrement()) 157 | createdAt DateTime @default(now()) 158 | updatedAt DateTime @updatedAt @default(now()) 159 | userId Int 160 | user User @relation(fields: [userId], references: [id]) 161 | riddleId Int 162 | riddle Riddle @relation(fields: [riddleId], references: [id]) 163 | completed Boolean @default(false) 164 | completedAt DateTime? 165 | tileId Int 166 | tile VisitedTile @relation(fields: [tileId], references: [id]) 167 | } 168 | 169 | model RiddleAttempt { 170 | id Int @id @default(autoincrement()) 171 | createdAt DateTime @default(now()) 172 | updatedAt DateTime @updatedAt @default(now()) 173 | riddleId Int 174 | riddle Riddle @relation(fields: [riddleId], references: [id]) 175 | attempt String 176 | userId Int 177 | user User @relation(fields: [userId], references: [id]) 178 | } 179 | 180 | model Log { 181 | id Int @id @default(autoincrement()) 182 | createdAt DateTime @default(now()) 183 | updatedAt DateTime @updatedAt @default(now()) 184 | userId Int 185 | user User @relation(fields: [userId], references: [id]) 186 | log String 187 | adminOnly Boolean @default(false) 188 | flagged Boolean @default(false) 189 | } 190 | -------------------------------------------------------------------------------- /backend/migrations/20200625162125-replace-json-types-with-strins/steps.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.3.14-fixed", 3 | "steps": [ 4 | { 5 | "tag": "UpdateField", 6 | "model": "Tile", 7 | "field": "story", 8 | "type": "String" 9 | }, 10 | { 11 | "tag": "UpdateField", 12 | "model": "Level", 13 | "field": "level", 14 | "type": "String" 15 | }, 16 | { 17 | "tag": "UpdateField", 18 | "model": "Riddle", 19 | "field": "riddle", 20 | "type": "String" 21 | }, 22 | { 23 | "tag": "UpdateField", 24 | "model": "Log", 25 | "field": "log", 26 | "type": "String" 27 | } 28 | ] 29 | } -------------------------------------------------------------------------------- /backend/migrations/migrate.lock: -------------------------------------------------------------------------------- 1 | # Prisma Migrate lockfile v1 2 | 3 | 20200624195700-init 4 | 20200625162125-replace-json-types-with-strins -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "createscripts": "chmod a+x bin/*", 8 | "serve": "./bin/serve", 9 | "start": "./bin/serve", 10 | "serve:dev": "nodemon bin/serve" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC", 15 | "dependencies": { 16 | "@prisma/client": "^2.0.1", 17 | "@sentry/node": "^5.17.0", 18 | "aws-serverless-express": "^3.3.8", 19 | "axios": "^0.19.2", 20 | "bcrypt": "^5.0.0", 21 | "compression": "^1.7.4", 22 | "connect-redis": "^4.0.4", 23 | "cookie-parser": "^1.4.5", 24 | "cors": "^2.8.5", 25 | "discord.js": "^12.2.0", 26 | "dotenv": "^8.2.0", 27 | "express": "^4.17.1", 28 | "express-rate-limit": "^5.1.3", 29 | "express-session": "^1.17.1", 30 | "helmet": "^3.23.1", 31 | "jsonwebtoken": "^8.5.1", 32 | "morgan": "^1.10.0", 33 | "nodemailer": "^6.4.10", 34 | "passport": "^0.4.1", 35 | "passport-local": "^1.0.0", 36 | "pino-http": "^5.2.0", 37 | "pino-pretty": "^4.0.0", 38 | "redis": "^3.0.2", 39 | "yup": "^0.29.1" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /backend/schema.prisma: -------------------------------------------------------------------------------- 1 | datasource db { 2 | provider = "mysql" 3 | url = env("MYSQL_URL") 4 | } 5 | 6 | generator client { 7 | provider = "prisma-client-js" 8 | } 9 | 10 | enum RandomPerson { 11 | ALLY 12 | PICKPOCKET 13 | SPHINX 14 | GUARD 15 | VILLAGER 16 | } 17 | 18 | enum RandomChance { 19 | JAIL 20 | BRIBE 21 | BOUNTY 22 | } 23 | 24 | enum TileType { 25 | STORY 26 | LEVEL 27 | RAND_PERSON 28 | RAND_CHANCE 29 | GO 30 | JAIL 31 | GATE 32 | GATEI 33 | MYSTERY 34 | CENTER 35 | } 36 | 37 | model User { 38 | id Int @id @default(autoincrement()) 39 | createdAt DateTime @default(now()) 40 | updatedAt DateTime @updatedAt @default(now()) 41 | email String @unique 42 | emailVerified Boolean @default(false) 43 | username String @unique 44 | timesPassedGo Int @default(1) 45 | name String? 46 | password String? 47 | discord String @unique 48 | discordVerified Boolean @default(false) 49 | edu String? 50 | points Int @default(0) 51 | admin Boolean @default(false) 52 | incarcerated Boolean @default(false) 53 | incarceratedAt DateTime? 54 | bountyBanned Boolean @default(false) 55 | dqed Boolean @default(false) 56 | hasHintCard Boolean @default(false) 57 | currentTileId Int 58 | currentTile Tile @relation(fields: [currentTileId], references: [id]) 59 | 60 | logs Log[] 61 | levels UserLevel[] 62 | riddles UserRiddle[] 63 | visitedTiles VisitedTile[] 64 | levelAttempt LevelAttempt[] 65 | riddleAttempt RiddleAttempt[] 66 | } 67 | 68 | model Tile { 69 | id Int @id @default(autoincrement()) 70 | createdAt DateTime @default(now()) 71 | updatedAt DateTime @updatedAt @default(now()) 72 | type TileType 73 | number Int? @unique 74 | 75 | // Level tile 76 | levelId Int? 77 | level Level? @relation(fields: [levelId], references: [id]) 78 | 79 | // Story tile 80 | story String? 81 | 82 | // Mystery tile 83 | mysteryTileOpen Boolean @default(false) 84 | 85 | visits VisitedTile[] 86 | usersOnTile User[] 87 | } 88 | 89 | model VisitedTile { 90 | id Int @id @default(autoincrement()) 91 | createdAt DateTime @default(now()) 92 | updatedAt DateTime @updatedAt @default(now()) 93 | userId Int 94 | user User @relation(fields: [userId], references: [id]) 95 | tileId Int 96 | tile Tile @relation(fields: [tileId], references: [id]) 97 | guardPassed Boolean? 98 | randomPersonType RandomPerson? 99 | riddleId Int? 100 | riddle Riddle? @relation(fields: [riddleId], references: [id]) 101 | randomChanceType RandomChance? 102 | UserRiddle UserRiddle[] 103 | } 104 | 105 | model Level { 106 | id Int @id @default(autoincrement()) 107 | createdAt DateTime @default(now()) 108 | updatedAt DateTime @updatedAt @default(now()) 109 | level String 110 | points Int 111 | answer String 112 | userLvls UserLevel[] 113 | tiles Tile[] 114 | LevelAttempt LevelAttempt[] 115 | } 116 | 117 | model UserLevel { 118 | id Int @id @default(autoincrement()) 119 | createdAt DateTime @default(now()) 120 | updatedAt DateTime @updatedAt @default(now()) 121 | userId Int 122 | user User @relation(fields: [userId], references: [id]) 123 | levelId Int 124 | level Level @relation(fields: [levelId], references: [id]) 125 | completed Boolean @default(false) 126 | completedAt DateTime? 127 | attempts LevelAttempt[] 128 | } 129 | 130 | model Riddle { 131 | id Int @id @default(autoincrement()) 132 | createdAt DateTime @default(now()) 133 | updatedAt DateTime @updatedAt @default(now()) 134 | riddle String 135 | answer String 136 | points Int 137 | completed UserRiddle[] 138 | tiles VisitedTile[] 139 | RiddleAttempt RiddleAttempt[] 140 | } 141 | 142 | model LevelAttempt { 143 | id Int @id @default(autoincrement()) 144 | createdAt DateTime @default(now()) 145 | updatedAt DateTime @updatedAt @default(now()) 146 | levelId Int 147 | level Level @relation(fields: [levelId], references: [id]) 148 | userId Int 149 | user User @relation(fields: [userId], references: [id]) 150 | attempt String 151 | UserLevel UserLevel? @relation(fields: [userLevelId], references: [id]) 152 | userLevelId Int? 153 | } 154 | 155 | model UserRiddle { 156 | id Int @id @default(autoincrement()) 157 | createdAt DateTime @default(now()) 158 | updatedAt DateTime @updatedAt @default(now()) 159 | userId Int 160 | user User @relation(fields: [userId], references: [id]) 161 | riddleId Int 162 | riddle Riddle @relation(fields: [riddleId], references: [id]) 163 | completed Boolean @default(false) 164 | completedAt DateTime? 165 | tileId Int 166 | tile VisitedTile @relation(fields: [tileId], references: [id]) 167 | } 168 | 169 | model RiddleAttempt { 170 | id Int @id @default(autoincrement()) 171 | createdAt DateTime @default(now()) 172 | updatedAt DateTime @updatedAt @default(now()) 173 | riddleId Int 174 | riddle Riddle @relation(fields: [riddleId], references: [id]) 175 | attempt String 176 | userId Int 177 | user User @relation(fields: [userId], references: [id]) 178 | } 179 | 180 | model Log { 181 | id Int @id @default(autoincrement()) 182 | createdAt DateTime @default(now()) 183 | updatedAt DateTime @updatedAt @default(now()) 184 | userId Int 185 | user User @relation(fields: [userId], references: [id]) 186 | log String 187 | adminOnly Boolean @default(false) 188 | flagged Boolean @default(false) 189 | } 190 | -------------------------------------------------------------------------------- /backend/seed/levels.js: -------------------------------------------------------------------------------- 1 | const { client } = require("../lib/prisma"); 2 | 3 | module.exports = async function main() { 4 | // Create levels 5 | console.log(`${Date.now()} Game levels`); 6 | const rawLevels = Array(47) 7 | .fill("x") 8 | .map((_, i) => ({ 9 | level: i === 46 ? "Mystery lvl" : `This is level ${i + 1}`, 10 | points: (i + 1) * 250, 11 | answer: "leedspls", 12 | })); 13 | const lvlRecords = []; 14 | for (let lvl of rawLevels) { 15 | lvlRecords.push(await client.level.create({ data: lvl })); 16 | } 17 | console.log(`${Date.now()} Game levels done`); 18 | 19 | return; 20 | }; 21 | -------------------------------------------------------------------------------- /backend/seed/riddles.js: -------------------------------------------------------------------------------- 1 | const { client } = require("../lib/prisma"); 2 | 3 | module.exports = async function main() { 4 | // Create riddles 5 | console.log(`${Date.now()} Game riddles`); 6 | const rawRiddles = Array(45) 7 | .fill("x") 8 | .map((_, i) => ({ 9 | riddle: `This is riddle ${i + 1}`, 10 | points: (i + 1) * 250, 11 | answer: "leedspls", 12 | })); 13 | const riddleRecords = []; 14 | for (let riddle of rawRiddles) { 15 | riddleRecords.push(await client.riddle.create({ data: riddle })); 16 | } 17 | console.log(`${Date.now()} Game riddles done`); 18 | 19 | return; 20 | }; 21 | -------------------------------------------------------------------------------- /backend/seed/run.js: -------------------------------------------------------------------------------- 1 | const dev = process.env.NODE_ENV !== 'production' 2 | if (dev) { 3 | require('dotenv').config({ path: `${__dirname}/../../.env` }) 4 | } 5 | 6 | const fs = require('fs') 7 | 8 | async function main() { 9 | const script = process.argv[2] 10 | if (!script) { 11 | console.error('Invalid script') 12 | return 13 | } 14 | 15 | if (script === 'all') { 16 | const scripts = fs.readdirSync(__dirname).filter((f) => f !== 'run.js') 17 | 18 | for (let script of scripts) { 19 | const fn = require(`./${script}`) 20 | 21 | console.log(`${Date.now()} Running ${__dirname}/${script}`) 22 | await fn() 23 | console.log(`${Date.now()} Ran ${__dirname}/${script}`) 24 | } 25 | 26 | return 27 | } 28 | 29 | if (fs.readdirSync(__dirname).indexOf(`${script}.js`) === -1) { 30 | console.error('Invalid script') 31 | return 32 | } 33 | 34 | const fn = require(`./${script}.js`) 35 | 36 | console.log(`${Date.now()} Running ${__dirname}/${script}.js`) 37 | await fn() 38 | console.log(`${Date.now()} Ran ${__dirname}/${script}.js`) 39 | 40 | return 41 | } 42 | 43 | main() 44 | .then(() => { 45 | process.exit() 46 | }) 47 | .catch(console.error) 48 | -------------------------------------------------------------------------------- /backend/seed/tiles.js: -------------------------------------------------------------------------------- 1 | const { client } = require("../lib/prisma"); 2 | const rawTiles = require("../lib/tiles"); 3 | const tiles = [...rawTiles]; 4 | 5 | const transformers = { 6 | GO: (t) => ({ type: t.type }), 7 | JAIL: (t) => ({ type: t.type }), 8 | GATE: (t) => ({ type: t.type }), 9 | GATEI: (t) => ({ type: t.type, number: t.number }), 10 | MYSTERY: (t) => ({ type: t.type, level: { connect: { id: t.levelId } } }), 11 | CENTER: (t) => ({ type: t.type, level: { connect: { id: t.levelId } } }), 12 | LEVEL: (t) => ({ 13 | type: t.type, 14 | number: t.number, 15 | level: { connect: { id: t.levelId } }, 16 | }), 17 | RAND_CHANCE: (t) => ({ 18 | type: t.type, 19 | number: t.number, 20 | }), 21 | RAND_PERSON: (t) => ({ 22 | type: t.type, 23 | number: t.number, 24 | }), 25 | STORY: (t) => ({ 26 | type: t.type, 27 | number: t.number, 28 | story: t.story, 29 | }), 30 | }; 31 | 32 | module.exports = async function main() { 33 | console.log(`${Date.now()} Game tiles`); 34 | for (let tile of tiles) { 35 | await client.tile.create({ 36 | data: transformers[tile.type](tile), 37 | }); 38 | } 39 | console.log(`${Date.now()} Game tiles done`); 40 | 41 | return; 42 | }; 43 | -------------------------------------------------------------------------------- /frontend/.env.example: -------------------------------------------------------------------------------- 1 | REACT_APP_API_ENDPOINT= 2 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `npm start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | 15 | ### `npm test` 16 | 17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `npm run build` 21 | 22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `npm run eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | 46 | ### Code Splitting 47 | 48 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting 49 | 50 | ### Analyzing the Bundle Size 51 | 52 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size 53 | 54 | ### Making a Progressive Web App 55 | 56 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app 57 | 58 | ### Advanced Configuration 59 | 60 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration 61 | 62 | ### Deployment 63 | 64 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment 65 | 66 | ### `npm run build` fails to minify 67 | 68 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify 69 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^4.2.4", 7 | "@testing-library/react": "^9.5.0", 8 | "@testing-library/user-event": "^7.2.1", 9 | "axios": "^0.19.2", 10 | "formik": "^2.1.4", 11 | "qs": "^6.9.4", 12 | "react": "^16.13.1", 13 | "react-dom": "^16.13.1", 14 | "react-google-recaptcha-v3": "^1.5.2", 15 | "react-helmet": "^6.1.0", 16 | "react-router-dom": "^5.2.0", 17 | "react-scripts": "3.4.1", 18 | "react-spinners": "^0.9.0", 19 | "react-toast-notifications": "^2.4.0", 20 | "styled-components": "^5.1.1", 21 | "sweetalert": "^2.1.2", 22 | "yup": "^0.29.1" 23 | }, 24 | "proxy": "http://localhost:62442", 25 | "scripts": { 26 | "start": "react-scripts start", 27 | "build": "react-scripts build", 28 | "test": "react-scripts test", 29 | "eject": "react-scripts eject", 30 | "deploy": "npm run build && rm -rf ../backend/react_build && mv build ../backend/react_build" 31 | }, 32 | "eslintConfig": { 33 | "extends": "react-app" 34 | }, 35 | "browserslist": { 36 | "production": [ 37 | ">0.2%", 38 | "not dead", 39 | "not op_mini all" 40 | ], 41 | "development": [ 42 | "last 1 chrome version", 43 | "last 1 firefox version", 44 | "last 1 safari version" 45 | ] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /frontend/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cryptichunt/platform/17b04d384aee97fe0f6745c9b4396d7a4f0ba1b9/frontend/public/favicon.png -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 14 | 15 | Cryptocracy 2020 16 | 17 | 18 | 22 | 23 | 32 | 33 | 34 | 35 |
36 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /frontend/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import styled, { createGlobalStyle } from "styled-components"; 3 | import { BrowserRouter as Router, Switch, Route } from "react-router-dom"; 4 | import { Helmet as Head } from "react-helmet"; 5 | import { ToastProvider } from "react-toast-notifications"; 6 | import { GoogleReCaptchaProvider } from "react-google-recaptcha-v3"; 7 | import AuthContext from "./lib/auth-context"; 8 | import Navbar from "./components/Layout/Navbar"; 9 | import FaviconPng from "./assets/favicon.png"; 10 | import IvyLogo from "./assets/ivyachievement.png"; 11 | 12 | import Home from "./pages/index"; 13 | import VerifyEmail from "./pages/verify/email"; 14 | import Leaderboard from "./pages/leaderboard"; 15 | import Logs from "./pages/logs"; 16 | import Play from "./pages/play"; 17 | import Register from "./pages/register"; 18 | import Signin from "./pages/signin"; 19 | import Shop from "./pages/shop"; 20 | import NotFound from "./pages/404"; 21 | 22 | const GlobalStyles = createGlobalStyle` 23 | html, 24 | body, #root { 25 | height: 100%; 26 | width: 100%; 27 | margin: 0; 28 | padding: 0; 29 | color: #fff; 30 | background: #23272A; 31 | overflow-x: hidden; 32 | } 33 | 34 | * { 35 | margin: 0; 36 | padding: 0; 37 | box-sizing: border-box; 38 | font-family: 'Inter', sans-serif; 39 | } 40 | 41 | @supports (font-variation-settings: normal) { 42 | html { 43 | font-family: 'Inter var', sans-serif; 44 | } 45 | } 46 | 47 | .grecaptcha-badge { 48 | opacity: 0; 49 | visibility: hidden; 50 | } 51 | `; 52 | 53 | const Footer = styled.div` 54 | height: 15vh; 55 | width: 100%; 56 | display: flex; 57 | justify-content: space-around; 58 | align-items: center; 59 | 60 | @media screen and (max-width: 700px) { 61 | flex-direction: column; 62 | padding: 10px 0; 63 | justify-content: space-between; 64 | } 65 | `; 66 | 67 | const Copy = styled.div` 68 | font-size: 1.2rem; 69 | `; 70 | 71 | const SponsorImg = styled.img` 72 | height: 8vh; 73 | width: auto; 74 | `; 75 | 76 | function App() { 77 | const [authenticated, setAuthenticated] = useState(false); 78 | const [user, setUser] = useState({}); 79 | 80 | return ( 81 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 96 | 97 | 98 | 99 | Cryptocracy 2020 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | {/* */} 110 | 111 | 112 | 113 | 114 | 115 |
116 | © Team Cryptocracy 2020 117 | 118 |
119 |
120 |
121 |
122 | ); 123 | } 124 | 125 | export default App; 126 | -------------------------------------------------------------------------------- /frontend/src/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cryptichunt/platform/17b04d384aee97fe0f6745c9b4396d7a4f0ba1b9/frontend/src/assets/favicon.png -------------------------------------------------------------------------------- /frontend/src/assets/ivyachievement.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cryptichunt/platform/17b04d384aee97fe0f6745c9b4396d7a4f0ba1b9/frontend/src/assets/ivyachievement.png -------------------------------------------------------------------------------- /frontend/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cryptichunt/platform/17b04d384aee97fe0f6745c9b4396d7a4f0ba1b9/frontend/src/assets/logo.png -------------------------------------------------------------------------------- /frontend/src/assets/svg/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /frontend/src/components/Layout/AuthCheck.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Redirect } from "react-router-dom"; 3 | import AuthContext from "../../lib/auth-context"; 4 | import api from "../../lib/api"; 5 | import Loading from "../Loading"; 6 | 7 | export default class AuthCheck extends React.Component { 8 | static contextType = AuthContext; 9 | 10 | state = { retComponent: }; 11 | 12 | async componentDidMount() { 13 | try { 14 | const data = await ( 15 | await fetch(api("/api/auth/me"), { 16 | method: "POST", 17 | }) 18 | ).json(); 19 | 20 | this.context.setAuthenticated(data.authenticated); 21 | this.context.setUser(data.user); 22 | 23 | if ( 24 | data.authenticated === this.props.allowAuthenticated || 25 | this.props.allowAll 26 | ) { 27 | this.setState({ retComponent: <>{this.props.children} }); 28 | } else { 29 | this.setState({ retComponent: }); 30 | } 31 | 32 | if ( 33 | data.authenticated && 34 | !data.user.emailVerified && 35 | !!!window.location.href.match(/verify\/email/) 36 | ) { 37 | this.setState({ 38 | retComponent: , 39 | }); 40 | } 41 | 42 | if (data.authenticated && data.user.dqed) { 43 | this.setState({ 44 | retComponent: , 45 | }); 46 | } 47 | 48 | if (data.authenticated && data.user.bountyBanned) { 49 | this.setState({ 50 | retComponent: , 51 | }); 52 | } 53 | } catch (e) { 54 | this.setState({ 55 | retComponent: , 56 | }); 57 | } 58 | } 59 | 60 | render() { 61 | return this.state.retComponent; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /frontend/src/components/Layout/Navbar.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import styled from "styled-components"; 3 | import api from "../../lib/api"; 4 | import { Link as RouterLink, useHistory } from "react-router-dom"; 5 | 6 | const NavContainer = styled.div` 7 | height: 100%; 8 | width: 100%; 9 | display: flex; 10 | align-items: center; 11 | justify-content: center; 12 | transition: left 0.5s ease; 13 | 14 | @media screen and (max-width: 850px) { 15 | display: flex; 16 | position: fixed; 17 | top: 0; 18 | left: ${(props) => (props.open ? "0" : "100vh")}; 19 | height: 100vh; 20 | width: 100vw; 21 | background: #00000095; 22 | flex-direction: column; 23 | z-index: 1001; 24 | > a { 25 | margin: 20px 0; 26 | font-size: 1.3rem; 27 | } 28 | 29 | > svg { 30 | margin-top: 40px; 31 | height: 40px; 32 | width: 40px; 33 | } 34 | } 35 | `; 36 | 37 | const RelativeNavContainer = styled.div` 38 | position: relative; 39 | height: 10vh; 40 | width: 100%; 41 | display: flex; 42 | padding: 0 30px; 43 | align-items: center; 44 | justify-content: space-between; 45 | `; 46 | 47 | const NavLogoContainer = styled.div` 48 | height: 6vh; 49 | width: 6vh; 50 | display: ${(props) => (props.mobile ? "none" : "flex")}; 51 | align-items: center; 52 | justify-content: center; 53 | /* background: #22272b; 54 | box-shadow: rgba(256, 256, 256, 0.07) 0px 4px 30px; 55 | border-radius: 3px; */ 56 | margin: 0 20px; 57 | text-decoration: none; 58 | 59 | @media screen and (max-width: 850px) { 60 | display: ${(props) => (props.mobile ? "flex" : "none")}; 61 | height: 3.3vh; 62 | width: 3.3vh; 63 | margin: 0; 64 | } 65 | `; 66 | 67 | const NavLogoSvg = styled.svg` 68 | height: 3.3vh; 69 | width: auto; 70 | color: inherit; 71 | `; 72 | 73 | const NavLinkComponent = styled.a` 74 | margin: 0 20px; 75 | color: inherit; 76 | font-size: 1rem; 77 | font-weight: bold; 78 | letter-spacing: 1.5px; 79 | text-transform: uppercase; 80 | cursor: pointer; 81 | `; 82 | 83 | const Link = ({ to, children }) => ( 84 | 89 | {children} 90 | 91 | ); 92 | 93 | const NavLogo = ({ mobile }) => ( 94 | 95 | 102 | 109 | 116 | 124 | 131 | 138 | 139 | 140 | ); 141 | 142 | const IconSvg = styled.svg` 143 | display: none; 144 | color: inherit; 145 | height: 30px; 146 | width: 30px; 147 | cursor: pointer; 148 | 149 | @media screen and (max-width: 850px) { 150 | display: inline; 151 | } 152 | `; 153 | 154 | const Hamburger = ({ onClick }) => ( 155 | 156 | 161 | 162 | ); 163 | 164 | const Close = ({ onClick }) => ( 165 | 166 | 171 | 172 | ); 173 | 174 | const handleSignOut = (history) => () => { 175 | fetch(api("/api/auth/logout")) 176 | .then(() => history.push("/signin")) 177 | .catch(console.error); 178 | }; 179 | 180 | const AuthenticatedNav = ({ handleClose }) => { 181 | const history = useHistory(); 182 | 183 | return ( 184 | <> 185 | Leaderboard 186 | {/* Shop */} 187 | Rules 188 | 189 | Play 190 | Logs 191 | 192 | Sign Out 193 | 194 | 195 | 196 | ); 197 | }; 198 | 199 | const UnauthenticatedNav = ({ handleClose }) => ( 200 | <> 201 | Leaderboard 202 | Rules 203 | 204 | Sign In 205 | Register 206 | 207 | 208 | ); 209 | 210 | export default ({ authenticated }) => { 211 | const [open, setOpen] = useState(false); 212 | 213 | return ( 214 | 215 | 216 | setOpen(true)} /> 217 | 218 | {authenticated ? ( 219 | setOpen(false)} /> 220 | ) : ( 221 | setOpen(false)} /> 222 | )} 223 | 224 | 225 | ); 226 | }; 227 | -------------------------------------------------------------------------------- /frontend/src/components/Layout/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { Helmet as Head } from "react-helmet"; 4 | 5 | import AuthCheck from "./AuthCheck"; 6 | 7 | const ContentContainer = styled.div` 8 | min-height: 90vh; 9 | width: 100%; 10 | position: relative; 11 | z-index: 1000; 12 | `; 13 | 14 | export default ({ 15 | title, 16 | children, 17 | allowAuthenticated, 18 | allowAll, 19 | fallback, 20 | }) => { 21 | return ( 22 | <> 23 | 24 | {title && `${title} | `}Cryptocracy 2020 25 | 26 | 27 | 28 | {children} 29 | 30 | 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /frontend/src/components/Loading.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { BounceLoader } from "react-spinners"; 4 | import { Helmet as Head } from "react-helmet"; 5 | 6 | const LoadingContainer = styled.div` 7 | height: 90vh; 8 | width: 100%; 9 | display: flex; 10 | justify-content: center; 11 | align-items: center; 12 | `; 13 | 14 | const ErrorTitle = styled.div` 15 | font-size: 2rem; 16 | font-weight: bold; 17 | color: #ff0000; 18 | text-align: center; 19 | `; 20 | 21 | const ErrorDesc = styled.div` 22 | font-size: 1.3rem; 23 | color: #ff0000; 24 | text-align: center; 25 | `; 26 | 27 | const LogoSvg = styled.svg` 28 | height: 70px; 29 | width: auto; 30 | margin: 40px 0; 31 | `; 32 | 33 | const ErrorContainer = styled.div` 34 | display: flex; 35 | flex-direction: column; 36 | justify-content: center; 37 | align-items: center; 38 | `; 39 | 40 | const Logo = () => ( 41 | 48 | 55 | 62 | 70 | 77 | 84 | 85 | ); 86 | 87 | export default function Loading({ error }) { 88 | return ( 89 | <> 90 | 91 | {error ? error : "Loading"} 92 | 93 | 94 | 99 | 100 | {error ? ( 101 | 102 | 103 | An error occurred 104 | {error} 105 | 106 | ) : ( 107 | "" 108 | )} 109 | 110 | 111 | ); 112 | } 113 | -------------------------------------------------------------------------------- /frontend/src/components/Login/form.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link, useHistory, useLocation } from "react-router-dom"; 3 | import * as yup from "yup"; 4 | import { Formik, Form } from "formik"; 5 | import { useGoogleReCaptcha } from "react-google-recaptcha-v3"; 6 | import { useToasts } from "react-toast-notifications"; 7 | import { ButtonContainer, Button, Field, Message } from "../forms"; 8 | import api from "../../lib/api"; 9 | 10 | function useQuery() { 11 | return new URLSearchParams(useLocation().search); 12 | } 13 | 14 | export default () => { 15 | const router = useHistory(); 16 | const query = useQuery(); 17 | const { addToast } = useToasts(); 18 | const { executeRecaptcha } = useGoogleReCaptcha(); 19 | const fromreg = query.get("fromreg"); 20 | 21 | React.useEffect(() => { 22 | if (fromreg) { 23 | addToast("Your account has been created, please sign in", { 24 | appearance: "success", 25 | }); 26 | } 27 | }, [fromreg]); 28 | 29 | const handleSubmit = async (values, { setSubmitting }) => { 30 | const token = await executeRecaptcha("login_page"); 31 | 32 | // same shape as initial values 33 | const data = { ...values, recaptcha: token }; 34 | 35 | fetch(api("/api/auth/login"), { 36 | method: "POST", 37 | headers: { 38 | "Content-Type": "application/json", 39 | }, 40 | body: JSON.stringify(data), 41 | }) 42 | .then((resp) => resp.json()) 43 | .then(({ success, message }) => { 44 | if (success) { 45 | router.push("/play?fromlogin=true", "/play?fromlogin=true", { 46 | shallow: true, 47 | }); 48 | } else { 49 | addToast(message, { appearance: "error" }); 50 | setSubmitting(false); 51 | } 52 | }) 53 | .catch((e) => { 54 | addToast(e.message, { appearance: "error" }); 55 | console.error(e); 56 | }); 57 | }; 58 | 59 | return ( 60 | 78 | {({ errors, touched, isSubmitting, isValid, isValidating, dirty }) => ( 79 |
80 | 88 | 96 | 97 | Don't have an account? 98 | 99 | 100 | 106 | 107 | 108 | )} 109 |
110 | ); 111 | }; 112 | -------------------------------------------------------------------------------- /frontend/src/components/Play/Grid/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useState, useContext } from "react"; 3 | import styled from "styled-components"; 4 | 5 | import { RenderTiles } from "./tile"; 6 | import AuthContext from "../../../lib/auth-context"; 7 | import api from "../../../lib/api"; 8 | import UserStates from "../States"; 9 | 10 | const XScrollable = styled.div` 11 | overflow-x: auto; 12 | padding: 0 50px; 13 | &::-webkit-scrollbar-track, 14 | &::-webkit-scrollbar { 15 | background: none; 16 | } 17 | 18 | &::-webkit-scrollbar-thumb { 19 | background: #2977f520; 20 | } 21 | `; 22 | 23 | const Container = styled.div` 24 | --max-dim: 1100px; 25 | --min-dim: 700px; 26 | --dim: calc(100vw - 100px); 27 | height: var(--dim); 28 | width: var(--dim); 29 | max-height: var(--max-dim); 30 | max-width: var(--max-dim); 31 | min-height: var(--min-dim); 32 | min-width: var(--min-dim); 33 | display: grid; 34 | margin: 50px auto; 35 | grid-template-columns: repeat(14, 1fr); 36 | grid-template-rows: repeat(14, 1fr); 37 | gap: 2px; 38 | grid-template-areas: 39 | "random3 random3 cell21 cell22 cell23 cell24 cell25 cell26 cell27 cell28 cell29 cell30 random4 random4" 40 | "random3 random3 . . . . . . . . . . random4 random4" 41 | "cell20 . cell41 cell42 cell43 cell44 cell45 cell46 cell47 cell48 cell49 cell50 . cell31" 42 | "cell19 . cell76 middle middle middle middle middle middle middle middle cell51 . cell32" 43 | "cell18 . cell75 middle middle middle middle middle middle middle middle cell52 . cell33" 44 | "cell17 . cell74 middle middle middle middle middle middle middle middle cell53 . cell34" 45 | "cell16 . cell73 middle middle middle middle middle middle middle middle cell54 . cell35" 46 | "cell15 . cell72 middle middle middle middle middle middle middle middle cell55 . cell36" 47 | "cell14 . cell71 middle middle middle middle middle middle middle middle cell56 . cell37" 48 | "cell13 . cell70 middle middle middle middle middle middle middle middle cell57 . cell38" 49 | "cell12 . cell69 middle middle middle middle middle middle middle middle cell58 . cell39" 50 | "cell11 . cell68 cell67 cell66 cell65 cell64 cell63 cell62 cell61 cell60 cell59 . cell40" 51 | "random2 random2 . . . . . . . . . . random1 random1" 52 | "random2 random2 cell10 cell9 cell8 cell7 cell6 cell5 cell4 cell3 cell2 cell1 random1 random1"; 53 | `; 54 | 55 | const Points = styled.div` 56 | font-size: 1.5rem; 57 | font-weight: bold; 58 | text-align: center; 59 | margin-top: 50px; 60 | `; 61 | 62 | export default () => { 63 | const { user, setUser } = useContext(AuthContext); 64 | 65 | const [selectedTile, setSelectedTile] = useState( 66 | user ? user.currentTileId - 1 : 0 67 | ); 68 | const [userState, setUserState] = useState(""); 69 | const [mysteryTileOpen, setMysteryTileOpen] = useState(false); 70 | const [vTiles, setVTiles] = useState([]); 71 | const [reload, setReload] = useState(false); 72 | 73 | React.useEffect(() => { 74 | async function ftch() { 75 | const r = await (await fetch(api("/api/play/ping"))).json(); 76 | 77 | console.log({ r }); 78 | 79 | setUserState(r.userState); 80 | setSelectedTile(r.user.currentTileId - 1); 81 | setVTiles(r.vTiles); 82 | } 83 | 84 | ftch(); 85 | }, [reload]); 86 | 87 | const Component = UserStates[userState]; 88 | 89 | return ( 90 | <> 91 | Points: {user?.points} 92 | 93 | 94 | 104 | 105 | 106 | 116 | 117 | ); 118 | }; 119 | -------------------------------------------------------------------------------- /frontend/src/components/Play/Grid/middle.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | 4 | const Container = styled.div` 5 | grid-area: middle; 6 | display: flex; 7 | flex-direction: column; 8 | justify-content: center; 9 | align-items: center; 10 | font-size: 4rem; 11 | font-weight: 500; 12 | `; 13 | 14 | const NavLogoSvg = styled.svg` 15 | height: 250px; 16 | width: auto; 17 | color: inherit; 18 | transform: translateX(-2vh); 19 | color: ${(p) => (p.selected ? "#fff" : "#545454")}; 20 | `; 21 | 22 | export default ({ selected }) => ( 23 | 24 | 32 | 39 | 46 | 54 | 61 | 68 | 69 | 70 | ); 71 | -------------------------------------------------------------------------------- /frontend/src/components/Play/Grid/tile-data.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { type: "GO", gridArea: "random1" }, 3 | { type: "LEVEL", gridArea: "cell1", number: 1 }, 4 | { type: "RAND_CHANCE", gridArea: "cell2", number: 2 }, 5 | { type: "LEVEL", gridArea: "cell3", number: 3 }, 6 | { type: "LEVEL", gridArea: "cell4", number: 4 }, 7 | { type: "RAND_PERSON", gridArea: "cell5", number: 5 }, 8 | { type: "STORY", gridArea: "cell6", number: 6 }, 9 | { type: "LEVEL", gridArea: "cell7", number: 7 }, 10 | { type: "RAND_CHANCE", gridArea: "cell8", number: 8 }, 11 | { type: "LEVEL", gridArea: "cell9", number: 9 }, 12 | { type: "LEVEL", gridArea: "cell10", number: 10 }, 13 | { type: "JAIL", gridArea: "random2" }, 14 | { type: "LEVEL", gridArea: "cell11", number: 11 }, 15 | { type: "RAND_PERSON", gridArea: "cell12", number: 12 }, 16 | { type: "LEVEL", gridArea: "cell13", number: 13 }, 17 | { type: "LEVEL", gridArea: "cell14", number: 14 }, 18 | { type: "RAND_CHANCE", gridArea: "cell15", number: 15 }, 19 | { type: "STORY", gridArea: "cell16", number: 16 }, 20 | { type: "LEVEL", gridArea: "cell17", number: 17 }, 21 | { type: "LEVEL", gridArea: "cell18", number: 18 }, 22 | { type: "RAND_PERSON", gridArea: "cell19", number: 19 }, 23 | { type: "LEVEL", gridArea: "cell20", number: 20 }, 24 | { type: "GATE", gridArea: "random3" }, 25 | { type: "LEVEL", gridArea: "cell21", number: 21 }, 26 | { type: "RAND_CHANCE", gridArea: "cell22", number: 22 }, 27 | { type: "LEVEL", gridArea: "cell23", number: 23 }, 28 | { type: "LEVEL", gridArea: "cell24", number: 24 }, 29 | { type: "RAND_PERSON", gridArea: "cell25", number: 25 }, 30 | { type: "STORY", gridArea: "cell26", number: 26 }, 31 | { type: "LEVEL", gridArea: "cell27", number: 27 }, 32 | { type: "RAND_CHANCE", gridArea: "cell28", number: 28 }, 33 | { type: "LEVEL", gridArea: "cell29", number: 29 }, 34 | { type: "LEVEL", gridArea: "cell30", number: 30 }, 35 | { type: "MYSTERY", gridArea: "random4" }, 36 | { type: "LEVEL", gridArea: "cell31", number: 31 }, 37 | { type: "RAND_CHANCE", gridArea: "cell32", number: 32 }, 38 | { type: "LEVEL", gridArea: "cell33", number: 33 }, 39 | { type: "LEVEL", gridArea: "cell34", number: 34 }, 40 | { type: "STORY", gridArea: "cell35", number: 35 }, 41 | { type: "RAND_PERSON", gridArea: "cell36", number: 36 }, 42 | { type: "LEVEL", gridArea: "cell37", number: 37 }, 43 | { type: "RAND_CHANCE", gridArea: "cell38", number: 38 }, 44 | { type: "LEVEL", gridArea: "cell39", number: 39 }, 45 | { type: "LEVEL", gridArea: "cell40", number: 40 }, 46 | { type: "STORY", gridArea: "cell41", number: 41 }, 47 | { type: "LEVEL", gridArea: "cell42", number: 42 }, 48 | { type: "RAND_CHANCE", gridArea: "cell43", number: 43 }, 49 | { type: "LEVEL", gridArea: "cell44", number: 44 }, 50 | { type: "LEVEL", gridArea: "cell45", number: 45 }, 51 | { type: "RAND_PERSON", gridArea: "cell46", number: 46 }, 52 | { type: "LEVEL", gridArea: "cell47", number: 47 }, 53 | { type: "RAND_CHANCE", gridArea: "cell48", number: 48 }, 54 | { type: "LEVEL", gridArea: "cell49", number: 49 }, 55 | { type: "STORY", gridArea: "cell50", number: 50 }, 56 | { type: "LEVEL", gridArea: "cell51", number: 51 }, 57 | { type: "RAND_CHANCE", gridArea: "cell52", number: 52 }, 58 | { type: "LEVEL", gridArea: "cell53", number: 53 }, 59 | { type: "LEVEL", gridArea: "cell54", number: 54 }, 60 | { type: "RAND_PERSON", gridArea: "cell55", number: 55 }, 61 | { type: "LEVEL", gridArea: "cell56", number: 56 }, 62 | { type: "RAND_CHANCE", gridArea: "cell57", number: 57 }, 63 | { type: "LEVEL", gridArea: "cell58", number: 58 }, 64 | { type: "STORY", gridArea: "cell59", number: 59 }, 65 | { type: "LEVEL", gridArea: "cell60", number: 60 }, 66 | { type: "RAND_CHANCE", gridArea: "cell61", number: 61 }, 67 | { type: "LEVEL", gridArea: "cell62", number: 62 }, 68 | { type: "LEVEL", gridArea: "cell63", number: 63 }, 69 | { type: "RAND_PERSON", gridArea: "cell64", number: 64 }, 70 | { type: "LEVEL", gridArea: "cell65", number: 65 }, 71 | { type: "RAND_CHANCE", gridArea: "cell66", number: 66 }, 72 | { type: "LEVEL", gridArea: "cell67", number: 67 }, 73 | { type: "STORY", gridArea: "cell68", number: 68 }, 74 | { type: "LEVEL", gridArea: "cell69", number: 69 }, 75 | { type: "LEVEL", gridArea: "cell70", number: 70 }, 76 | { type: "RAND_CHANCE", gridArea: "cell71", number: 71 }, 77 | { type: "LEVEL", gridArea: "cell72", number: 72 }, 78 | { type: "LEVEL", gridArea: "cell73", number: 73 }, 79 | { type: "LEVEL", gridArea: "cell74", number: 74 }, 80 | { type: "LEVEL", gridArea: "cell75", number: 75 }, 81 | { type: "GATEI", gridArea: "cell76", number: 76 }, 82 | { type: "CENTER", gridArea: "middle" }, 83 | ]; 84 | -------------------------------------------------------------------------------- /frontend/src/components/Play/Grid/tile.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { useToasts } from "react-toast-notifications"; 4 | import swal from "sweetalert"; 5 | 6 | import tiles from "./tile-data"; 7 | import api from "../../../lib/api"; 8 | import Middle from "./middle"; 9 | 10 | export const Random = styled.div` 11 | --color: ${(props) => (props.selected ? "#2977f5" : "#545454")}; 12 | 13 | border: 2px solid var(--color); 14 | color: var(--color); 15 | font-size: 2rem; 16 | text-transform: uppercase; 17 | font-weight: bold; 18 | letter-spacing: 2px; 19 | display: flex; 20 | justify-content: flex-start; 21 | align-items: flex-end; 22 | padding: 10px; 23 | 24 | @media screen and (max-width: 930px) { 25 | font-size: 1.5rem; 26 | } 27 | `; 28 | 29 | export const Tile = styled.div` 30 | --color: ${(props) => 31 | props.selected ? "#2977f5" : props.visited ? "#3a3a3a" : "#545454"}; 32 | 33 | border: 2px solid var(--color); 34 | color: var(--color); 35 | font-size: 0.9rem; 36 | text-transform: uppercase; 37 | font-weight: bold; 38 | letter-spacing: 2px; 39 | display: flex; 40 | justify-content: flex-start; 41 | align-items: flex-end; 42 | padding: 5px; 43 | cursor: ${(props) => (props.pointer ? "pointer" : "default")}; 44 | 45 | @media screen and (max-width: 930px) { 46 | font-size: 0.8rem; 47 | font-weight: 600; 48 | } 49 | `; 50 | 51 | const shorten = { 52 | LEVEL: "LVL", 53 | RAND_PERSON: "RP", 54 | RAND_CHANCE: "RC", 55 | STORY: "ST", 56 | GATEI: "GT", 57 | }; 58 | 59 | export const RenderTiles = ({ selectedTile, vTiles, user }) => { 60 | const { addToast } = useToasts(); 61 | 62 | const handleClick = (t, visited, i) => async () => { 63 | try { 64 | if (t.type !== "STORY" || !visited) return; 65 | 66 | const r = await (await fetch(api(`/api/play/story/${i + 1}`))).json(); 67 | 68 | if (r.tile?.story) { 69 | const div = document.createElement("div"); 70 | div.innerHTML = r.tile.story; 71 | div.style.textAlign = "left"; 72 | div.style.color = "#333"; 73 | 74 | swal({ 75 | title: `Story #${r.tile?.number}`, 76 | content: div, 77 | html: true, 78 | }); 79 | } 80 | 81 | console.log({ tile: r }); 82 | } catch (e) { 83 | addToast(e.message, { appearance: "error" }); 84 | } 85 | }; 86 | 87 | return ( 88 | <> 89 | {tiles.map((t, i) => 90 | t.gridArea.startsWith("cell") ? ( 91 | 99 | {t.number} {shorten[t.type]} 100 | 101 | ) : t.gridArea === "middle" ? ( 102 | 103 | ) : ( 104 | 110 | {t.type.substring(0, 4)} 111 | 112 | ) 113 | )} 114 | 115 | ); 116 | }; 117 | -------------------------------------------------------------------------------- /frontend/src/components/Play/States/Finished.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | 4 | const Container = styled.div` 5 | height: 30vh; 6 | width: 100%; 7 | display: flex; 8 | flex-direction: column; 9 | justify-content: center; 10 | align-items: center; 11 | `; 12 | 13 | const TableFlip = styled.div` 14 | font-size: 1.5rem; 15 | padding-bottom: 20px; 16 | `; 17 | 18 | const Line = styled.div` 19 | font-size: 1.2rem; 20 | `; 21 | 22 | export default () => ( 23 | 24 | (ノಠ益ಠ)ノ彡┻━┻ 25 | Fin 26 | 27 | ); 28 | -------------------------------------------------------------------------------- /frontend/src/components/Play/States/Gate.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import styled from "styled-components"; 3 | import { useToasts } from "react-toast-notifications"; 4 | import { Button } from "../../forms"; 5 | import api from "../../../lib/api"; 6 | 7 | const Container = styled.div` 8 | margin: 50px auto; 9 | max-width: 350px; 10 | min-width: 250px; 11 | width: 100%; 12 | `; 13 | 14 | const Heading = styled.div` 15 | font-size: 3rem; 16 | font-weight: bolder; 17 | margin-bottom: 20px; 18 | `; 19 | 20 | const ButtonContainer = styled.div` 21 | width: 100%; 22 | display: flex; 23 | justify-content: space-between; 24 | `; 25 | 26 | export default ({ 27 | selectedTile, 28 | setSelectedTile, 29 | setUser, 30 | reload, 31 | setReload, 32 | }) => { 33 | const [allowedIn, setAllowedIn] = useState(false); 34 | const [sub, setSub] = useState(false); 35 | const { addToast } = useToasts(); 36 | 37 | useEffect(() => { 38 | async function f() { 39 | const r = await (await fetch(api("/api/play/gate/in"))).json(); 40 | 41 | console.log({ r }); 42 | 43 | if (r.allowed) { 44 | setAllowedIn(true); 45 | } 46 | } 47 | 48 | f(); 49 | }, []); 50 | 51 | const handleMove = (goIn, setSub) => async () => { 52 | setSub(true); 53 | const mv = await ( 54 | await fetch(api("/api/play/move"), { 55 | method: "post", 56 | headers: { 57 | "Content-Type": "application/json", 58 | }, 59 | body: JSON.stringify({ goIn }), 60 | }) 61 | ).json(); 62 | 63 | if (mv.success) { 64 | addToast(mv.message, { appearance: "success" }); 65 | } else { 66 | addToast(mv.message, { appearance: "error" }); 67 | } 68 | 69 | setUser(mv.user); 70 | setReload(!reload); 71 | setSelectedTile(mv.user.currentTileId - 1); 72 | console.log({ mv }); 73 | setSub(false); 74 | }; 75 | 76 | return ( 77 | 78 | Gate 79 | 80 | 83 | 86 | 87 | 88 | ); 89 | }; 90 | -------------------------------------------------------------------------------- /frontend/src/components/Play/States/GateI.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import styled from "styled-components"; 3 | import { useToasts } from "react-toast-notifications"; 4 | import { Button } from "../../forms"; 5 | import api from "../../../lib/api"; 6 | 7 | const Container = styled.div` 8 | margin: 50px auto; 9 | max-width: 350px; 10 | min-width: 250px; 11 | width: 100%; 12 | `; 13 | 14 | const Heading = styled.div` 15 | font-size: 3rem; 16 | font-weight: bolder; 17 | margin-bottom: 20px; 18 | `; 19 | 20 | const ButtonContainer = styled.div` 21 | width: 100%; 22 | display: flex; 23 | justify-content: space-between; 24 | `; 25 | 26 | export default ({ 27 | selectedTile, 28 | setSelectedTile, 29 | setUser, 30 | reload, 31 | setReload, 32 | }) => { 33 | const [sub, setSub] = useState(false); 34 | const { addToast } = useToasts(); 35 | 36 | const handleMove = (goOut, setSub) => async () => { 37 | try { 38 | setSub(true); 39 | const mv = await ( 40 | await fetch(api("/api/play/move"), { 41 | method: "post", 42 | headers: { 43 | "Content-Type": "application/json", 44 | }, 45 | body: JSON.stringify({ goOut }), 46 | }) 47 | ).json(); 48 | 49 | if (mv.success) { 50 | addToast(mv.message, { appearance: "success" }); 51 | } else { 52 | addToast(mv.message, { appearance: "error" }); 53 | } 54 | 55 | setUser(mv.user); 56 | setReload(!reload); 57 | setSelectedTile(mv.user.currentTileId - 1); 58 | console.log({ mv }); 59 | setSub(false); 60 | } catch (e) { 61 | addToast(e.message, { appearance: "error" }); 62 | setSub(false); 63 | } 64 | }; 65 | 66 | return ( 67 | 68 | Gate 69 | 70 | 73 | 76 | 77 | 78 | ); 79 | }; 80 | -------------------------------------------------------------------------------- /frontend/src/components/Play/States/Guard.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import styled from "styled-components"; 3 | import { useToasts } from "react-toast-notifications"; 4 | import { Button } from "../../forms"; 5 | import api from "../../../lib/api"; 6 | 7 | const Container = styled.div` 8 | margin: 50px auto; 9 | max-width: 350px; 10 | min-width: 250px; 11 | width: 100%; 12 | `; 13 | 14 | const Heading = styled.div` 15 | font-size: 3rem; 16 | font-weight: bolder; 17 | margin-bottom: 20px; 18 | `; 19 | 20 | const ButtonContainer = styled.div` 21 | width: 100%; 22 | display: flex; 23 | justify-content: space-between; 24 | `; 25 | 26 | export default ({ 27 | selectedTile, 28 | setSelectedTile, 29 | user, 30 | setUser, 31 | reload, 32 | setReload, 33 | }) => { 34 | const [sub, setSub] = useState(false); 35 | const { addToast } = useToasts(); 36 | 37 | const handleBribe = async () => { 38 | setSub(true); 39 | const mv = await ( 40 | await fetch(api("/api/play/guard/bribe"), { 41 | method: "post", 42 | }) 43 | ).json(); 44 | 45 | if (mv.success) { 46 | if (mv.message.match(/lost/)) { 47 | addToast(mv.message, { appearance: "error" }); 48 | } else { 49 | addToast(mv.message, { appearance: "success" }); 50 | } 51 | } else { 52 | addToast(mv.message, { appearance: "error" }); 53 | } 54 | 55 | setUser(mv.user); 56 | setSelectedTile(mv.user.currentTileId - 1); 57 | setReload(!reload); 58 | console.log({ mv }); 59 | setSub(false); 60 | }; 61 | 62 | const handleFight = async () => { 63 | setSub(true); 64 | const mv = await ( 65 | await fetch(api("/api/play/guard/fight"), { 66 | method: "post", 67 | }) 68 | ).json(); 69 | 70 | if (mv.success) { 71 | if (mv.message.match(/lost/)) { 72 | addToast(mv.message, { appearance: "error" }); 73 | } else { 74 | addToast(mv.message, { appearance: "success" }); 75 | } 76 | } else { 77 | addToast(mv.message, { appearance: "error" }); 78 | } 79 | 80 | setUser(mv.user); 81 | setReload(!reload); 82 | setSelectedTile(mv.user.currentTileId - 1); 83 | console.log({ mv }); 84 | setSub(false); 85 | }; 86 | 87 | return ( 88 | 89 | Guard 90 | 91 | {/* TODO: add confirmation, this costs 125 points */} 92 | 95 | 98 | 99 | 100 | ); 101 | }; 102 | -------------------------------------------------------------------------------- /frontend/src/components/Play/States/Jail.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import styled from "styled-components"; 3 | import { useToasts } from "react-toast-notifications"; 4 | import { Button } from "../../forms"; 5 | import api from "../../../lib/api"; 6 | 7 | const Container = styled.div` 8 | margin: 50px auto; 9 | max-width: 350px; 10 | min-width: 250px; 11 | width: 100%; 12 | `; 13 | 14 | const Heading = styled.div` 15 | font-size: 3rem; 16 | font-weight: bolder; 17 | margin-bottom: 20px; 18 | `; 19 | 20 | export default ({ setSelectedTile, setUser, reload, setReload }) => { 21 | const [sub, setSub] = useState(false); 22 | const { addToast } = useToasts(); 23 | 24 | const handleBribe = async () => { 25 | setSub(true); 26 | const mv = await ( 27 | await fetch(api("/api/play/jail/bribe"), { 28 | method: "post", 29 | }) 30 | ).json(); 31 | 32 | if (mv.success) { 33 | addToast(mv.message, { appearance: "success" }); 34 | } else { 35 | addToast(mv.message, { appearance: "error" }); 36 | } 37 | 38 | setUser(mv.user); 39 | setReload(!reload); 40 | setSelectedTile(mv.user.currentTileId - 1); 41 | console.log({ mv }); 42 | setSub(false); 43 | }; 44 | 45 | return ( 46 | 47 | You are in Jail 48 | {/* TODO: add confirmation, this costs 50 points */} 49 | 52 | 53 | ); 54 | }; 55 | -------------------------------------------------------------------------------- /frontend/src/components/Play/States/MinRiddle.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { useToasts } from "react-toast-notifications"; 4 | import { Button } from "../../forms"; 5 | import api from "../../../lib/api"; 6 | 7 | const ButtonContainer = styled.div` 8 | width: 100%; 9 | height: 30vh; 10 | min-height: 400px; 11 | display: flex; 12 | flex-direction: column; 13 | justify-content: center; 14 | align-items: center; 15 | `; 16 | 17 | const Heading = styled.div` 18 | font-weight: bold; 19 | font-size: 1.5rem; 20 | text-align: center; 21 | margin-bottom: 30px; 22 | `; 23 | 24 | export const handleMove = ( 25 | setUser, 26 | setSelectedTile, 27 | setSub, 28 | addToast, 29 | reload, 30 | setReload 31 | ) => async () => { 32 | setSub(true); 33 | const mv = await ( 34 | await fetch(api("/api/play/move"), { method: "post" }) 35 | ).json(); 36 | setUser(mv.user); 37 | setSelectedTile(mv.user.currentTileId - 1); 38 | 39 | if (mv.success) { 40 | addToast(mv.message, { appearance: "success" }); 41 | } else { 42 | addToast(mv.message, { appearance: "error" }); 43 | } 44 | 45 | setReload(!reload); 46 | setSub(false); 47 | }; 48 | 49 | export default ({ setUser, setSelectedTile, reload, setReload }) => { 50 | const [sub, setSub] = React.useState(false); 51 | const { addToast } = useToasts(); 52 | 53 | return ( 54 | 55 | 56 | You have been offered a Riddle! Screenshot this message and your grid 57 | and send it to an admin to earn extra points. 58 | 59 | 72 | 73 | ); 74 | }; 75 | -------------------------------------------------------------------------------- /frontend/src/components/Play/States/Move.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { useToasts } from "react-toast-notifications"; 4 | import { Button } from "../../forms"; 5 | import api from "../../../lib/api"; 6 | 7 | const ButtonContainer = styled.div` 8 | width: 100%; 9 | height: 30vh; 10 | display: flex; 11 | justify-content: center; 12 | align-items: center; 13 | `; 14 | 15 | export const handleMove = ( 16 | setUser, 17 | setSelectedTile, 18 | setSub, 19 | addToast, 20 | reload, 21 | setReload 22 | ) => async () => { 23 | setSub(true); 24 | const mv = await ( 25 | await fetch(api("/api/play/move"), { method: "post" }) 26 | ).json(); 27 | setUser(mv.user); 28 | setSelectedTile(mv.user.currentTileId - 1); 29 | 30 | if (mv.success) { 31 | addToast(mv.message, { appearance: "success" }); 32 | } else { 33 | addToast(mv.message, { appearance: "error" }); 34 | } 35 | 36 | setSub(false); 37 | setReload(!reload); 38 | }; 39 | 40 | export default ({ setUser, setSelectedTile, reload, setReload }) => { 41 | const [sub, setSub] = React.useState(false); 42 | const { addToast } = useToasts(); 43 | 44 | return ( 45 | 46 | 59 | 60 | ); 61 | }; 62 | -------------------------------------------------------------------------------- /frontend/src/components/Play/States/MoveWithStory.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import styled from "styled-components"; 3 | import { useToasts } from "react-toast-notifications"; 4 | import { Button } from "../../forms"; 5 | import Story from "../Widgets/story"; 6 | import api from "../../../lib/api"; 7 | 8 | const ButtonContainer = styled.div` 9 | width: 100%; 10 | height: 30vh; 11 | display: flex; 12 | justify-content: center; 13 | align-items: center; 14 | `; 15 | 16 | export const handleMove = ( 17 | setUser, 18 | setSelectedTile, 19 | setSub, 20 | addToast, 21 | reload, 22 | setReload 23 | ) => async () => { 24 | setSub(true); 25 | const mv = await ( 26 | await fetch(api("/api/play/move"), { method: "post" }) 27 | ).json(); 28 | setUser(mv.user); 29 | setSelectedTile(mv.user.currentTileId - 1); 30 | 31 | if (mv.success) { 32 | addToast(mv.message, { appearance: "success" }); 33 | } else { 34 | addToast(mv.message, { appearance: "error" }); 35 | } 36 | 37 | setReload(!reload); 38 | setSub(false); 39 | }; 40 | 41 | export default ({ 42 | setUser, 43 | setSelectedTile, 44 | selectedTile, 45 | reload, 46 | setReload, 47 | }) => { 48 | const [story, setStory] = useState(""); 49 | const [sub, setSub] = useState(false); 50 | const { addToast } = useToasts(); 51 | 52 | useEffect(() => { 53 | async function f() { 54 | const r = await ( 55 | await fetch(api("/api/play/story/") + (selectedTile + 1)) 56 | ).json(); 57 | 58 | if (r.tile) { 59 | setStory(r.tile.story); 60 | } 61 | } 62 | 63 | f(); 64 | }); 65 | 66 | return ( 67 | <> 68 | 69 | 82 | 83 | 84 | 85 | ); 86 | }; 87 | -------------------------------------------------------------------------------- /frontend/src/components/Play/States/Mystery.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import styled from "styled-components"; 3 | import { useToasts } from "react-toast-notifications"; 4 | import { Button } from "../../forms"; 5 | import api from "../../../lib/api"; 6 | 7 | const ButtonContainer = styled.div` 8 | width: 100%; 9 | height: 30vh; 10 | display: flex; 11 | flex-direction: column; 12 | justify-content: center; 13 | align-items: center; 14 | `; 15 | 16 | const Heading = styled.div` 17 | font-weight: bold; 18 | font-size: 1.5rem; 19 | text-align: center; 20 | margin-bottom: 30px; 21 | `; 22 | 23 | export const handleMove = ( 24 | setUser, 25 | setSelectedTile, 26 | setSub, 27 | addToast, 28 | reload, 29 | setReload 30 | ) => async () => { 31 | setSub(true); 32 | const mv = await ( 33 | await fetch(api("/api/play/move"), { method: "post" }) 34 | ).json(); 35 | setUser(mv.user); 36 | setSelectedTile(mv.user.currentTileId - 1); 37 | 38 | if (mv.success) { 39 | addToast(mv.message, { appearance: "success" }); 40 | } else { 41 | addToast(mv.message, { appearance: "error" }); 42 | } 43 | 44 | setReload(!reload); 45 | setSub(false); 46 | }; 47 | 48 | export default ({ 49 | setUser, 50 | setSelectedTile, 51 | mysteryTileOpen, 52 | reload, 53 | setReload, 54 | }) => { 55 | const [sub, setSub] = useState(false); 56 | const { addToast } = useToasts(); 57 | 58 | return ( 59 | 60 | 61 | {mysteryTileOpen 62 | ? "The Mystery Tile is open. Please contact an admin for more information." 63 | : "The mystery tile is closed! Please move forward"} 64 | 65 | 78 | 79 | ); 80 | }; 81 | -------------------------------------------------------------------------------- /frontend/src/components/Play/States/Riddle.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { withToastManager } from "react-toast-notifications"; 4 | 5 | import { Button } from "../../forms"; 6 | import api from "../../../lib/api"; 7 | 8 | const Big = styled.div` 9 | width: 100%; 10 | margin-top: 20px; 11 | display: flex; 12 | justify-content: center; 13 | align-items: center; 14 | margin-bottom: 50px; 15 | `; 16 | 17 | const StyledField = styled.input` 18 | border: 2px solid #545454; 19 | /* border-radius: 3px; */ 20 | padding: 15px 0; 21 | text-indent: 20px; 22 | font-size: 1.1rem; 23 | background: #23272a; 24 | width: 95%; 25 | transition: border-color 0.3s ease; 26 | color: #fff; 27 | 28 | &:focus, 29 | &:active { 30 | border-color: #2977f5; 31 | outline: none; 32 | } 33 | 34 | &::placeholder { 35 | color: #686d71; 36 | } 37 | `; 38 | 39 | const Container = styled.div` 40 | min-width: 320px; 41 | width: 20vw; 42 | padding: 30px; 43 | border-radius: 5px; 44 | background: #484f54; 45 | 46 | box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 47 | 0 4px 6px -2px rgba(0, 0, 0, 0.05); 48 | 49 | @media screen and (max-width: 768px) { 50 | width: 90vw; 51 | } 52 | `; 53 | 54 | const Title = styled.div` 55 | font-weight: 600; 56 | font-size: 30px; 57 | `; 58 | 59 | const Number = styled.div` 60 | font-size: 25px; 61 | opacity: 0.5; 62 | `; 63 | 64 | const Text = styled.div` 65 | font-size: 18px; 66 | `; 67 | 68 | class Riddle extends React.Component { 69 | constructor() { 70 | super(); 71 | this.state = { 72 | riddle: {}, 73 | loading: true, 74 | answer: "", 75 | submitting: false, 76 | }; 77 | } 78 | 79 | async componentDidMount() { 80 | try { 81 | this.setState({ submitting: true }); 82 | const r = await (await fetch(api("/api/play/riddle"))).json(); 83 | 84 | console.log({ lvl: r }, r.lvl); 85 | 86 | if (r.riddle) { 87 | this.setState({ loading: false, riddle: r.riddle }); 88 | } 89 | this.setState({ submitting: false }); 90 | } catch (e) { 91 | console.error(e); 92 | } 93 | } 94 | 95 | async handleSubmit() { 96 | try { 97 | this.setState({ submitting: true }); 98 | const r = await ( 99 | await fetch(api("/api/play/riddle/answer"), { 100 | method: "POST", 101 | headers: { "Content-Type": "application/json" }, 102 | body: JSON.stringify({ answer: this.state.answer }), 103 | }) 104 | ).json(); 105 | 106 | if (r.user) { 107 | this.props.setUser(r.user); 108 | this.props.setSelectedTile(r.user.currentTileId - 1); 109 | } 110 | 111 | this.setState({ answer: "" }); 112 | this.props.setReload(!this.props.reload); 113 | 114 | if (r.success) { 115 | this.props.toastManager.add("Riddle solved", { 116 | appearance: "success", 117 | }); 118 | } else { 119 | this.props.toastManager.add(r.message, { 120 | appearance: "error", 121 | }); 122 | } 123 | 124 | console.log({ r }); 125 | this.setState({ submitting: false }); 126 | } catch (e) { 127 | console.error(e); 128 | } 129 | } 130 | 131 | render() { 132 | return ( 133 | 134 | 135 |
143 | Riddle 144 |
145 | 152 |
153 | this.setState({ answer: e.target.value })} 159 | /> 160 |
161 | 168 |
169 |
170 | ); 171 | } 172 | } 173 | 174 | export default withToastManager(Riddle); 175 | -------------------------------------------------------------------------------- /frontend/src/components/Play/States/SideQuest.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { useToasts } from "react-toast-notifications"; 4 | import { Button } from "../../forms"; 5 | import api from "../../../lib/api"; 6 | 7 | const ButtonContainer = styled.div` 8 | width: 100%; 9 | height: 30vh; 10 | min-height: 400px; 11 | display: flex; 12 | flex-direction: column; 13 | justify-content: center; 14 | align-items: center; 15 | `; 16 | 17 | const Heading = styled.div` 18 | font-weight: bold; 19 | font-size: 1.5rem; 20 | text-align: center; 21 | margin-bottom: 30px; 22 | `; 23 | 24 | export const handleMove = ( 25 | setUser, 26 | setSelectedTile, 27 | setSub, 28 | addToast, 29 | reload, 30 | setReload 31 | ) => async () => { 32 | setSub(true); 33 | const mv = await ( 34 | await fetch(api("/api/play/move"), { method: "post" }) 35 | ).json(); 36 | setUser(mv.user); 37 | setSelectedTile(mv.user.currentTileId - 1); 38 | 39 | if (mv.success) { 40 | addToast(mv.message, { appearance: "success" }); 41 | } else { 42 | addToast(mv.message, { appearance: "error" }); 43 | } 44 | 45 | setReload(!reload); 46 | setSub(false); 47 | }; 48 | 49 | export default ({ setUser, setSelectedTile, reload, setReload }) => { 50 | const [sub, setSub] = React.useState(false); 51 | const { addToast } = useToasts(); 52 | 53 | return ( 54 | 55 | 56 | You have been offered a SideQuest! Screenshot this message and your grid 57 | and send it to an admin to earn extra points. 58 | 59 | 72 | 73 | ); 74 | }; 75 | -------------------------------------------------------------------------------- /frontend/src/components/Play/States/SkipRiddle.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import styled from "styled-components"; 3 | import { useToasts } from "react-toast-notifications"; 4 | import { Button } from "../../forms"; 5 | import api from "../../../lib/api"; 6 | 7 | const Container = styled.div` 8 | margin: 50px auto; 9 | max-width: 450px; 10 | width: 100%; 11 | `; 12 | 13 | const ButtonContainer = styled.div` 14 | width: 100%; 15 | display: flex; 16 | justify-content: space-between; 17 | `; 18 | 19 | export default ({ setUser, setSelectedTile, setReload, reload }) => { 20 | const [sub, setSub] = useState(false); 21 | const { addToast } = useToasts(); 22 | 23 | const handleMove = async () => { 24 | setSub(true); 25 | const mv = await ( 26 | await fetch(api("/api/play/move"), { 27 | method: "post", 28 | body: JSON.stringify({ skip: true }), 29 | }) 30 | ).json(); 31 | setUser(mv.user); 32 | setSelectedTile(mv.user.currentTileId - 1); 33 | 34 | if (mv.success) { 35 | addToast(mv.message, { appearance: "success" }); 36 | } else { 37 | addToast(mv.message, { appearance: "error" }); 38 | } 39 | 40 | setReload(!reload); 41 | setSub(false); 42 | }; 43 | 44 | const handleAccept = async () => { 45 | setSub(true); 46 | const mv = await ( 47 | await fetch(api("/api/play/riddle/accept"), { 48 | method: "post", 49 | }) 50 | ).json(); 51 | setUser(mv.user); 52 | setSelectedTile(mv.user.currentTileId - 1); 53 | 54 | if (mv.success) { 55 | addToast(mv.message, { appearance: "success" }); 56 | } else { 57 | addToast(mv.message, { appearance: "error" }); 58 | } 59 | 60 | setReload(!reload); 61 | setSub(false); 62 | }; 63 | 64 | return ( 65 | 66 | 67 | {/* TODO: add confirmation, "you will not see this riddle again and will not be able to gain extra points" */} 68 | 71 | 74 | 75 | 76 | ); 77 | }; 78 | -------------------------------------------------------------------------------- /frontend/src/components/Play/States/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Move from "./Move"; 3 | import Level from "../Widgets/level"; 4 | import MoveWithStory from "./MoveWithStory"; 5 | import Gate from "./Gate"; 6 | import GateI from "./GateI"; 7 | import SkipRiddle from "./SkipRiddle"; 8 | import Riddle from "./Riddle"; 9 | import Guard from "./Guard"; 10 | import Jail from "./Jail"; 11 | import Mystery from "./Mystery"; 12 | import SideQuest from "./SideQuest"; 13 | import MinRiddle from "./MinRiddle"; 14 | import Finished from "./Finished"; 15 | 16 | export default { 17 | "": () =>
Loading...
, 18 | "visited-moveable": Move, 19 | "go-moveable": Move, 20 | "story-moveable": MoveWithStory, 21 | "levelsolved-moveable": Move, 22 | level: Level, 23 | "randchance-moveable": Move, 24 | "rp-riddle-skippable": MinRiddle, 25 | "rp-riddle-moveable": Move, 26 | "rp-riddle": MinRiddle, 27 | "rp-moveable": Move, 28 | "rp-guard": Guard, 29 | "rp-guard-moveable": Move, 30 | "rp-sidequest-skippable": SideQuest, 31 | jail: Jail, 32 | "jailvisiting-moveable": Move, 33 | "gate-moveable": Gate, 34 | "gatei-moveable": GateI, 35 | "mystery-moveable": Mystery, 36 | mystery: Level, 37 | "mystery-completed-moveable": Move, 38 | center: Level, 39 | finished: Finished, 40 | }; 41 | -------------------------------------------------------------------------------- /frontend/src/components/Play/Widgets/story.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | 4 | const Big = styled.div` 5 | width: 100%; 6 | margin-bottom: 40px; 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | `; 11 | 12 | const Container = styled.div` 13 | max-width: 450px; 14 | min-width: 300px; 15 | width: 95%; 16 | padding: 30px; 17 | border-radius: 5px; 18 | background: #484f54; 19 | 20 | box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 21 | 0 4px 6px -2px rgba(0, 0, 0, 0.05); 22 | 23 | @media screen and (max-width: 768px) { 24 | width: 90vw; 25 | } 26 | `; 27 | 28 | const Title = styled.div` 29 | font-weight: 600; 30 | font-size: 30px; 31 | `; 32 | 33 | const Number = styled.div` 34 | font-size: 25px; 35 | opacity: 0.5; 36 | `; 37 | 38 | const Text = styled.div` 39 | font-size: 18px; 40 | `; 41 | 42 | export default ({ story }) => ( 43 | 44 | 45 |
53 | Story 54 |
55 | 56 |
57 |
58 | ); 59 | -------------------------------------------------------------------------------- /frontend/src/components/Register/form.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link, useHistory } from "react-router-dom"; 3 | import { Formik, Form } from "formik"; 4 | import { useToasts } from "react-toast-notifications"; 5 | import { useGoogleReCaptcha as useRecaptcha } from "react-google-recaptcha-v3"; 6 | import RegistrationSchema from "./register-schema"; 7 | import { ButtonContainer, Button, Field, Message } from "../forms"; 8 | import api from "../../lib/api"; 9 | 10 | export default () => { 11 | const { addToast } = useToasts(); 12 | const router = useHistory(); 13 | 14 | const { executeRecaptcha } = useRecaptcha(); 15 | 16 | const handleSubmit = async (values, { setSubmitting }) => { 17 | const recaptcha = await executeRecaptcha("login_page"); 18 | 19 | const data = { ...values, recaptcha }; 20 | 21 | fetch(api("/api/auth/register"), { 22 | method: "POST", 23 | headers: { 24 | "Content-Type": "application/json", 25 | }, 26 | body: JSON.stringify(data), 27 | }) 28 | .then((resp) => resp.json()) 29 | .then(({ success, message }) => { 30 | if (success) { 31 | router.push("/signin?fromreg=true", "/signin?fromreg=true", { 32 | shallow: true, 33 | }); 34 | } else { 35 | addToast(message, { appearance: "error" }); 36 | setSubmitting(false); 37 | } 38 | }) 39 | .catch((e) => { 40 | addToast(e.message, { appearance: "error" }); 41 | console.error(e); 42 | }); 43 | }; 44 | 45 | return ( 46 | 58 | {({ errors, touched, isSubmitting, isValid, isValidating, dirty }) => ( 59 |
60 | 68 | 76 | 84 | 92 | 100 | 108 | 109 | Already have an account? 110 | 111 | 112 | 118 | 119 | 120 | )} 121 |
122 | ); 123 | }; 124 | -------------------------------------------------------------------------------- /frontend/src/components/Register/register-schema.js: -------------------------------------------------------------------------------- 1 | const yup = require('yup') 2 | 3 | module.exports = yup.object().shape({ 4 | email: yup 5 | .string('Invalid email') 6 | .email('Invalid email') 7 | .required('Email is required'), 8 | username: yup 9 | .string('Invalid username') 10 | .required('Username is required') 11 | .min(3, 'Username must be longer than 3 characters') 12 | .max(20, "Username can't be longer than 20 characters") 13 | .matches( 14 | /^[a-zA-Z0-9_]*$/g, 15 | 'Username can only contain letters, digits and underscores' 16 | ), 17 | password: yup 18 | .string('Invalid password') 19 | .required('Password is required') 20 | .min(3, 'Password must be longer than 3 characters') 21 | .max(20, "Password can't be longer than 20 characters"), 22 | discord: yup 23 | .string('Invalid discord username') 24 | .required('Discord username is required') 25 | .matches(/^[^@#:]*#\d{4}$/gu, 'Invalid discord username') 26 | .matches(/^((?!```).)*$/gu, 'Invalid discord username') 27 | .notOneOf(['everyone', 'discordtag', 'here'], 'Invalid discord username'), 28 | edu: yup.string('Invalid educational innstitution'), 29 | name: yup 30 | .string('Invalid name') 31 | .min(3, 'Name must be over 3 characters') 32 | .max(255, 'Name must be less than 255 characters'), 33 | }) 34 | -------------------------------------------------------------------------------- /frontend/src/components/forms.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { Field as FormikField } from "formik"; 4 | 5 | export const FieldContainer = styled.div` 6 | width: 100%; 7 | margin: 30px 0; 8 | `; 9 | 10 | export const Message = styled.a` 11 | font-size: 0.9rem; 12 | color: #aaa; 13 | text-decoration: none; 14 | cursor: pointer; 15 | display: block; 16 | text-align: center; 17 | transition: color 0.15s ease; 18 | 19 | &:hover { 20 | color: #2977f5; 21 | } 22 | `; 23 | 24 | export const Label = styled.div` 25 | font-size: 0.7rem; 26 | text-transform: uppercase; 27 | letter-spacing: 1.3px; 28 | margin-bottom: 10px; 29 | `; 30 | 31 | export const Error = styled.div` 32 | font-size: 0.9rem; 33 | margin-top: 10px; 34 | color: #ff0000; 35 | `; 36 | 37 | export const StyledField = styled(FormikField)` 38 | border: 2px solid #545454; 39 | /* border-radius: 3px; */ 40 | padding: 15px 0; 41 | text-indent: 20px; 42 | font-size: 1.1rem; 43 | background: #23272a; 44 | width: 450px; 45 | transition: border-color 0.3s ease; 46 | color: #fff; 47 | 48 | &:focus, 49 | &:active { 50 | border-color: #2977f5; 51 | outline: none; 52 | } 53 | 54 | &::placeholder { 55 | color: #686d71; 56 | } 57 | 58 | @media screen and (max-width: 450px) { 59 | width: 300px; 60 | } 61 | `; 62 | 63 | export const ButtonContainer = styled.div` 64 | width: 100%; 65 | display: flex; 66 | justify-content: center; 67 | margin: 40px 0; 68 | `; 69 | 70 | export const Button = styled.button` 71 | border: none; 72 | padding: 15px 25px; 73 | /* border-radius: 3px; */ 74 | color: white; 75 | background: #2977f5; 76 | box-shadow: 0px 4px 35px rgba(0, 0, 0, 0.25); 77 | font-size: 1rem; 78 | text-transform: uppercase; 79 | font-weight: bold; 80 | letter-spacing: 2px; 81 | cursor: pointer; 82 | transition: box-shadow 0.3s ease, transform 0.3s ease; 83 | 84 | &:hover { 85 | box-shadow: 0px 7px 35px #2977f590; 86 | transform: translateY(-2px); 87 | outline: none; 88 | } 89 | 90 | &[disabled] { 91 | background: #aaa; 92 | cursor: default; 93 | } 94 | 95 | &[disabled]:hover { 96 | box-shadow: 0px 4px 35px rgba(0, 0, 0, 0.25); 97 | transform: none; 98 | outline: none; 99 | } 100 | `; 101 | 102 | export const Field = ({ label, name, type, errors, touched, placeholder }) => ( 103 | 104 | 105 | 106 | {errors[name] && touched[name] && {errors[name]}} 107 | 108 | ); 109 | -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ); 13 | 14 | // If you want your app to work offline and load faster, you can change 15 | // unregister() to register() below. Note this comes with some pitfalls. 16 | // Learn more about service workers: https://bit.ly/CRA-PWA 17 | serviceWorker.unregister(); 18 | -------------------------------------------------------------------------------- /frontend/src/lib/api.js: -------------------------------------------------------------------------------- 1 | export default (route) => route; 2 | // export default (route) => `${process.env.REACT_APP_API_ENDPOINT}${route}`; 3 | -------------------------------------------------------------------------------- /frontend/src/lib/auth-context.js: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | 3 | export default createContext(); 4 | -------------------------------------------------------------------------------- /frontend/src/pages/404.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useContext } from "react"; 2 | import styled from "styled-components"; 3 | import Layout from "../components/Layout"; 4 | 5 | const Container = styled.div` 6 | max-width: 840px; 7 | width: 100%; 8 | padding: 0 20px; 9 | margin: 0 auto; 10 | `; 11 | 12 | const Heading = styled.div` 13 | margin-top: 50px; 14 | font-size: 2.5rem; 15 | font-weight: bold; 16 | margin-bottom: 20px; 17 | `; 18 | 19 | const NotFound = () => { 20 | return ( 21 | <> 22 | 23 | Not Found 24 |

The page you requested could not be found.

25 |
26 | 27 | ); 28 | }; 29 | 30 | const FormatLayout = () => { 31 | return ( 32 | 38 | 39 | 40 | ); 41 | }; 42 | 43 | export default FormatLayout; 44 | -------------------------------------------------------------------------------- /frontend/src/pages/leaderboard.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import styled from "styled-components"; 3 | import Layout from "../components/Layout"; 4 | import api from "../lib/api"; 5 | 6 | const Container = styled.div` 7 | width: 100%; 8 | margin: 50px 0; 9 | `; 10 | 11 | const Table = styled.table` 12 | max-width: 1200px; 13 | width: 95%; 14 | border-collapse: collapse; 15 | margin: 0 auto; 16 | `; 17 | const Cell = styled.td` 18 | font-size: 1rem; 19 | padding: 15px 20px; 20 | text-align: center; 21 | `; 22 | 23 | const Header = styled.tr` 24 | background: #2977f5; 25 | * { 26 | color: #fff; 27 | text-transform: uppercase; 28 | font-weight: bold; 29 | font-size: 0.9rem; 30 | letter-spacing: 1px; 31 | } 32 | `; 33 | 34 | const HeaderCell = styled(Cell)` 35 | font-weight: bold; 36 | text-transform: uppercase; 37 | font-size: 0.8rem; 38 | letter-spacing: 1px; 39 | `; 40 | 41 | const Row = styled.tr` 42 | &:nth-child(odd) { 43 | background: #2977f520; 44 | } 45 | `; 46 | 47 | const Leaderboard = () => { 48 | const [users, setUsers] = useState([]); 49 | 50 | useEffect(() => { 51 | async function f() { 52 | const r = await (await fetch(api("/api/leaderboard/"))).json(); 53 | 54 | if (r.success) { 55 | setUsers(r.users); 56 | } 57 | } 58 | 59 | f(); 60 | }, []); 61 | 62 | return ( 63 | 64 | 65 |
66 | # 67 | Username 68 | Points 69 | Bounty 70 |
71 | {users.map((u, i) => ( 72 | 73 | {i + 1} 74 | {u.username} 75 | {u.points} 76 | {Math.floor(u.points / 10)} 77 | 78 | ))} 79 |
80 |
81 | ); 82 | }; 83 | 84 | export default () => { 85 | return ( 86 | 92 | 93 | 94 | ); 95 | }; 96 | -------------------------------------------------------------------------------- /frontend/src/pages/logs.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import styled from "styled-components"; 3 | import Layout from "../components/Layout"; 4 | import api from "../lib/api"; 5 | 6 | const Terminal = styled.div` 7 | margin: auto; 8 | width: 60%; 9 | color: #eee; 10 | line-height: 32px; 11 | * { 12 | font-family: monospace; 13 | } 14 | @media screen and (max-width: 768px) { 15 | width: 75%; 16 | } 17 | div { 18 | display: inline-block; 19 | font-size: 18px; 20 | } 21 | `; 22 | 23 | const MainTerminal = styled.div` 24 | padding: 50px; 25 | `; 26 | 27 | const Logs = () => { 28 | const [logs, setLogs] = useState([]); 29 | 30 | useEffect(() => { 31 | async function f() { 32 | const r = await (await fetch(api("/api/logs/self"))).json(); 33 | 34 | if (r.logs) { 35 | setLogs(r.logs); 36 | } 37 | } 38 | 39 | f(); 40 | }, []); 41 | 42 | return ( 43 | 44 | 45 | {logs.map(({ createdAt, log }, i) => ( 46 |
47 | {new Date(createdAt).getTime() / 1000} {log} 48 |
49 | ))} 50 |
51 |
52 | ); 53 | }; 54 | 55 | const FormatLayout = () => { 56 | return ( 57 | 63 | 64 | 65 | ); 66 | }; 67 | 68 | export default FormatLayout; 69 | -------------------------------------------------------------------------------- /frontend/src/pages/play.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Layout from "../components/Layout/index"; 3 | 4 | import Grid from "../components/Play/Grid"; 5 | 6 | export default function Play() { 7 | return ( 8 | 14 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /frontend/src/pages/register.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import RegisterForm from "../components/Register/form"; 4 | import Layout from "../components/Layout/index"; 5 | 6 | const FlexContainer = styled.div` 7 | min-height: 90vh; 8 | height: 100%; 9 | width: 100%; 10 | display: flex; 11 | justify-content: center; 12 | align-items: center; 13 | `; 14 | 15 | const Heading = styled.h1` 16 | font-size: 2.5rem; 17 | margin: 20px 0; 18 | `; 19 | 20 | export default function Register() { 21 | return ( 22 | 28 | 29 |
30 | Register 31 | 32 |
33 |
34 |
35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /frontend/src/pages/shop.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext } from "react"; 2 | import styled from "styled-components"; 3 | import { useToasts } from "react-toast-notifications"; 4 | import AuthContext from "../lib/auth-context"; 5 | import api from "../lib/api"; 6 | import Layout from "../components/Layout"; 7 | import { Button } from "../components/forms"; 8 | 9 | const Heading = styled.div` 10 | margin-top: 50px; 11 | text-align: center; 12 | font-size: 2.5rem; 13 | font-weight: bold; 14 | margin-bottom: 20px; 15 | `; 16 | 17 | const ButtonContainer = styled.div` 18 | width: 100%; 19 | display: flex; 20 | justify-content: center; 21 | `; 22 | 23 | const Shop = () => { 24 | const { user, setUser } = useContext(AuthContext); 25 | const { addToast } = useToasts(); 26 | 27 | const handleShop = async () => { 28 | try { 29 | const r = await ( 30 | await fetch(api("/api/shop/buy"), { method: "POST" }) 31 | ).json(); 32 | 33 | if (r.success) { 34 | addToast("You bought a hint card, contact an admin to use it.", { 35 | appearance: "success", 36 | }); 37 | setUser(r.user); 38 | } else { 39 | addToast(r.message, { appearance: "error" }); 40 | } 41 | } catch (e) { 42 | addToast(e.message, { appearance: "error" }); 43 | console.error(e); 44 | } 45 | }; 46 | 47 | return ( 48 | <> 49 | Shop 50 | 51 | 56 | 57 | 58 | ); 59 | }; 60 | 61 | const FormatLayout = () => { 62 | return ( 63 | 69 | 70 | 71 | ); 72 | }; 73 | 74 | export default FormatLayout; 75 | -------------------------------------------------------------------------------- /frontend/src/pages/signin.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import LoginForm from "../components/Login/form"; 4 | import Layout from "../components/Layout/index"; 5 | 6 | const FlexContainer = styled.div` 7 | min-height: 90vh; 8 | height: 100%; 9 | width: 100%; 10 | display: flex; 11 | justify-content: center; 12 | align-items: center; 13 | `; 14 | 15 | const Heading = styled.h1` 16 | font-size: 2.5rem; 17 | margin: 20px 0; 18 | `; 19 | 20 | export default function Login() { 21 | return ( 22 | 28 | 29 |
30 | Sign In 31 | 32 |
33 |
34 |
35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /frontend/src/pages/verify/email.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { useHistory, useLocation } from "react-router-dom"; 3 | import qs from "qs"; 4 | import styled from "styled-components"; 5 | import Layout from "../../components/Layout/index"; 6 | import api from "../../lib/api"; 7 | 8 | const FlexContainer = styled.div` 9 | min-height: 90vh; 10 | height: 100%; 11 | width: 100%; 12 | display: flex; 13 | justify-content: center; 14 | align-items: center; 15 | `; 16 | 17 | const Heading = styled.h1` 18 | font-size: 2.5rem; 19 | margin: 20px 0; 20 | `; 21 | 22 | const Text = styled.div` 23 | font-size: 1.5rem; 24 | text-align: center; 25 | `; 26 | 27 | export default function EmailVerification() { 28 | const [loading, setLoading] = useState(false); 29 | const location = useLocation(); 30 | 31 | useEffect(() => { 32 | async function f() { 33 | const { token } = qs.parse( 34 | location.search.slice(1, location.search.length - 1) 35 | ); 36 | 37 | const r = await ( 38 | await fetch(api("/api/auth/verification/email"), { 39 | method: "POST", 40 | headers: { 41 | "Content-Type": "application/json", 42 | }, 43 | body: JSON.stringify({ token }), 44 | }) 45 | ).json(); 46 | 47 | if (r.success) { 48 | setLoading(false); 49 | } 50 | } 51 | 52 | f(); 53 | }, []); 54 | 55 | return ( 56 | 62 | 63 |
64 | Email Verification 65 | {loading && Your email has been verified} 66 | {!loading && Your email is being verified} 67 |
68 |
69 |
70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /frontend/src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl, { 104 | headers: { 'Service-Worker': 'script' }, 105 | }) 106 | .then(response => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get('content-type'); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf('javascript') === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then(registration => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | 'No internet connection found. App is running in offline mode.' 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ('serviceWorker' in navigator) { 133 | navigator.serviceWorker.ready 134 | .then(registration => { 135 | registration.unregister(); 136 | }) 137 | .catch(error => { 138 | console.error(error.message); 139 | }); 140 | } 141 | } 142 | --------------------------------------------------------------------------------