├── .github └── workflows │ └── semgrep.yml ├── .gitignore ├── LICENSE ├── README.md ├── backEnd ├── .env.example ├── Dockerfile ├── assets │ └── .gitignore ├── controllers │ ├── adminController.js │ ├── challengesController.js │ ├── dbController.js │ ├── encryptionController.js │ ├── inputController.js │ ├── logController.js │ ├── teamController.js │ ├── userController.js │ └── validationController.js ├── models │ ├── challengeModel.js │ ├── ctfConfigModel.js │ ├── logModel.js │ ├── teamModel.js │ ├── themeModel.js │ └── userModel.js ├── package.json ├── plugins │ └── EmailVerification │ │ └── init.js ├── restart.sh ├── routes │ ├── adminRoutes.js │ ├── globalRoutes.js │ └── userRoutes.js ├── scripts │ ├── categoryToTags.js │ ├── resetProgress.js │ ├── testRace.py │ └── tests.js ├── server.js └── setup.js ├── discordBot ├── README.md ├── bot.js ├── commands │ ├── auth.js │ ├── createTeam.js │ ├── help.js │ ├── joinTeam.js │ ├── launch.js │ ├── models │ │ ├── challengeModel.js │ │ ├── teamModel.js │ │ └── userModel.js │ ├── ping.js │ ├── score.js │ ├── scoreboard.js │ ├── submit.js │ └── team.js ├── controllers │ └── encryptionController.js ├── events │ └── ready.js ├── package-lock.json └── package.json ├── docker-compose.yml ├── dockerAPI ├── .env.example ├── controllers │ └── dockerController.js ├── dockers │ └── .gitignore ├── models │ └── dockerModel.js ├── package.json ├── routes │ └── apiRoutes.js └── server.js ├── frontEnd ├── .DS_Store ├── .env.example ├── Dockerfile ├── bin.js ├── package.json ├── public │ ├── favicon.ico │ ├── icon.png │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt ├── restart.sh └── src │ ├── .DS_Store │ ├── App.jsx │ ├── components │ ├── .DS_Store │ ├── Admin │ │ ├── Admin.jsx │ │ ├── Assets.jsx │ │ ├── Challenges.jsx │ │ ├── Config.jsx │ │ ├── Dockers.jsx │ │ ├── Logs.jsx │ │ ├── Stats.jsx │ │ ├── Teams.jsx │ │ ├── Theme.jsx │ │ ├── Tools.jsx │ │ ├── Users.jsx │ │ └── components │ │ │ └── ChallengeCard.jsx │ ├── Challenges.jsx │ ├── Charts │ │ ├── ColumnChart.jsx │ │ ├── LineChart.jsx │ │ └── PieChart.jsx │ ├── Data │ │ └── AppContext.jsx │ ├── FourOFour.jsx │ ├── Global │ │ ├── ConfirmModal.jsx │ │ ├── Navbar.jsx │ │ └── TradeMark.jsx │ ├── Hackerboard.jsx │ ├── Index.jsx │ ├── Login.jsx │ ├── Logout.jsx │ ├── Register.jsx │ ├── Rules.jsx │ ├── Team.jsx │ ├── User.jsx │ ├── UserSettings.jsx │ ├── UserTeam.jsx │ └── img │ │ ├── bronzeMask.png │ │ ├── goldMask.png │ │ ├── logo.png │ │ └── silverMask.png │ ├── css │ ├── bootstrap4-neon-glow.css │ ├── bootstrap4-neon-glow.min.css │ ├── main.css │ └── prism.css │ ├── fonts │ ├── Doctor Glitch.otf │ └── PhelixBoomgartner.otf │ ├── images │ ├── 404.gif │ ├── bg--world.png │ ├── bg.jpg │ ├── bg.png │ ├── bg2.png │ └── ng-background-dot.png │ └── index.jsx ├── nginx_ctf.conf └── template.jpg /.github/workflows/semgrep.yml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: {} 3 | push: 4 | branches: 5 | - main 6 | - master 7 | paths: 8 | - .github/workflows/semgrep.yml 9 | schedule: 10 | - cron: '0 0 * * 0' 11 | name: Semgrep 12 | jobs: 13 | semgrep: 14 | name: Scan 15 | runs-on: ubuntu-20.04 16 | env: 17 | SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} 18 | container: 19 | image: returntocorp/semgrep 20 | steps: 21 | - uses: actions/checkout@v3 22 | - run: semgrep ci 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | package-lock.json 10 | 11 | build 12 | 13 | challenges 14 | 15 | serverSetup 16 | 17 | .env 18 | 19 | # Diagnostic reports (https://nodejs.org/api/report.html) 20 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 21 | 22 | # Runtime data 23 | pids 24 | *.pid 25 | *.seed 26 | *.pid.lock 27 | 28 | # Directory for instrumented libs generated by jscoverage/JSCover 29 | lib-cov 30 | 31 | # Coverage directory used by tools like istanbul 32 | coverage 33 | *.lcov 34 | 35 | # nyc test coverage 36 | .nyc_output 37 | 38 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 39 | .grunt 40 | 41 | # Bower dependency directory (https://bower.io/) 42 | bower_components 43 | 44 | # node-waf configuration 45 | .lock-wscript 46 | 47 | # Compiled binary addons (https://nodejs.org/api/addons.html) 48 | build/Release 49 | 50 | # Dependency directories 51 | node_modules/ 52 | jspm_packages/ 53 | 54 | # Snowpack dependency directory (https://snowpack.dev/) 55 | web_modules/ 56 | 57 | # TypeScript cache 58 | *.tsbuildinfo 59 | 60 | # Optional npm cache directory 61 | .npm 62 | 63 | # Optional eslint cache 64 | .eslintcache 65 | 66 | # Optional stylelint cache 67 | .stylelintcache 68 | 69 | # Microbundle cache 70 | .rpt2_cache/ 71 | .rts2_cache_cjs/ 72 | .rts2_cache_es/ 73 | .rts2_cache_umd/ 74 | 75 | # Optional REPL history 76 | .node_repl_history 77 | 78 | # Output of 'npm pack' 79 | *.tgz 80 | 81 | # Yarn Integrity file 82 | .yarn-integrity 83 | 84 | # dotenv environment variable files 85 | .env 86 | .env.development.local 87 | .env.test.local 88 | .env.production.local 89 | .env.local 90 | 91 | # parcel-bundler cache (https://parceljs.org/) 92 | .cache 93 | .parcel-cache 94 | 95 | # Next.js build output 96 | .next 97 | out 98 | 99 | # Nuxt.js build / generate output 100 | .nuxt 101 | dist 102 | 103 | # Gatsby files 104 | .cache/ 105 | # Comment in the public line in if your project uses Gatsby and not Next.js 106 | # https://nextjs.org/blog/next-9-1#public-directory-support 107 | # public 108 | 109 | # vuepress build output 110 | .vuepress/dist 111 | 112 | # vuepress v2.x temp and cache directory 113 | .temp 114 | .cache 115 | 116 | # Docusaurus cache and generated files 117 | .docusaurus 118 | 119 | # Serverless directories 120 | .serverless/ 121 | 122 | # FuseBox cache 123 | .fusebox/ 124 | 125 | # DynamoDB Local files 126 | .dynamodb/ 127 | 128 | # TernJS port file 129 | .tern-port 130 | 131 | # Stores VSCode versions used for testing VSCode extensions 132 | .vscode-test 133 | 134 | # yarn v2 135 | .yarn/cache 136 | .yarn/unplugged 137 | .yarn/build-state.yml 138 | .yarn/install-state.gz 139 | .pnp.* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Currently working on v2... 2 | Updates : soon 3 | 4 | [![StackShare](http://img.shields.io/badge/tech-stack-0690fa.svg?style=flat)](https://stackshare.io/ctfcafe/ctfcafe) 5 | 6 | # Contributors 7 | 8 | [@Zerotistic]( https://github.com/Zerotistic ) 9 | [@Eteckq]( https://github.com/Eteckq ) 10 | 11 | 12 | # Features 13 | 14 | - Create your own challenges, files, hints, code snippets and flags from the admin dashboard 15 | - File uploads to the server 16 | - Flag submit bruteforce protection 17 | - Docker challenges 18 | - Dynamic flags for each team 19 | - Dynamic scoring possible with a decay of 15 20 | - Individual and Team based competitions 21 | - Have users play on their own or form teams to play together 22 | - First blood's 23 | - Docker's launched by team 24 | - Scoreboard with automatic tie resolution 25 | - See global user, team and challenge stats 26 | - See indivudal team & user stats 27 | - Automatic competition starting and ending 28 | - Easily set endTime & startTime from the admin dashboard 29 | - Team and user management ( promoting, banning, ect ) 30 | - Customize site colors, background, rules & frontpage 31 | - Importing and Exporting of CTF scoreboards into json 32 | - Email verification on registration 33 | - And more... 34 | 35 | # Manual Backend & Frontend Setup ( Dockers ) 36 | 37 | *docker compose setup is only usable if you wont run any dockerized challenges else use pm2* 38 | 39 | # Manual Backend & Frontend Setup ( No Dockers ) 40 | 41 | ## Prerequisites 42 | - Node.JS 43 | - MongoDB 44 | 45 | ## Setup 46 | - copy `.env.example` to `.env` file in /backEnd/ and fill the info needed 47 | 48 | - copy `.env.example` to `.env` file in /dockerAPI/ and fill the info needed 49 | 50 | - copy `.env.example` to `.env` file in /frontEnd/ and fill the info needed 51 | 52 | ## Startup 53 | 54 | `MongoDB` 55 | - Start your mongoDB database 56 | - If you're testing on Windows, you may need to download https://www.mongodb.com/try/download/community 57 | 58 | `/frontEnd` 59 | - Run `npm install` to install the requirements from `package.json`, then run `npm start` or `npm run start-react` for easier dev to start the frontend 60 | 61 | `/backEnd` 62 | - Run `npm install` to install the requirements from `package.json`, then run `npm start` to start & setup the backend 63 | 64 | `/dockerAPI` 65 | - Run `npm install` to install the requirements from `package.json`, then run `npm start` to start & setup the backend 66 | 67 | `/discordBot` 68 | - This is optional, but can be used to setup a bot for the CTF. See here: https://github.com/CTF-Cafe/CTF_Cafe/tree/master/discordBot 69 | 70 | *You can use `pm2` if you want an easy way to handle the nodejs processes.* 71 | 72 | Make sure to create a new account, promote him to admin and delete the admin:admin user after setup! 73 | 74 | Good to go! -------------------------------------------------------------------------------- /backEnd/.env.example: -------------------------------------------------------------------------------- 1 | 2 | # DB 3 | SESSION_SECRET= 4 | MONGODB_CONNSTRING= 5 | 6 | # Routing 7 | FRONTEND_URI= 8 | 9 | # Mail Verification 10 | MAIL_VERIFICATION= 11 | BACKEND_URI= 12 | HOST= 13 | MAIL_PORT= 14 | MAIL= 15 | PASS= 16 | 17 | # Docker API 18 | DEPLOYER_API= 19 | DEPLOYER_SECRET= 20 | 21 | # Config 22 | NODE_ENV= 23 | PORT=3001 24 | 25 | # First Blood Webhook 26 | WEBHOOK= 27 | WEBHOOK_2= -------------------------------------------------------------------------------- /backEnd/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine 2 | 3 | WORKDIR /server 4 | 5 | COPY package.json ./ 6 | 7 | RUN npm install --production 8 | 9 | COPY . . 10 | 11 | EXPOSE 3001 12 | 13 | CMD npm start 14 | -------------------------------------------------------------------------------- /backEnd/assets/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | */ 3 | !.gitignore -------------------------------------------------------------------------------- /backEnd/controllers/encryptionController.js: -------------------------------------------------------------------------------- 1 | // VERY IMPORTANT!! READ: https://www.troyhunt.com/we-didnt-encrypt-your-password-we-hashed-it-heres-what-that-means/ 2 | 3 | const bcrypt = require('bcryptjs'); 4 | 5 | exports.encrypt = async function(pass) { 6 | return await bcrypt.hash(pass, 10); 7 | } 8 | 9 | exports.compare = async function(pass, hash) { 10 | return await bcrypt.compare(pass, hash); 11 | } -------------------------------------------------------------------------------- /backEnd/controllers/inputController.js: -------------------------------------------------------------------------------- 1 | const ObjectId = require("mongoose").Types.ObjectId; 2 | 3 | exports.validateRequestBody = function(body, validationObject) { 4 | for (const [key, validation] of Object.entries(validationObject)) { 5 | const value = body[key]; 6 | if (validation.required && (value === undefined || value === '')) { 7 | throw new Error(`${key} is required`); 8 | } 9 | if (validation.type === 'number' && isNaN(value)) { 10 | throw new Error(`${key} must be a number`); 11 | } 12 | if (validation.type === 'positiveNumber' && (isNaN(value) || parseInt(value) < 0)) { 13 | throw new Error(`${key} must be a positive number`); 14 | } 15 | if (validation.type === 'objectId' && (value !== '' && !ObjectId.isValid(value))) { 16 | throw new Error(`${key} must be a valid ObjectId`); 17 | } 18 | if (validation.type === 'array') { 19 | const parsedArray = JSON.parse(value); 20 | parsedArray.forEach((item, index) => { 21 | for (const [itemKey, itemValidation] of Object.entries(validation.itemValidation)) { 22 | const itemValue = item[itemKey]; 23 | if (itemValidation.required && (itemValue === undefined || itemValue === '')) { 24 | throw new Error(`Item ${index} of ${key} must have ${itemKey} defined`); 25 | } 26 | if (itemValidation.type === 'number' && isNaN(itemValue)) { 27 | throw new Error(`Item ${index} of ${key} ${itemKey} must be a number`); 28 | } 29 | if (itemValidation.type === 'positiveNumber' && (isNaN(itemValue) || parseInt(itemValue) < 0)) { 30 | throw new Error(`Item ${index} of ${key} ${itemKey} must be a positive number`); 31 | } 32 | } 33 | }); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /backEnd/controllers/logController.js: -------------------------------------------------------------------------------- 1 | const callerId = require('caller-id'); 2 | const logs = require("../models/logModel"); 3 | 4 | exports.createLog = async function (req, user, result) { 5 | const caller = callerId.getData(); 6 | await logs.create({ 7 | authorIp: req.ip == "::ffff:127.0.0.1" ? req.socket.remoteAddress : req.ip, 8 | authorId: user._id, 9 | authorName: user.username, 10 | function: caller.functionName, 11 | result: JSON.stringify(result), 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /backEnd/controllers/validationController.js: -------------------------------------------------------------------------------- 1 | const { check } = require("express-validator"); 2 | const ObjectId = require("mongoose").Types.ObjectId; 3 | 4 | exports.username = (id = "username") => 5 | check(id) 6 | .notEmpty() 7 | .withMessage("must not be empty") 8 | .trim() 9 | .isLength({ min: 1, max: 32 }) 10 | .withMessage("must be >1 and <32") 11 | .matches(/^[^"$\n@]+$/) 12 | .withMessage("dont use any sus characters"); 13 | 14 | exports.password = (id = "password") => 15 | check(id) 16 | .notEmpty() 17 | .withMessage("must not be empty") 18 | .trim() 19 | .isLength({ min: 8 }) 20 | .withMessage("must be >8"); 21 | 22 | exports.email = (id = "email") => 23 | check(id) 24 | .notEmpty() 25 | .withMessage("must not be empty") 26 | .trim() 27 | .isEmail() 28 | .withMessage("Not a valid Email"); 29 | 30 | exports.userCategory = (id = "userCategory") => 31 | check(id).notEmpty().withMessage("must not be empty").trim(); 32 | 33 | exports.id = (id = "id") => 34 | check(id) 35 | .notEmpty() 36 | .withMessage("must not be empty") 37 | .trim() 38 | .custom((val) => ObjectId.isValid(val)) 39 | .withMessage("not a valid ObjectId") 40 | .customSanitizer((val) => ObjectId(val)); 41 | 42 | exports.page = (id = "page") => 43 | check(id) 44 | .optional() 45 | .default(1) 46 | .isInt({ min: 0 }) 47 | .withMessage("must be an int"); 48 | 49 | exports.search = (id = "search") => 50 | check(id) 51 | .optional() 52 | .default("") 53 | .isString() 54 | .withMessage("must be a string") 55 | .customSanitizer((val) => new RegExp(val, "i")); 56 | 57 | exports.teamName = (id = "teamName") => 58 | check(id) 59 | .notEmpty() 60 | .withMessage("must not be empty") 61 | .trim() 62 | .isLength({ min: 1, max: 32 }) 63 | .withMessage("must be >1 and <32") 64 | .matches(/^[^"$\n]+$/) 65 | .withMessage("dont use any sus characters"); 66 | 67 | exports.teamCode = (id = "teamCode") => 68 | check(id) 69 | .notEmpty() 70 | .withMessage("must not be empty") 71 | .trim() 72 | .isUUID(4) 73 | .withMessage("must be a uuid"); 74 | 75 | exports.flag = (id = "flag") => 76 | check(id).notEmpty().withMessage("must not be empty").trim(); 77 | 78 | exports.emoji = (id = "emoji") => 79 | check(id) 80 | .notEmpty() 81 | .withMessage("must not be empty") 82 | .trim() 83 | .matches(/^[\uD83C][\uDDE6-\uDDFF][\uD83C][\uDDE6-\uDDFF]$/) 84 | .withMessage("must be an emoji!"); 85 | -------------------------------------------------------------------------------- /backEnd/models/challengeModel.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const mongoose = require('mongoose'); 3 | var Schema = mongoose.Schema; 4 | 5 | var challengeSchema = new Schema({ 6 | hidden: { 7 | type: Boolean, 8 | default: true 9 | }, 10 | name: { 11 | type: String, 12 | required: true 13 | }, 14 | tags: { 15 | type: Array, 16 | required: true 17 | }, 18 | flag: { 19 | type: String, 20 | default: "FLAG{HELLO}" 21 | }, 22 | hints: { 23 | type: Array, 24 | default: [] 25 | }, 26 | points: { 27 | type: Number, 28 | default: 100 29 | }, 30 | firstBloodPoints: { 31 | type: Number, 32 | default: 0 33 | }, 34 | initialPoints: { 35 | type: Number, 36 | default: 100 37 | }, 38 | minimumPoints: { 39 | type: Number, 40 | default: 50 41 | }, 42 | info: { 43 | type: String, 44 | default: "Beep. Boop." 45 | }, 46 | level: { 47 | type: Number, 48 | default: 0 49 | }, 50 | solveCount: { 51 | type: Number, 52 | default: 0 53 | }, 54 | file: { 55 | type: String, 56 | default: '', 57 | }, 58 | codeSnippet: { 59 | type: String, 60 | default: '' 61 | }, 62 | codeLanguage: { 63 | type: String, 64 | default: 'none' 65 | }, 66 | firstBlood: { 67 | type: String, 68 | default: 'none' 69 | }, 70 | githubUrl: { 71 | type: String, 72 | default: null 73 | }, 74 | isInstance: { 75 | type: Boolean, 76 | default: false 77 | }, 78 | randomFlag: { 79 | type: Boolean, 80 | default: false 81 | }, 82 | randomFlags: { 83 | type: Array, 84 | default: [] 85 | }, 86 | requirement: { 87 | type: String, 88 | default: '' 89 | } 90 | }); 91 | 92 | module.exports = mongoose.model('Challenges', challengeSchema); -------------------------------------------------------------------------------- /backEnd/models/ctfConfigModel.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const mongoose = require('mongoose'); 3 | var Schema = mongoose.Schema; 4 | 5 | var configSchema = new Schema({ 6 | name: { 7 | type: String, 8 | required: true 9 | }, 10 | value: { 11 | type: mongoose.Mixed, 12 | required: true 13 | } 14 | }); 15 | 16 | module.exports = mongoose.model('ctfConfigs', configSchema); -------------------------------------------------------------------------------- /backEnd/models/logModel.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const mongoose = require("mongoose"); 3 | var Schema = mongoose.Schema; 4 | 5 | var logSchema = new Schema({ 6 | authorIp: { 7 | type: String, 8 | required: true, 9 | }, 10 | authorId: { 11 | type: String, 12 | required: true, 13 | }, 14 | authorName: { 15 | type: String, 16 | required: true, 17 | }, 18 | function: { 19 | type: String, 20 | required: true, 21 | }, 22 | result: { 23 | type: String, 24 | required: true, 25 | }, 26 | }); 27 | 28 | module.exports = mongoose.model("logs", logSchema); 29 | -------------------------------------------------------------------------------- /backEnd/models/teamModel.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const mongoose = require('mongoose'); 3 | var Schema = mongoose.Schema; 4 | 5 | function teamLimit(val) { 6 | return val.length <= 4; 7 | } 8 | 9 | var teamSchema = new Schema({ 10 | name: { 11 | type: String, 12 | required: true 13 | }, 14 | inviteCode: { 15 | type: String, 16 | required: true 17 | }, 18 | users: { 19 | type: Array, 20 | validate: [teamLimit, '{PATH} exceeds the limit of 4'], 21 | required: true, 22 | }, 23 | teamCaptain: { 24 | type: String, 25 | required: false, 26 | }, 27 | category: { 28 | type: String, 29 | required: true 30 | }, 31 | country: { 32 | type: String, 33 | default: "🌐" 34 | } 35 | }); 36 | 37 | module.exports = mongoose.model('Teams', teamSchema); -------------------------------------------------------------------------------- /backEnd/models/themeModel.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const mongoose = require('mongoose'); 3 | var Schema = mongoose.Schema; 4 | 5 | const colorValidator = (v) => (/^#([0-9a-f]{3}){1,2}$/i).test(v) 6 | 7 | var themeSchema = new Schema({ 8 | color_1: { 9 | type: String, 10 | validator: [colorValidator, 'Invalid color'], 11 | default: '#ff3c5c' 12 | }, 13 | color_2: { 14 | type: String, 15 | validator: [colorValidator, 'Invalid color'], 16 | default: '#ff707f' 17 | }, 18 | bg_img: { 19 | type: String, 20 | default: 'none' 21 | }, 22 | top1_icon: { 23 | type: String, 24 | default: 'none' 25 | }, 26 | top2_icon: { 27 | type: String, 28 | default: 'none' 29 | }, 30 | top3_icon: { 31 | type: String, 32 | default: 'none' 33 | } 34 | }); 35 | 36 | module.exports = mongoose.model('Theme', themeSchema); -------------------------------------------------------------------------------- /backEnd/models/userModel.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const mongoose = require('mongoose'); 3 | var Schema = mongoose.Schema; 4 | 5 | var userSchema = new Schema({ 6 | username: { 7 | type: String, 8 | required: true 9 | }, 10 | email: { 11 | type: String, 12 | required: true 13 | }, 14 | discordId: { 15 | type: String, 16 | default: 'none' 17 | }, 18 | password: { 19 | type: String, 20 | required: true 21 | }, 22 | solved: { 23 | type: Array, 24 | default: [], 25 | }, 26 | score: { 27 | type: Number, 28 | default: 0 29 | }, 30 | key: { 31 | type: String, 32 | required: true 33 | }, 34 | isAdmin: { 35 | type: Boolean, 36 | default: false 37 | }, 38 | teamId: { 39 | type: String, 40 | default: 'none' 41 | }, 42 | verified: { 43 | type: Boolean, 44 | default: false 45 | }, 46 | token: { 47 | type: String, 48 | }, 49 | hintsBought: { 50 | type: Array, 51 | default: [], 52 | }, 53 | shadowBanned: { 54 | type: Boolean, 55 | default: false 56 | }, 57 | adminPoints: { 58 | type: Number, 59 | default: 0 60 | }, 61 | category: { 62 | type: String, 63 | required: false 64 | } 65 | }); 66 | 67 | module.exports = mongoose.model('Users', userSchema); -------------------------------------------------------------------------------- /backEnd/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ctf_cafe_backend", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "server.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node server.js", 9 | "release": "npx pkg ." 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "bcryptjs": "^2.4.3", 15 | "body-parser": "^1.20.1", 16 | "caller-id": "^0.1.0", 17 | "connect-mongo": "^4.6.0", 18 | "discord-webhook-node": "^1.1.8", 19 | "dotenv": "^16.0.0", 20 | "express": "^4.17.3", 21 | "express-fileupload": "^1.3.1", 22 | "express-mongo-sanitize": "^2.2.0", 23 | "express-session": "^1.17.2", 24 | "express-validator": "^7.0.1", 25 | "mongoose": "^6.2.8", 26 | "node-cron": "^3.0.2", 27 | "nodemailer": "^6.9.0", 28 | "unzipper": "^0.10.11", 29 | "uuid": "^9.0.0", 30 | "validator": "^13.9.0", 31 | "xss-clean": "^0.1.1" 32 | }, 33 | "bin": "./server.js", 34 | "pkg": { 35 | "outputPath": "release", 36 | "targets": [ 37 | "node18-linux-x64", 38 | "node18-macos-x64", 39 | "node18-win-x64", 40 | "node18-linux-arm64", 41 | "node18-macos-arm64", 42 | "node18-win-arm64" 43 | ] 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /backEnd/plugins/EmailVerification/init.js: -------------------------------------------------------------------------------- 1 | exports.init = async function (api) {}; 2 | 3 | exports.registerEntrypoint = async function (api, req, res) { 4 | [req, res] = api.userController.register.verify(req, res); 5 | if (!checkEmail(req.body.email)) throw Error("Email is invalid!"); 6 | [req, res] = api.userController.register.manipulate(req, res); 7 | 8 | const message = `Verify your email : ${process.env.BACKEND_URI}/api/verify/${user._id}/${user.token}`; 9 | await sendEmail(user.email, "Verify Email CTF", message); 10 | 11 | res.message = { 12 | state: "success", 13 | message: "Registered! Now verify email!", 14 | }; 15 | 16 | return req, res; 17 | }; 18 | 19 | function checkEmail(email) { 20 | if ( 21 | !email.match( 22 | /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ 23 | ) 24 | ) { 25 | return false; 26 | } 27 | return true; 28 | } 29 | 30 | const sendEmail = async (email, subject, text) => { 31 | try { 32 | const transporter = nodemailer.createTransport({ 33 | host: process.env.HOST, 34 | port: process.env.MAIL_PORT, 35 | secure: true, 36 | auth: { 37 | user: process.env.MAIL, 38 | pass: process.env.PASS, 39 | }, 40 | }); 41 | 42 | await transporter.sendMail({ 43 | from: process.env.USER, 44 | to: email, 45 | subject: subject, 46 | text: text, 47 | }); 48 | } catch (error) { 49 | throw Error("Sending email failed!"); 50 | } 51 | }; 52 | 53 | async function verifyMail(req, res) { 54 | try { 55 | const user = await users.findOne({ _id: req.params.id }); 56 | if (!user) throw new Error("Invalid Link"); 57 | 58 | if (user.token != req.params.token) throw new Error("Invalid Link"); 59 | 60 | await users.updateOne({ _id: user._id }, { verified: true, token: "" }); 61 | 62 | res.send("Email verified!"); 63 | } catch (err) { 64 | if (err) { 65 | res.send(err.message); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /backEnd/restart.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | read -p "Enter the database password: " dbPass 3 | docker build -t ctf_cafe/backend . 4 | docker stop backend_1 5 | docker rm backend_1 6 | docker run -d -p 3001:3001 -e MONGODB_CONNSTRING="mongodb://dev:$dbPass@$(docker inspect -f '{{.NetworkSettings.IPAddress}}' mongodb):27017/ctfDB?directConnection=true&authSource=admin" -v /home/dev/CTF_Cafe/backEnd/assets:/server/assets -v /home/dev/CTF_Cafe/backEnd/dockers:/server/dockers --name backend_1 ctf_cafe/backend 7 | -------------------------------------------------------------------------------- /backEnd/routes/adminRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const router = express.Router() 3 | const adminController = require('../controllers/adminController.js'); 4 | 5 | router.post('/getLogs', (req, res) => { 6 | adminController.getLogs(req, res); 7 | }); 8 | 9 | router.post('/getUsers', (req, res) => { 10 | adminController.getUsers(req, res); 11 | }); 12 | 13 | router.post('/getTeams', (req, res) => { 14 | adminController.getTeams(req, res); 15 | }); 16 | 17 | router.post('/deleteUser', (req, res) => { 18 | adminController.deleteUser(req, res); 19 | }); 20 | 21 | router.post('/deleteTeam', (req, res) => { 22 | adminController.deleteTeam(req, res); 23 | }); 24 | 25 | router.post('/addAdmin', (req, res) => { 26 | adminController.addAdmin(req, res); 27 | }); 28 | 29 | router.post('/removeAdmin', (req, res) => { 30 | adminController.removeAdmin(req, res); 31 | }); 32 | 33 | router.post('/shadowBan', (req, res) => { 34 | adminController.shadowBan(req, res); 35 | }); 36 | 37 | router.post('/unShadowBan', (req, res) => { 38 | adminController.unShadowBan(req, res); 39 | }); 40 | 41 | router.post('/setUserAdminPoints', (req, res) => { 42 | adminController.setUserAdminPoints(req, res); 43 | }); 44 | 45 | router.post('/changeUserPassword', (req, res) => { 46 | adminController.changeUserPassword(req, res); 47 | }); 48 | 49 | router.post('/getStats', (req, res) => { 50 | adminController.getStats(req, res); 51 | }); 52 | 53 | router.get('/getAssets', (req, res) => { 54 | adminController.getAssets(req, res); 55 | }); 56 | 57 | router.post('/saveConfigs', (req, res) => { 58 | adminController.saveConfigs(req, res); 59 | }); 60 | 61 | router.post('/deleteAsset', (req, res) => { 62 | adminController.deleteAsset(req, res); 63 | }); 64 | 65 | router.post('/uploadAsset', (req, res) => { 66 | adminController.uploadAsset(req, res); 67 | }); 68 | 69 | router.post('/saveChallenge', (req, res) => { 70 | adminController.saveChallenge(req, res); 71 | }); 72 | 73 | router.post('/createChallenge', (req, res) => { 74 | adminController.createChallenge(req, res); 75 | }); 76 | 77 | router.post('/updateChallengeCategory', (req, res) => { 78 | adminController.updateChallengeCategory(req, res); 79 | }); 80 | 81 | router.post('/deleteChallenge', (req, res) => { 82 | adminController.deleteChallenge(req, res); 83 | }); 84 | 85 | router.post('/saveTheme', (req, res) => { 86 | adminController.saveTheme(req, res); 87 | }); 88 | 89 | router.post('/sendGlobalMessage', (req, res) => { 90 | adminController.sendGlobalMessage(req, res); 91 | }); 92 | 93 | router.post('/getDockers', (req, res) => { 94 | adminController.getDockers(req, res); 95 | }); 96 | 97 | router.post('/shutdownDocker', (req, res) => { 98 | adminController.shutdownDocker(req, res); 99 | }); 100 | 101 | router.post('/restartDocker', (req, res) => { 102 | adminController.restartDocker(req, res); 103 | }); 104 | 105 | module.exports = router; -------------------------------------------------------------------------------- /backEnd/routes/globalRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const validation = require("../controllers/validationController"); 4 | const userController = require("../controllers/userController.js"); 5 | const teamController = require("../controllers/teamController.js"); 6 | const users = require("../models/userModel.js"); 7 | const teams = require("../models/teamModel.js"); 8 | const mongoose = require("mongoose"); 9 | const ObjectId = mongoose.Types.ObjectId; 10 | 11 | router.get("/easteregg", async (req, res) => { 12 | res.send("Guacamole soon..."); 13 | }); 14 | 15 | router.post( 16 | "/login", 17 | [validation.username(), validation.password()], 18 | (req, res) => { 19 | userController.login(req, res); 20 | } 21 | ); 22 | 23 | router.get("/logout", (req, res) => { 24 | userController.logout(req, res); 25 | }); 26 | 27 | router.post( 28 | "/register", 29 | [ 30 | validation.username(), 31 | validation.email(), 32 | validation.password(), 33 | validation.userCategory(), 34 | ], 35 | (req, res) => { 36 | userController.register(req, res); 37 | } 38 | ); 39 | 40 | router.get("/verify/:id/:token", [validation.id()], async (req, res) => { 41 | userController.verifyMail(req, res); 42 | }); 43 | 44 | router.post( 45 | "/getUsers", 46 | [validation.page(), validation.search()], 47 | (req, res) => { 48 | userController.getUsers(req, res); 49 | } 50 | ); 51 | 52 | router.post("/getUser", [validation.username()], (req, res) => { 53 | userController.getUser(req, res); 54 | }); 55 | 56 | router.post("/getTeam", [validation.teamName()], (req, res) => { 57 | teamController.getTeam(req, res); 58 | }); 59 | 60 | router.post( 61 | "/getTeams", 62 | [validation.page()], 63 | [validation.search()], 64 | (req, res) => { 65 | teamController.getTeams(req, res); 66 | } 67 | ); 68 | 69 | router.get("/getScoreboard", (req, res) => { 70 | userController.getScoreboard(req, res); 71 | }); 72 | 73 | router.get("/getEndTime", (req, res) => { 74 | userController.getEndTime(req, res); 75 | }); 76 | 77 | router.get("/getConfigs", (req, res) => { 78 | userController.getConfigs(req, res); 79 | }); 80 | 81 | router.get("/checkSession", (req, res) => { 82 | users.findById(req.session.userId).then(async function (user) { 83 | if (!user) { 84 | res.send({ state: "sessionError" }); 85 | } else if (!(user.key == req.session.key)) { 86 | res.send({ state: "sessionError" }); 87 | } else { 88 | user.password = undefined; 89 | user.key = undefined; 90 | 91 | if (ObjectId.isValid(user.teamId)) { 92 | let team = await teams.findById(ObjectId(user.teamId)); 93 | 94 | if (team) { 95 | team.inviteCode = undefined; 96 | res.send({ state: "success", user: user, team: team }); 97 | } else { 98 | res.send({ state: "success", user: user }); 99 | } 100 | } else { 101 | res.send({ state: "success", user: user }); 102 | } 103 | } 104 | }); 105 | }); 106 | 107 | router.get("/getTheme", (req, res) => { 108 | userController.getTheme(req, res); 109 | }); 110 | 111 | module.exports = router; 112 | -------------------------------------------------------------------------------- /backEnd/routes/userRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const validation = require("../controllers/validationController"); 4 | const challengesController = require("../controllers/challengesController.js"); 5 | const teamController = require("../controllers/teamController.js"); 6 | const userController = require("../controllers/userController.js"); 7 | const ctfConfig = require("../models/ctfConfigModel.js"); 8 | 9 | router.post("/registerTeam", [validation.teamName()], (req, res) => { 10 | teamController.registerTeam(req, res); 11 | }); 12 | 13 | router.get("/getTeamCount", (req, res) => { 14 | userController.getTeamCount(req, res); 15 | }); 16 | 17 | router.post("/joinTeam", [validation.teamCode()], (req, res) => { 18 | teamController.joinTeam(req, res); 19 | }); 20 | 21 | router.get("/leaveTeam", (req, res) => { 22 | teamController.leaveTeam(req, res); 23 | }); 24 | 25 | router.post("/kickUser", [validation.username("userToKick")], (req, res) => { 26 | teamController.kickUser(req, res); 27 | }); 28 | 29 | router.post( 30 | "/updateUsername", 31 | [validation.username("newUsername")], 32 | (req, res) => { 33 | userController.updateUsername(req, res); 34 | } 35 | ); 36 | 37 | router.post( 38 | "/updatePassword", 39 | [validation.password("newPassword")], 40 | [validation.password("oldPassword")], 41 | (req, res) => { 42 | userController.updatePassword(req, res); 43 | } 44 | ); 45 | 46 | router.post("/getTeamCode", [validation.teamName()], (req, res) => { 47 | teamController.getCode(req, res); 48 | }); 49 | 50 | router.get("/getUserTeam", (req, res) => { 51 | teamController.getUserTeam(req, res); 52 | }); 53 | 54 | router.post("/saveTeamCountry", [validation.emoji("country")], (req, res) => { 55 | teamController.saveTeamCountry(req, res); 56 | }); 57 | 58 | router.post("/deployDocker", [validation.id("challengeId")], (req, res) => { 59 | challengesController.deployDocker(req, res); 60 | }); 61 | 62 | router.post("/shutdownDocker", [validation.id("challengeId")], (req, res) => { 63 | challengesController.shutdownDocker(req, res); 64 | }); 65 | 66 | router.post( 67 | "/submitFlag", 68 | [validation.id("challengeId"), validation.flag()], 69 | (req, res) => { 70 | challengesController.submitFlag(req, res); 71 | } 72 | ); 73 | 74 | router.post("/buyHint", [validation.id("challengeId")], (req, res) => { 75 | challengesController.buyHint(req, res); 76 | }); 77 | 78 | router.get("/getChallenges", (req, res) => { 79 | challengesController.getChallenges(req, res); 80 | }); 81 | 82 | router.get("/getNotifications", async (req, res) => { 83 | const notifications = await ctfConfig.findOne({ name: "notifications" }); 84 | 85 | if (notifications && notifications.value && notifications.value.length > 0) { 86 | const editedNotifications = notifications.value 87 | .map((notification) => { 88 | if (notification && notification.message && notification.seenBy) { 89 | if (!notification.seenBy.includes(req.session.userId)) { 90 | return { 91 | ...notification, 92 | seenBy: [...notification.seenBy, req.session.userId], 93 | }; 94 | } else { 95 | return notification; 96 | } 97 | } else { 98 | return undefined; 99 | } 100 | }) 101 | .filter((x) => x !== undefined); 102 | 103 | if ( 104 | JSON.stringify(editedNotifications) !== 105 | JSON.stringify(notifications.value) 106 | ) { 107 | await ctfConfig.updateOne( 108 | { name: "notifications" }, 109 | { $set: { value: editedNotifications } } 110 | ); 111 | } 112 | 113 | const notificationsNeeded = notifications.value.filter( 114 | (notification) => !notification.seenBy.includes(req.session.userId) 115 | ); 116 | 117 | res.send({ state: "success", notifications: notificationsNeeded }); 118 | } else { 119 | res.send({ state: "error", notifications: [] }); 120 | } 121 | }); 122 | 123 | module.exports = router; 124 | -------------------------------------------------------------------------------- /backEnd/scripts/categoryToTags.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const db = mongoose.connection; 3 | const dotenv = require("dotenv"); 4 | dotenv.config(); 5 | 6 | const users = require("../models/userModel.js"); 7 | const teams = require("../models/teamModel.js"); 8 | const challenges = require("../models/challengeModel.js"); 9 | const ObjectId = mongoose.Types.ObjectId; 10 | 11 | mongoose.connect(process.env.MONGODB_CONNSTRING, { 12 | useNewUrlParser: true, 13 | useUnifiedTopology: true, 14 | }); 15 | 16 | db.once("open", async function () { 17 | console.log("Database Connected successfully"); 18 | 19 | 20 | await challenges.updateMany({ 21 | category: { $gt: '' } 22 | }, [ 23 | { 24 | $set: { 25 | tags: ["$category"] 26 | } 27 | } 28 | ]); 29 | 30 | 31 | console.log("Done."); 32 | }); 33 | -------------------------------------------------------------------------------- /backEnd/scripts/resetProgress.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const db = mongoose.connection; 3 | const dotenv = require("dotenv"); 4 | dotenv.config(); 5 | 6 | const users = require("../models/userModel.js"); 7 | const teams = require("../models/teamModel.js"); 8 | const challenges = require("../models/challengeModel.js"); 9 | const ObjectId = mongoose.Types.ObjectId; 10 | 11 | mongoose.connect(process.env.MONGODB_CONNSTRING, { 12 | useNewUrlParser: true, 13 | useUnifiedTopology: true, 14 | }); 15 | 16 | db.once("open", async function () { 17 | console.log("Database Connected successfully"); 18 | 19 | await users.updateMany( 20 | { 21 | $or: [ 22 | { $size: { solved: { $gt: 0 } } }, 23 | { 24 | $size: { hintsBought: { $gt: 0 } }, 25 | }, 26 | ], 27 | }, 28 | { score: 0, solved: [], hintsBought: [] } 29 | ); 30 | 31 | console.log("Users reset successfully"); 32 | 33 | await teams.updateMany( 34 | { 35 | users: { $elemMatch: { "hintsBought.0": { $exists: true } } }, 36 | }, 37 | { 38 | $set: { 39 | "users.$.solved": [], 40 | "users.$.hintsBought": [], 41 | }, 42 | } 43 | ); 44 | 45 | console.log("Teams reset successfully"); 46 | 47 | await challenges.updateMany( 48 | { solveCount: { $gt: 0 } }, 49 | { solveCount: 0, firstBlood: "none" } 50 | ); 51 | 52 | console.log("Challenges reset successfully"); 53 | 54 | console.log("Done."); 55 | }); 56 | -------------------------------------------------------------------------------- /backEnd/scripts/testRace.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import threading 3 | 4 | s = requests.Session() 5 | 6 | requests.get("http://localhost:3000") 7 | 8 | 9 | def submitFlag(): 10 | cookies = { 11 | "connect.sid": "s%3A2DYIpywZB5ZenpmfDrcSReMav-DiyaEE.FC8yNbH5PnHBA7Fc6uIr2oItnQ68sKB1JjDOWSq5Sv4"} 12 | r = requests.post("http://localhost:3001/api/user/submitFlag", 13 | {"challengeId": "645659f7eca1981a33486e2b", "flag": "FLAG{HELLO}"}, cookies=cookies) 14 | print(r.json()) 15 | 16 | 17 | # Create two threads as follows 18 | threads = list() 19 | for index in range(2): 20 | x = threading.Thread(target=submitFlag) 21 | threads.append(x) 22 | x.start() 23 | -------------------------------------------------------------------------------- /backEnd/scripts/tests.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const db = mongoose.connection; 3 | const dotenv = require('dotenv'); 4 | dotenv.config() 5 | 6 | const users = require('../models/userModel.js'); 7 | const teams = require('../models/teamModel.js'); 8 | const challenges = require('../models/challengeModel.js'); 9 | const ObjectId = mongoose.Types.ObjectId; 10 | 11 | 12 | mongoose.connect(process.env.MONGODB_CONNSTRING, { useNewUrlParser: true, useUnifiedTopology: true }); 13 | 14 | db.once("open", async function() { 15 | console.log("Database Connected successfully"); 16 | 17 | 18 | const usersFound = await users.aggregate([{ 19 | "$unwind": { 20 | "path": "$solved", 21 | "preserveNullAndEmptyArrays": true 22 | } 23 | }, 24 | { 25 | $lookup: { 26 | from: "challenges", 27 | let: { "chalId": "$solved._id", "timestamp": "$solved.timestamp" }, 28 | pipeline: [{ 29 | $match: { 30 | $expr: { $eq: ["$$chalId", "$_id"] }, 31 | }, 32 | }, 33 | { 34 | $project: { 35 | _id: 0, 36 | solve: { 37 | _id: "$_id", 38 | challenge: { points: "$points", name: "$name", _id: "$_id" }, 39 | timestamp: "$$timestamp", 40 | points: "$points", 41 | } 42 | } 43 | }, 44 | { 45 | $replaceRoot: { newRoot: "$solve" } 46 | } 47 | ], 48 | as: "solved" 49 | } 50 | }, 51 | { 52 | "$unwind": { 53 | "path": "$solved", 54 | "preserveNullAndEmptyArrays": true 55 | } 56 | }, 57 | { 58 | $group: { 59 | _id: "$_id", 60 | username: { $first: "$username" }, 61 | score: { $sum: "$solved.points" }, 62 | solved: { $push: "$solved" }, 63 | isAdmin: { $first: "$isAdmin" } 64 | } 65 | } 66 | ]) 67 | 68 | console.log(usersFound, usersFound[1]) 69 | 70 | process.exit(0); 71 | }); -------------------------------------------------------------------------------- /backEnd/server.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const app = express(); 3 | const mongoose = require("mongoose"); 4 | const db = mongoose.connection; 5 | const dotenv = require("dotenv"); 6 | dotenv.config(); 7 | const port = process.env.PORT || 3001; 8 | const bodyparser = require("body-parser"); 9 | const session = require("express-session"); 10 | const MongoStore = require("connect-mongo"); 11 | const fileUpload = require("express-fileupload"); 12 | const mongoSanitize = require("express-mongo-sanitize"); 13 | const xssClean = require("xss-clean/lib/xss").clean; 14 | const setup = require("./setup.js"); 15 | const userRouter = require("./routes/userRoutes.js"); 16 | const adminRouter = require("./routes/adminRoutes.js"); 17 | const globalRouter = require("./routes/globalRoutes.js"); 18 | const users = require("./models/userModel.js"); 19 | 20 | mongoose.connect(process.env.MONGODB_CONNSTRING, { 21 | authSource: "admin", 22 | useNewUrlParser: true, 23 | useUnifiedTopology: true, 24 | }); 25 | 26 | db.once("open", async function () { 27 | console.log("Database Connected successfully"); 28 | setup.setupDB(); 29 | }); 30 | 31 | // Body-parser middleware 32 | app.use(bodyparser.urlencoded({ extended: false })); 33 | app.use(bodyparser.json()); 34 | 35 | var sess = { 36 | store: MongoStore.create({ 37 | mongoUrl: process.env.MONGODB_CONNSTRING, 38 | touchAfter: 24 * 3600, // time period in seconds 39 | }), 40 | secret: process.env.SESSION_SECRET, 41 | resave: false, 42 | saveUninitialized: false, 43 | cookie: { 44 | maxAge: 1000 * 60 * 60 * 24 * 7 * 2, // two weeks 45 | }, 46 | }; 47 | 48 | // Trust Proxy to be able to read X-Forwaded-For (user ips) 49 | if (process.env.NODE_ENV === "production") { 50 | app.set("trust proxy", 1); // trust first proxy 51 | // sess.cookie.secure = true; // serve secure cookies 52 | sess.proxy = true; 53 | } 54 | 55 | app.use(session(sess)); 56 | 57 | // Add headers before the routes are defined 58 | app.use(function (req, res, next) { 59 | // Website you wish to allow to connect 60 | res.setHeader("Access-Control-Allow-Origin", process.env.FRONTEND_URI); 61 | 62 | // Request methods you wish to allow 63 | res.setHeader( 64 | "Access-Control-Allow-Methods", 65 | "GET, POST, OPTIONS" 66 | ); 67 | 68 | // Request headers you wish to allow 69 | res.setHeader( 70 | "Access-Control-Allow-Headers", 71 | "Set-Cookie,X-Requested-With,content-type" 72 | ); 73 | 74 | // Set to true if you need the website to include cookies in the requests sent 75 | // to the API (e.g. in case you use sessions) 76 | res.setHeader("Access-Control-Allow-Credentials", true); 77 | 78 | // Pass to next layer of middleware 79 | next(); 80 | }); 81 | 82 | app.use( 83 | fileUpload({ 84 | limits: { 85 | fileSize: 100000000, //100mb 86 | }, 87 | createParentPath: true, 88 | }) 89 | ); 90 | 91 | // function customSanitize(req, res, next) { 92 | // Object.entries(req.body).forEach(([key, value]) => { 93 | // req.body[key] = mongoSanitize.sanitize(xssClean(value)); 94 | // }); 95 | 96 | // Object.entries(req.query).forEach(([key, value]) => { 97 | // req.query[key] = mongoSanitize.sanitize(xssClean(value)); 98 | // }); 99 | 100 | // Object.entries(req.params).forEach(([key, value]) => { 101 | // req.params[key] = mongoSanitize.sanitize(xssClean(value)); 102 | // }); 103 | 104 | // next(); 105 | // } 106 | 107 | // app.use(customSanitize); 108 | 109 | function checkAuth(req, res, next) { 110 | users.findById(req.session.userId).then(function (user) { 111 | if (!user) { 112 | res.send({ state: "sessionError" }); 113 | } else if (!(user.key == req.session.key)) { 114 | res.send({ state: "sessionError" }); 115 | } else { 116 | if (user.isAdmin) { 117 | req.isAdmin = true; 118 | } 119 | next(); 120 | } 121 | }); 122 | } 123 | 124 | function checkAdminAuth(req, res, next) { 125 | users.findById(req.session.userId).then(function (user) { 126 | if (!user) { 127 | res.send({ state: "sessionError" }); 128 | } else if (!(user.key == req.session.key)) { 129 | res.send({ state: "sessionError" }); 130 | } else if (!user.isAdmin) { 131 | res.send({ state: "sessionError" }); 132 | } else { 133 | next(); 134 | } 135 | }); 136 | } 137 | 138 | app.use("/api", globalRouter); 139 | app.use("/api/user", checkAuth, userRouter); 140 | app.use("/api/admin", checkAdminAuth, adminRouter); 141 | 142 | app.use("/api/assets", express.static("./assets")); 143 | 144 | process.on("uncaughtException", function (err) { 145 | console.log("Uncaught exception: " + err.stack); 146 | console.log("NODE NOT EXITING"); 147 | }); 148 | 149 | app.listen(port, () => { 150 | console.log(`listening on port ${port}`); 151 | }); 152 | -------------------------------------------------------------------------------- /backEnd/setup.js: -------------------------------------------------------------------------------- 1 | const ctfConfig = require("./models/ctfConfigModel.js"); 2 | const theme = require("./models/themeModel.js"); 3 | const users = require("./models/userModel.js"); 4 | const encryptionController = require("./controllers/encryptionController.js"); 5 | 6 | function generatePassword(length) { 7 | const characters = 8 | "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@"; 9 | let password = ""; 10 | for (let i = 0; i < length; i++) { 11 | const randomIndex = Math.floor(Math.random() * characters.length); 12 | password += characters[randomIndex]; 13 | } 14 | return password; 15 | } 16 | 17 | exports.setupDB = async function () { 18 | const startTimeConfig = await ctfConfig.findOne({ name: "startTime" }); 19 | 20 | if (!startTimeConfig) { 21 | await ctfConfig.create({ 22 | name: "startTime", 23 | value: new Date().getTime(), 24 | }); 25 | } 26 | 27 | const endTimeConfig = await ctfConfig.findOne({ name: "endTime" }); 28 | 29 | if (!endTimeConfig) { 30 | await ctfConfig.create({ 31 | name: "endTime", 32 | value: new Date().getTime(), 33 | }); 34 | } 35 | 36 | const rulesConfig = await ctfConfig.findOne({ name: "rules" }); 37 | 38 | if (!rulesConfig) { 39 | await ctfConfig.create({ 40 | name: "rules", 41 | value: [ 42 | { 43 | text: "- Don't share flags, or share info with other teams!", 44 | }, 45 | { 46 | text: "- Dont DDOS our services or bruteforce any flags/websites", 47 | }, 48 | { 49 | text: "- If you do not understand the rules:", 50 | link: "https://www.youtube.com/watch?v=oHg5SJYRHA0", 51 | linkText: "https://www.sec.gov/", 52 | }, 53 | ], 54 | }); 55 | } 56 | 57 | const notificationsConfig = await ctfConfig.findOne({ 58 | name: "notifications", 59 | }); 60 | 61 | if (!notificationsConfig) { 62 | await ctfConfig.create({ 63 | name: "notifications", 64 | value: [{ message: "Welcome to CTFCafe!", type: "admin", seenBy: [] }], 65 | }); 66 | } 67 | 68 | const sponsorsConfig = await ctfConfig.findOne({ name: "sponsors" }); 69 | 70 | if (!sponsorsConfig) { 71 | await ctfConfig.create({ 72 | name: "sponsors", 73 | value: [ 74 | { 75 | image: 76 | "https://www.offensive-security.com/wp-content/themes/OffSec/assets/images/offsec-logo.svg", 77 | }, 78 | ], 79 | }); 80 | } 81 | 82 | const currentTheme = await theme.findOne({}); 83 | if (!currentTheme) { 84 | await theme.create({ 85 | color_1: "#ff3d3d", 86 | color_2: "#ff7070", 87 | bg_img: "", 88 | top1_icon: "", 89 | top2_icon: "", 90 | top3_icon: "", 91 | }); 92 | } 93 | 94 | const adminExists = await users.findOne({ isAdmin: true }); 95 | 96 | if (!adminExists) { 97 | const rawPassword = generatePassword(10); 98 | const password = await encryptionController.encrypt(rawPassword); 99 | 100 | await users.create({ 101 | username: "admin", 102 | password: password, 103 | email: "admin@admin.com", 104 | verified: true, 105 | key: "none", 106 | isAdmin: true, 107 | }); 108 | console.log(`Created default admin. admin:${rawPassword} (DELETE ASAP)`); 109 | } 110 | 111 | const tagsConfig = await ctfConfig.findOne({ name: "tags" }); 112 | 113 | if (!tagsConfig) { 114 | await ctfConfig.create({ 115 | name: "tags", 116 | value: ["web", "crypto", "reverse", "pwn", "forensics"], 117 | }); 118 | } 119 | 120 | const tagColorsConfig = await ctfConfig.findOne({ 121 | name: "tagColors", 122 | }); 123 | 124 | if (!tagColorsConfig) { 125 | await ctfConfig.create({ 126 | name: "tagColors", 127 | value: [ 128 | { name: "web", color: "#ef121b94" }, 129 | { name: "osint", color: "#b017a494" }, 130 | { name: "reverse", color: "#17b06b94" }, 131 | { name: "pwn", color: "#00d5ff94" }, 132 | { name: "forensics", color: "#0f329894" }, 133 | { name: "misc", color: "#ffff00c4" }, 134 | { name: "crypto", color: "#9966ff94" }, 135 | ], 136 | }); 137 | } 138 | 139 | const dynamicScoringConfig = await ctfConfig.findOne({ 140 | name: "dynamicScoring", 141 | }); 142 | 143 | if (!dynamicScoringConfig) { 144 | await ctfConfig.create({ 145 | name: "dynamicScoring", 146 | value: false, 147 | }); 148 | } 149 | 150 | const scoreboardHiddenConfig = await ctfConfig.findOne({ 151 | name: "scoreboardHidden", 152 | }); 153 | 154 | if (!scoreboardHiddenConfig) { 155 | await ctfConfig.create({ 156 | name: "scoreboardHidden", 157 | value: false, 158 | }); 159 | } 160 | 161 | const dockerLimitConfig = await ctfConfig.findOne({ name: "dockerLimit" }); 162 | 163 | if (!dockerLimitConfig) { 164 | await ctfConfig.create({ 165 | name: "dockerLimit", 166 | value: 1, 167 | }); 168 | } 169 | 170 | const socialLinksConfig = await ctfConfig.findOne({ name: "socialLinks" }); 171 | 172 | if (!socialLinksConfig) { 173 | await ctfConfig.create({ 174 | name: "socialLinks", 175 | value: [{ link: "https://github.com", icon: "github" }], 176 | }); 177 | } 178 | 179 | const userCategoriesConfig = await ctfConfig.findOne({ name: "userCategories" }); 180 | 181 | if (!userCategoriesConfig) { 182 | await ctfConfig.create({ 183 | name: "userCategories", 184 | value: ["Student", "Other"], 185 | }); 186 | } 187 | 188 | console.log("Database Setup successfully"); 189 | }; 190 | -------------------------------------------------------------------------------- /discordBot/README.md: -------------------------------------------------------------------------------- 1 | # Setup 2 | ## Prerequisites 3 | - Discord.JS v13 4 | - Node.JS 5 | - If you want to use `?launch` to display the challenge list, make sure that you add your organizers' Discord IDs into the site's database with ?auth 6 | 7 | ## Installation 8 | - Set up a bot on https://discord.com/developers 9 | - Make a `.env` file with the token in this format: 10 | ``` 11 | DISCORD_BOT_TOKEN= 12 | DISCORD_BOT_PREFIX= 13 | 14 | SERVER_URI= 15 | 16 | MONGODB_URI= 17 | 18 | CTF_NAME= 19 | ``` 20 | - Run `npm install` to install the requirements from `package.json`, then run `npm start` in the bot's directory -------------------------------------------------------------------------------- /discordBot/bot.js: -------------------------------------------------------------------------------- 1 | const { Client, Intents, Partials, Collection } = require('discord.js'); 2 | const bot = new Client({ 3 | intents: [Intents.FLAGS.DIRECT_MESSAGES, Intents.FLAGS.GUILDS, Intents.FLAGS.GUILD_MESSAGES, Intents.FLAGS.GUILD_MEMBERS, Intents.FLAGS.GUILD_MESSAGE_REACTIONS, Intents.FLAGS.DIRECT_MESSAGE_REACTIONS, ], 4 | partials: ['MESSAGE', 'CHANNEL'] 5 | }); 6 | require('dotenv').config(); 7 | const prefix = process.env.DISCORD_BOT_PREFIX; 8 | const fs = require("fs"); 9 | bot.commands = new Collection(); 10 | const mongoose = require('mongoose'); 11 | const db = mongoose.connection; 12 | 13 | mongoose.connect(process.env.MONGODB_URI, { useNewUrlParser: true, useUnifiedTopology: true }); 14 | 15 | db.once("open", async function() { 16 | console.log("Database Connected successfully"); 17 | }); 18 | 19 | const commandFiles = fs.readdirSync('./commands/').filter(f => f.endsWith('.js')) 20 | for (const file of commandFiles) { 21 | const props = require(`./commands/${file}`) 22 | console.log(`${file} loaded`) 23 | bot.commands.set(props.info.name, props) 24 | } 25 | 26 | // Load Event files from events folder 27 | const eventFiles = fs.readdirSync('./events/').filter(f => f.endsWith('.js')) 28 | 29 | for (const file of eventFiles) { 30 | const event = require(`./events/${file}`) 31 | if (event.once) { 32 | bot.once(event.name, (...args) => event.execute(...args, bot)) 33 | } else { 34 | bot.on(event.name, (...args) => event.execute(...args, bot)) 35 | } 36 | } 37 | 38 | //Command Manager 39 | bot.on("messageCreate", async message => { 40 | //Check if author is a bot or the message was sent in dms and return 41 | if (message.author.bot) return; 42 | 43 | // if (message.channel.type === "dm") return; 44 | 45 | //get prefix from config and prepare message so it can be read as a command 46 | let messageArray = message.content.split(" "); 47 | let cmd = messageArray[0]; 48 | let args = messageArray.slice(1); 49 | 50 | //Check for prefix 51 | if (!cmd.startsWith(prefix)) return; 52 | 53 | //Get the command from the commands collection and then if the command is found run the command file 54 | let commandfile = bot.commands.get(cmd.slice(prefix.length)); 55 | if (commandfile) commandfile.run(bot, message, args); 56 | 57 | }); 58 | 59 | bot.login(process.env.DISCORD_BOT_TOKEN); -------------------------------------------------------------------------------- /discordBot/commands/auth.js: -------------------------------------------------------------------------------- 1 | const users = require('./models/userModel'); 2 | const { MessageEmbed } = require('discord.js'); 3 | const encryptionController = require('../controllers/encryptionController.js'); 4 | 5 | exports.run = async(bot, message, args) => { 6 | if (!(message.guild === null)) { 7 | message.reply("This command only works in dms!").then(() => { 8 | message.delete() 9 | }); 10 | return 11 | } 12 | if (!args[0]) return message.reply('You must provide a username'); 13 | if (!args[1]) return message.reply('You must provide a password'); 14 | 15 | const username = args[0]; 16 | const password = encryptionController.encrypt(args[1]); 17 | 18 | let userCheck = await users.findOne({ username: username, password: password }); 19 | 20 | if (userCheck) { 21 | if (userCheck.discordId != 'none' || userCheck.discordId != undefined) { 22 | await users.findOneAndUpdate({ username: username, password: password }, { discordId: message.author.id }); 23 | message.reply("Discord has been linked!"); 24 | } else { 25 | message.reply("Account already linked!"); 26 | } 27 | } else { 28 | message.reply("Wrong credentials!"); 29 | } 30 | } 31 | 32 | exports.info = { 33 | name: "auth", 34 | description: "Authenticate your Discord with your CTF account. (Use in DM) \nUsage: ?auth username password" 35 | } -------------------------------------------------------------------------------- /discordBot/commands/createTeam.js: -------------------------------------------------------------------------------- 1 | const users = require('./models/userModel'); 2 | const teams = require('./models/teamModel'); 3 | const challenges = require('./models/challengeModel'); 4 | const { MessageEmbed } = require('discord.js'); 5 | const ObjectId = require('mongoose').Types.ObjectId; 6 | const { v4 } = require('uuid'); 7 | 8 | exports.run = async(bot, message, args) => { 9 | if (!(message.guild === null)) return message.reply("This command only works in dms!"); 10 | if (!args[0]) return message.reply('You must provide a team_name!'); 11 | 12 | const teamName = args[0].trim(); 13 | 14 | const teamCheck = await teams.findOne({ name: teamName }); 15 | 16 | if (!teamCheck) { 17 | let checkUser = await users.findOne({ discordId: message.author.id }); 18 | 19 | if (checkUser) { 20 | 21 | let userTeamCheck; 22 | if (ObjectId.isValid(checkUser.teamId)) { 23 | userTeamCheck = await teams.findById(checkUser.teamId); 24 | } 25 | 26 | if (!userTeamCheck) { 27 | const inviteCode = v4(); 28 | await teams.create({ name: teamName, inviteCode: inviteCode, users: [{ username: checkUser.username, score: checkUser.score, solved: checkUser.solved }] }).then(async function(team) { 29 | await users.findOneAndUpdate({ username: checkUser.username }, { teamId: team.id }, { returnOriginal: false }).then(async function(user) { 30 | message.reply(`Registered team! Here is the invite code: ${inviteCode}`); 31 | }); 32 | }).catch(function(err) { 33 | message.reply(`Team creation failed!`); 34 | }); 35 | } else { 36 | message.reply(`Already in a team!`); 37 | } 38 | 39 | } else { 40 | message.reply("Your discord is not linked to any account!"); 41 | } 42 | } else { 43 | message.reply("Team name already in use!"); 44 | } 45 | } 46 | 47 | exports.info = { 48 | name: "createTeam", 49 | description: "Create a Team. (Use in DMs) \nUsage: ?createTeam team_name" 50 | } -------------------------------------------------------------------------------- /discordBot/commands/help.js: -------------------------------------------------------------------------------- 1 | exports.run = async(bot, message, args) => { 2 | let commandsArray = Array.from(bot.commands.values()); 3 | let msg = 'Commands: \n'; 4 | for (let i = 0; i < commandsArray.length; i++) { 5 | msg += '```' + commandsArray[i].info.name + ' - ' + commandsArray[i].info.description + '```\n'; 6 | } 7 | 8 | message.channel.send(msg); 9 | } 10 | 11 | exports.info = { 12 | name: "help", 13 | description: "Help Command", 14 | } -------------------------------------------------------------------------------- /discordBot/commands/joinTeam.js: -------------------------------------------------------------------------------- 1 | const users = require('./models/userModel'); 2 | const teams = require('./models/teamModel'); 3 | const challenges = require('./models/challengeModel'); 4 | const { MessageEmbed } = require('discord.js'); 5 | const ObjectId = require('mongoose').Types.ObjectId; 6 | const { v4 } = require('uuid'); 7 | 8 | exports.run = async(bot, message, args) => { 9 | if (!(message.guild === null)) return message.reply("This command only works in dms!"); 10 | if (!args[0]) return message.reply('You must provide an invite code!'); 11 | 12 | const inviteCode = args[0].trim(); 13 | 14 | const teamCheck = await teams.findOne({ inviteCode: inviteCode }); 15 | 16 | if (teamCheck) { 17 | let checkUser = await users.findOne({ discordId: message.author.id }); 18 | 19 | if (checkUser) { 20 | 21 | let userTeamCheck; 22 | if (ObjectId.isValid(checkUser.teamId)) { 23 | userTeamCheck = await teams.findById(checkUser.teamId); 24 | } 25 | 26 | if (!userTeamCheck) { 27 | if (teamCheck.users.length < 4) { 28 | await teams.findOneAndUpdate({ inviteCode: inviteCode }, { $push: { users: { username: checkUser.username, score: checkUser.score, solved: checkUser.solved } } }, { returnOriginal: false }).then(async function(team) { 29 | await users.findOneAndUpdate({ username: checkUser.username }, { teamId: team.id }, { returnOriginal: false }).then(async function(user) { 30 | res.send({ state: 'success', message: 'Joined team!', user: user, team: team }); 31 | }); 32 | }).catch(error => { 33 | res.send({ state: 'error', message: error.messsage }); 34 | }); 35 | } else { 36 | res.send({ state: 'error', message: 'Team is full!' }); 37 | } 38 | } else { 39 | message.reply(`Already in a team!`); 40 | } 41 | 42 | } else { 43 | message.reply("Your discord is not linked to any account!"); 44 | } 45 | } else { 46 | message.reply("Team does not exist!"); 47 | } 48 | } 49 | 50 | exports.info = { 51 | name: "joinTeam", 52 | description: "Join a Team. (Use in DMs) \nUsage: ?joinTeam invite_code" 53 | } -------------------------------------------------------------------------------- /discordBot/commands/launch.js: -------------------------------------------------------------------------------- 1 | const challenges = require('./models/challengeModel'); 2 | const { MessageEmbed } = require('discord.js'); 3 | const users = require('./models/userModel'); 4 | 5 | exports.run = async(bot, message, args) => { 6 | if (!message.member) return message.reply("You are not an admin!"); 7 | if (!message.member.permissions.has("ADMINISTRATOR")) return message.reply("You are not an admin!"); 8 | if (!args[0]) return message.reply('You must provide a channel id!'); 9 | 10 | const allChallenges = await challenges.find(); 11 | 12 | let checkUser = await users.findOne({ discordId: message.author.id }); 13 | 14 | if (checkUser) { 15 | if (checkUser.isAdmin) 16 | /* checks the site's database to make sure the Discord user is 17 | an admin on the backend as well */ 18 | { 19 | let scoreboardEmbed = new MessageEmbed() 20 | .setColor('#ff0000') 21 | .setTitle(process.env.CTF_NAME + ' | Challenges') 22 | .setTimestamp() 23 | .setFooter({ text: 'powered by CTF Cafe' }); 24 | 25 | 26 | for (let i = 0; i < allChallenges.length; i++) { 27 | const challenge = allChallenges[i]; 28 | await scoreboardEmbed.addField( 29 | `${challenge.name.trim()} | ${challenge.points} | ${challenge.level == 0 ? 'Easy' : challenge.level == 1 ? 'Medium' : challenge.level == 2 ? 'Hard' : 'Ninja'} | ${challenge.category}`, 30 | "Info: \n```" + challenge.info.replace(/\\n/g, `\n`) + "```\nHints: \n" + challenge.hints.map((hint, index) => `|| ${hint} ||`) + (challenge.file.length > 0 ? `\n\nFile below:` : '')) 31 | 32 | if (challenge.file.length > 0) { 33 | await message.guild.channels.cache.get(args[0]).send({ embeds: [scoreboardEmbed] }).then(async() => { 34 | await message.guild.channels.cache.get(args[0]).send({ 35 | files: ['./assets/' + challenge.file] 36 | }) 37 | }); 38 | 39 | scoreboardEmbed = new MessageEmbed() 40 | .setColor('#ff0000') 41 | .setTitle(process.env.CTF_NAME + ' | Challenges') 42 | .setTimestamp() 43 | .setFooter({ text: 'powered by CTF Cafe' }); 44 | } 45 | } 46 | 47 | message.guild.channels.cache.get(args[0]).send({ embeds: [scoreboardEmbed] }); 48 | } else { 49 | message.reply('Not an admin!'); 50 | } 51 | } else { 52 | message.reply('Discord is not linked to any account!'); 53 | } 54 | } 55 | 56 | exports.info = { 57 | name: "launch", 58 | description: "Very Secret, only for Admins" 59 | } -------------------------------------------------------------------------------- /discordBot/commands/models/challengeModel.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const mongoose = require('mongoose'); 3 | var Schema = mongoose.Schema; 4 | 5 | var challengeSchema = new Schema({ 6 | name: { 7 | type: String, 8 | required: true 9 | }, 10 | category: { 11 | type: String, 12 | required: true 13 | }, 14 | flag: { 15 | type: String, 16 | required: true 17 | }, 18 | hints: { 19 | type: Array, 20 | default: [], 21 | }, 22 | points: { 23 | type: Number, 24 | default: 100 25 | }, 26 | info: { 27 | type: String, 28 | required: true 29 | }, 30 | level: { 31 | type: Number, 32 | default: 0 33 | }, 34 | solveCount: { 35 | type: Number, 36 | default: 0 37 | }, 38 | file: { 39 | type: String, 40 | default: '', 41 | } 42 | }); 43 | 44 | module.exports = mongoose.model('Challenges', challengeSchema); -------------------------------------------------------------------------------- /discordBot/commands/models/teamModel.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const mongoose = require('mongoose'); 3 | var Schema = mongoose.Schema; 4 | 5 | var teamSchema = new Schema({ 6 | name: { 7 | type: String, 8 | required: true 9 | }, 10 | inviteCode: { 11 | type: String, 12 | required: true 13 | }, 14 | users: { 15 | type: Array, 16 | required: true 17 | }, 18 | 19 | }); 20 | 21 | module.exports = mongoose.model('Teams', teamSchema); -------------------------------------------------------------------------------- /discordBot/commands/models/userModel.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const mongoose = require('mongoose'); 3 | var Schema = mongoose.Schema; 4 | 5 | var userSchema = new Schema({ 6 | username: { 7 | type: String, 8 | required: true 9 | }, 10 | discordId: { 11 | type: String, 12 | }, 13 | password: { 14 | type: String, 15 | required: true 16 | }, 17 | solved: { 18 | type: Array, 19 | default: [], 20 | }, 21 | score: { 22 | type: Number, 23 | default: 0 24 | }, 25 | key: { 26 | type: String, 27 | required: true 28 | }, 29 | isAdmin: { 30 | type: Boolean, 31 | default: false 32 | }, 33 | teamId: { 34 | type: String, 35 | default: 'none' 36 | } 37 | }); 38 | 39 | module.exports = mongoose.model('Users', userSchema); -------------------------------------------------------------------------------- /discordBot/commands/ping.js: -------------------------------------------------------------------------------- 1 | exports.run = async(bot, message, args) => { 2 | message.channel.send("My ping is \`" + bot.ws.ping + " ms\`"); 3 | } 4 | 5 | exports.info = { 6 | name: "ping", 7 | description: "Check Bot Ping" 8 | } -------------------------------------------------------------------------------- /discordBot/commands/score.js: -------------------------------------------------------------------------------- 1 | const users = require('./models/userModel'); 2 | const teams = require('./models/teamModel'); 3 | const challenges = require('./models/challengeModel'); 4 | const { MessageEmbed } = require('discord.js'); 5 | const ObjectId = require('mongoose').Types.ObjectId; 6 | 7 | exports.run = async(bot, message, args) => { 8 | if (!(message.guild === null)) return message.reply("This command only works in dms!"); 9 | 10 | let checkUser = await users.findOne({ discordId: message.author.id }); 11 | 12 | if (checkUser) { 13 | message.reply("Your score: " + checkUser.score + "\n\nChallenges Solved: \n" + checkUser.solved.map((s) => s.challenge.name + ' | ')); 14 | } else { 15 | message.reply("Your discord is not linked to any account!"); 16 | } 17 | } 18 | 19 | exports.info = { 20 | name: "score", 21 | description: "See your current score! (Use in DMs)" 22 | } -------------------------------------------------------------------------------- /discordBot/commands/scoreboard.js: -------------------------------------------------------------------------------- 1 | const { MessageEmbed } = require('discord.js'); 2 | const users = require('./models/userModel'); 3 | const teams = require('./models/teamModel'); 4 | 5 | function max(input) { 6 | if (toString.call(input) !== "[object Array]") 7 | return false; 8 | return Math.max.apply(null, input); 9 | } 10 | 11 | exports.run = async(bot, message, args) => { 12 | if (!args[0]) return message.reply('Please specify (users/teams) as the first argument.'); 13 | 14 | if (args[0] === 'users') { 15 | const top = await users.find().sort({ score: -1, _id: 1 }).limit(24); 16 | 17 | const scoreboardEmbed = new MessageEmbed() 18 | .setColor('#ff0000') 19 | .setTitle(process.env.CTF_NAME + ' | Scoreboard') 20 | .setTimestamp() 21 | .setFooter({ text: 'powered by CTF Cafe' }); 22 | 23 | top.map((user, index) => scoreboardEmbed.addField(`${index + 1}. ${user.username}`, user.score.toString(), true)) 24 | 25 | message.reply({ embeds: [scoreboardEmbed] }); 26 | } else if (args[0] === 'teams') { 27 | const top = await teams.aggregate([{ 28 | "$project": { 29 | "name": 1, 30 | "users": 1, 31 | "totalScore": { 32 | "$sum": "$users.score" 33 | }, 34 | "timestamps": "$users.solved.timestamp" 35 | } 36 | }, { 37 | '$sort': { 38 | 'totalScore': -1 39 | } 40 | }]).limit(24); 41 | 42 | top.forEach(team => { 43 | let maxTimestamp = 0; 44 | 45 | team.timestamps.forEach(timestamp => { 46 | if (max(timestamp) > maxTimestamp) { 47 | maxTimestamp = max(timestamp) 48 | } 49 | }); 50 | 51 | team.maxTimestamp = maxTimestamp; 52 | }) 53 | 54 | top.sort((a, b) => { 55 | if (b.totalScore - a.totalScore == 0) { 56 | return a.maxTimestamp - b.maxTimestamp; 57 | } else { 58 | return b.totalScore - a.totalScore; 59 | } 60 | }); 61 | 62 | const scoreboardEmbed = new MessageEmbed() 63 | .setColor('#ff0000') 64 | .setTitle(process.env.CTF_NAME + ' | Scoreboard') 65 | .setTimestamp() 66 | .setFooter({ text: 'powered by CTF Cafe' }); 67 | 68 | top.map((team, index) => { 69 | scoreboardEmbed.addField(`${index + 1}. ${team.name}`, team.totalScore.toString(), true); 70 | }); 71 | 72 | message.reply({ embeds: [scoreboardEmbed] }); 73 | } else { 74 | message.reply('Please specify (users/teams) as the first argument.'); 75 | } 76 | } 77 | 78 | exports.info = { 79 | name: "scoreboard", 80 | description: "Get live scoreboard. \nUsage: ?scoreboard users|teams", 81 | } -------------------------------------------------------------------------------- /discordBot/commands/submit.js: -------------------------------------------------------------------------------- 1 | const users = require('./models/userModel'); 2 | const teams = require('./models/teamModel'); 3 | const challenges = require('./models/challengeModel'); 4 | const { MessageEmbed } = require('discord.js'); 5 | const ObjectId = require('mongoose').Types.ObjectId; 6 | 7 | exports.run = async(bot, message, args) => { 8 | if (!(message.guild === null)) { 9 | message.reply("This command only works in dms!").then(() => { 10 | message.delete() 11 | }); 12 | return 13 | } 14 | if (!args[0]) return message.reply('You must provide a challenge name (replace spaces with _)'); 15 | if (!args[1]) return message.reply('You must provide a flag'); 16 | 17 | const challengeName = args[0].replace('_', ' ').trim(); 18 | const challengeFlag = args[1].trim(); 19 | 20 | const challengeCheck = await challenges.findOne({ name: challengeName }); 21 | 22 | let allChallenges = await challenges.find({}); 23 | 24 | 25 | if (challengeCheck) { 26 | if (challengeCheck.flag == challengeFlag) { 27 | let checkUser = await users.findOne({ discordId: message.author.id }); 28 | 29 | if (checkUser) { 30 | if (checkUser.solved.filter(obj => { return obj.challenge._id.equals(challengeCheck._id) }).length > 0) { 31 | message.reply(`Already solved!`); 32 | } else { 33 | if (ObjectId.isValid(checkUser.teamId)) { 34 | const team = await teams.findById(checkUser.teamId); 35 | 36 | if (team) { 37 | if (team.users.filter(user => { 38 | return (user.solved.filter(obj => { 39 | return obj.challenge._id.equals(challengeCheck._id) 40 | }).length > 0) 41 | }).length > 0) { 42 | message.reply(`Already solved!`); 43 | } else { 44 | let timestamp = new Date().getTime(); 45 | challengeCheck.flag = 'Nice try XD'; 46 | 47 | await users.updateOne({ discordId: message.author.id }, { $push: { solved: { _id: challengeCheck._id, challenge: challengeCheck, timestamp: timestamp } } }); 48 | await users.updateOne({ discordId: message.author.id }, { $inc: { score: challengeCheck.points } }); 49 | 50 | const updatedUser = await users.findOne({ discordId: message.author.id }); 51 | 52 | await teams.updateOne({ 53 | _id: team._id, 54 | users: { $elemMatch: { username: updatedUser.username } } 55 | }, { 56 | $set: { 57 | "users.$.solved": updatedUser.solved, 58 | "users.$.score": updatedUser.score, 59 | } 60 | }); 61 | 62 | await challenges.updateOne({ _id: challengeCheck.id }, { $inc: { solveCount: 1 } }); 63 | 64 | message.reply(`Correct flag, +${challengeCheck.points} points!`); 65 | } 66 | } else { 67 | message.reply(`Not in a team!`); 68 | } 69 | } else { 70 | message.reply(`Not in a team!`); 71 | } 72 | } 73 | 74 | } else { 75 | message.reply("Your discord is not linked to any account!"); 76 | } 77 | 78 | } else { 79 | message.reply("Wrong flag!"); 80 | } 81 | } else { 82 | message.reply("Challenge does not exist!"); 83 | } 84 | } 85 | 86 | exports.info = { 87 | name: "submit", 88 | description: "Submit a challenge Flag. (Use in DMs) \nUsage: ?submit challenge_name flag" 89 | } -------------------------------------------------------------------------------- /discordBot/commands/team.js: -------------------------------------------------------------------------------- 1 | const users = require('./models/userModel'); 2 | const teams = require('./models/teamModel'); 3 | const challenges = require('./models/challengeModel'); 4 | const { MessageEmbed } = require('discord.js'); 5 | const ObjectId = require('mongoose').Types.ObjectId; 6 | 7 | exports.run = async(bot, message, args) => { 8 | if (!(message.guild === null)) return message.reply("This command only works in dms!"); 9 | 10 | let checkUser = await users.findOne({ discordId: message.author.id }); 11 | 12 | if (checkUser) { 13 | 14 | let userTeam; 15 | if (ObjectId.isValid(checkUser.teamId)) { 16 | userTeam = await teams.findById(checkUser.teamId); 17 | } 18 | 19 | if (userTeam) { 20 | 21 | const teamEmbed = new MessageEmbed() 22 | .setColor('#ff0000') 23 | .setTitle(process.env.CTF_NAME + ` | ${userTeam.name} Team`) 24 | .setTimestamp() 25 | .setFooter({ text: 'powered by CTF Cafe' }); 26 | 27 | userTeam.users.map((user, index) => { 28 | teamEmbed.addField(`${index + 1}. ${user.username}`, user.score.toString(), true); 29 | }); 30 | 31 | message.reply({ embeds: [teamEmbed] }); 32 | } else { 33 | message.reply("Not in a team!"); 34 | } 35 | 36 | } else { 37 | message.reply("Your discord is not linked to any account!"); 38 | } 39 | } 40 | 41 | exports.info = { 42 | name: "team", 43 | description: "See your current team! (Use in DMs)" 44 | } -------------------------------------------------------------------------------- /discordBot/controllers/encryptionController.js: -------------------------------------------------------------------------------- 1 | const bcrypt = require('bcryptjs'); 2 | 3 | exports.encrypt = async function(pass) { 4 | return await bcrypt.hash(pass, 10); 5 | } 6 | 7 | exports.compare = async function(pass, hash) { 8 | return await bcrypt.compare(pass, hash); 9 | } -------------------------------------------------------------------------------- /discordBot/events/ready.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'ready', 3 | once: true, 4 | execute(bot) { 5 | //Log Bot's username and the amount of servers its in to console 6 | console.log(`${bot.user.username} is online and ready to fly!`); 7 | 8 | //Set the Presence of the bot user 9 | bot.user.setPresence({ activities: [{ name: 'Hacking into the mainframe! ?help' }] }); 10 | } 11 | } -------------------------------------------------------------------------------- /discordBot/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "bcryptjs": "^2.4.3", 4 | "crypto": "^1.0.1", 5 | "discord.js": "^13.6.0", 6 | "dotenv": "^14.2.0", 7 | "fs": "^0.0.1-security", 8 | "mongoose": "^6.1.8", 9 | "random-reddit": "^2.0.3", 10 | "uuid": "^8.3.2" 11 | }, 12 | "name": "discord-bot", 13 | "version": "1.0.0", 14 | "main": "bot.js", 15 | "scripts": { 16 | "start": "node bot.js", 17 | "test": "echo \"Error: no test specified\" && exit 1" 18 | }, 19 | "author": "", 20 | "license": "ISC", 21 | "description": "" 22 | } 23 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # TO WORK ON A VPS: 2 | # CHANGE FRONTEND_URI and BACKEND_URI to http://vps_ip:port 3 | # FOR HTTPS CHANGE TO https://domain.com/ AND SETUP NGINX FOR ROUTING 4 | 5 | version: '3.1' 6 | 7 | services: 8 | 9 | mongo: 10 | image: mongo 11 | restart: always 12 | environment: 13 | MONGO_INITDB_ROOT_USERNAME: dbuser 14 | MONGO_INITDB_ROOT_PASSWORD: changeme 15 | volumes: 16 | - ~/mongo/data:/data/db 17 | 18 | backend: 19 | build: ./backEnd/ 20 | restart: always 21 | ports: 22 | - '3001:3001' 23 | environment: 24 | # DB 25 | - SESSION_SECRET=CHANGEME 26 | - MONGODB_CONNSTRING=mongodb://dbuser:changeme@mongo:27017/ctfDB?authSource=admin 27 | 28 | # Routing 29 | - FRONTEND_URI=http://localhost:3000 30 | 31 | # Mail Verification 32 | - MAIL_VERIFICATION=false 33 | - BACKEND_URI= 34 | - HOST= 35 | - MAIL_PORT= 36 | - MAIL= 37 | - PASS= 38 | 39 | # Docker API 40 | - DEPLOYER_API= 41 | - DEPLOYER_SECRET= 42 | 43 | # Config 44 | - NODE_ENV=development 45 | - PORT=3001 46 | 47 | # First Blood Webhook 48 | - WEBHOOK= 49 | volumes: 50 | - ./backEnd/assets:/server/assets 51 | depends_on: 52 | - mongo 53 | 54 | frontend: 55 | build: ./frontEnd/ 56 | restart: always 57 | ports: 58 | - '3000:3000' 59 | environment: 60 | - REACT_APP_BACKEND_URI=http://localhost:3001 61 | - REACT_APP_CTF_NAME=TEST_CTF 62 | - GENERATE_SOURCEMAP=false -------------------------------------------------------------------------------- /dockerAPI/.env.example: -------------------------------------------------------------------------------- 1 | # Routing 2 | BACKEND_URI= 3 | DOCKER_URI= 4 | 5 | # DB 6 | MONGODB_CONNSTRING= 7 | 8 | # Secrets 9 | SECRET_TOKEN=s3cret 10 | GITHUB_TOKEN= 11 | 12 | # Config 13 | PORT=3002 14 | RANDOM_FLAG_FORMAT= -------------------------------------------------------------------------------- /dockerAPI/controllers/dockerController.js: -------------------------------------------------------------------------------- 1 | const compose = require("docker-compose"); 2 | const dockers = require("../models/dockerModel"); 3 | const path = require("path"); 4 | const cron = require("node-cron"); 5 | const fs = require("fs"); 6 | const simpleGit = require("simple-git"); 7 | const crypto = require("crypto"); 8 | const { fromUrl } = require("hosted-git-info"); 9 | require("dotenv").config(); 10 | 11 | const progress = new Map(); 12 | 13 | // Cron Job to check if docker containers should be stopped after 2 hours 14 | cron.schedule("* * * * *", () => { 15 | dockers 16 | .find({ deployTime: { $lt: new Date(Date.now() - 2 * 60 * 60 * 1000) } }) 17 | .then(async (allDockers) => { 18 | allDockers.forEach(async (docker) => { 19 | if (Date.now() - docker.deployTime >= 1000 * 60 * 60 * 2) { 20 | // stop docker 21 | await compose.stop({ 22 | cwd: docker.path, 23 | composeOptions: [["-p", docker.dockerId]], 24 | }); 25 | await compose.rm({ 26 | cwd: docker.path, 27 | composeOptions: [["-p", docker.dockerId]], 28 | }); 29 | await dockers.findByIdAndDelete(docker._id); 30 | } 31 | }); 32 | }); 33 | }); 34 | 35 | async function getInfosfromUrl(url) { 36 | let infos = await fromUrl(url); 37 | if (!infos.treepath) throw new Error("Invalid project path"); 38 | if (!infos.committish) throw new Error("Invalid project path"); 39 | // Get the challenge path, from Github Url. ex: prog/nop 40 | let projectPath = url.split("/"); 41 | infos.projectPath = projectPath 42 | .slice(projectPath.indexOf(infos.committish) + 1) 43 | .join("/"); 44 | infos.url = projectPath 45 | .slice(0, projectPath.indexOf(infos.committish) - 1) 46 | .join("/"); 47 | 48 | return infos; 49 | } 50 | 51 | async function checkDockerExists(githubUrl) { 52 | // Get git infos from Github Url 53 | let infos = await getInfosfromUrl(githubUrl); 54 | 55 | // Path where the project will be cloned 56 | let dockerPath = `${__dirname}/../dockers/${ 57 | infos.user 58 | }/${infos.project.replace(" ", "_")}`; 59 | 60 | // Create directory for the project 61 | let res = await fs.mkdirSync(dockerPath, { recursive: true }); 62 | 63 | // Setup github module from infos 64 | const git = simpleGit(dockerPath, { 65 | config: ["core.sparsecheckout=true"], 66 | }); 67 | 68 | if (res) { 69 | await git.init(); 70 | let url = `https://${infos.url.slice(8)}.git`; 71 | if (process.env.GITHUB_TOKEN) { 72 | url = `https://${process.env.GITHUB_TOKEN}@${infos.url.slice(8)}.git`; 73 | } 74 | await git.addRemote("origin", url); 75 | } else { 76 | await git.fetch(); 77 | } 78 | 79 | dockerPath += `/${infos.projectPath}`; 80 | 81 | // Pull project 82 | await git.pull("origin", infos.committish); 83 | 84 | return dockerPath; 85 | } 86 | 87 | exports.deployDocker = async function (req, res) { 88 | const ownerId = req.body.ownerId; 89 | const challengeId = req.body.challengeId; 90 | 91 | try { 92 | if (progress.has(challengeId + "_" + ownerId)) { 93 | if (progress.get(challengeId + "_" + ownerId) != "stopping") 94 | throw Error("Docker is launching!"); 95 | else if (progress.get(challengeId + "_" + ownerId) == "stopping") 96 | throw Error("Docker is stopping!"); 97 | } 98 | 99 | await progress.set(challengeId + "_" + ownerId, "deploying"); 100 | 101 | if ( 102 | (await dockers.findOne({ dockerId: challengeId + "_" + ownerId })) != 103 | undefined 104 | ) { 105 | progress.delete(challengeId + "_" + ownerId); 106 | throw new Error("Owner already has a docker running!"); 107 | } 108 | 109 | const dockerPath = await checkDockerExists(req.body.githubUrl).catch( 110 | (e) => { 111 | console.log(e); 112 | throw Error("Failed!"); 113 | } 114 | ); 115 | 116 | if ( 117 | !fs.existsSync(`${dockerPath}/docker-compose.yaml`) && 118 | !fs.existsSync(`${dockerPath}/docker-compose.yml`) 119 | ) { 120 | progress.delete(challengeId + "_" + ownerId); 121 | throw new Error("docker-compose.yml not found in project"); 122 | } 123 | 124 | const randomFlag = crypto.randomBytes(16).toString("hex"); 125 | 126 | if (req.body.randomFlag) { 127 | // Create env 128 | fs.writeFileSync( 129 | path.join(dockerPath, "/" + ownerId + ".env"), 130 | "RANDOM_FLAG=" + 131 | process.env.RANDOM_FLAG_FORMAT.replace("RAND", randomFlag) 132 | ); 133 | 134 | // launch docker 135 | await compose.upAll({ 136 | cwd: dockerPath, 137 | composeOptions: [ 138 | ["-p", challengeId + "_" + ownerId], 139 | ["--env-file", ownerId + ".env"], 140 | ], 141 | }); 142 | 143 | // Delete env 144 | fs.rmSync(path.join(dockerPath, "/" + ownerId + ".env")); 145 | } else { 146 | // launch docker 147 | await compose.upAll({ 148 | cwd: dockerPath, 149 | composeOptions: [["-p", challengeId + "_" + ownerId]], 150 | }); 151 | } 152 | 153 | var containers = await compose.ps({ 154 | cwd: dockerPath, 155 | composeOptions: [["-p", challengeId + "_" + ownerId]], 156 | }); 157 | 158 | let i = 0; 159 | try { 160 | let port = "none"; 161 | 162 | while ( 163 | containers.data.services[i] && 164 | (containers.data.services[i].ports.length == 0 || 165 | !containers.data.services[i].ports[0].hasOwnProperty("mapped")) 166 | ) { 167 | console.log(containers.data.services[i].ports); 168 | i += 1; 169 | } 170 | 171 | if (containers.data.services[i]) { 172 | port = containers.data.services[i].ports[0].mapped.port; 173 | } else { 174 | port = containers.out.split("0.0.0.0:")[1].split("->")[0]; 175 | } 176 | 177 | await dockers.create({ 178 | dockerId: challengeId + "_" + ownerId, 179 | challengeId: challengeId, 180 | ownerId: ownerId, 181 | mappedPort: port, 182 | deployTime: new Date().getTime(), 183 | githubUrl: req.body.githubUrl, 184 | path: dockerPath, 185 | randomFlag: req.body.randomFlag 186 | ? process.env.RANDOM_FLAG_FORMAT.replace("RAND", randomFlag) 187 | : "false", 188 | }); 189 | } catch (err) { 190 | await dockers.deleteOne({ 191 | dockerId: challengeId + "_" + ownerId, 192 | challengeId: challengeId, 193 | ownerId: ownerId, 194 | }); 195 | 196 | await compose.stop({ 197 | cwd: dockerPath, 198 | composeOptions: [["-p", challengeId + "_" + ownerId]], 199 | }); 200 | 201 | await compose.rm({ 202 | cwd: dockerPath, 203 | composeOptions: [["-p", challengeId + "_" + ownerId]], 204 | }); 205 | 206 | progress.delete(challengeId + "_" + ownerId); 207 | 208 | console.log(err); 209 | throw new Error("Error launching docker!"); 210 | } 211 | 212 | progress.delete(challengeId + "_" + ownerId); 213 | 214 | if (req.body.randomFlag) res.send({ state: "success", flag: randomFlag }); 215 | else res.send({ state: "success" }); 216 | } catch (err) { 217 | console.log(err); 218 | if (err) { 219 | res.send({ state: "error", message: err.message }); 220 | } 221 | } 222 | }; 223 | 224 | exports.shutdownDocker = async function (req, res) { 225 | const ownerId = req.body.ownerId; 226 | const challengeId = req.body.challengeId; 227 | 228 | try { 229 | if (progress.has(challengeId + "_" + ownerId)) { 230 | if (progress.get(challengeId + "_" + ownerId) != "stopping") 231 | throw Error("Docker is launching!"); 232 | if (progress.get(challengeId + "_" + ownerId) == "stopping") 233 | throw Error("Docker is stopping!"); 234 | } 235 | 236 | progress.set(challengeId + "_" + ownerId, "stopping"); 237 | 238 | const docker = await dockers.findOne({ 239 | dockerId: challengeId + "_" + ownerId, 240 | }); 241 | 242 | if (!docker) { 243 | progress.delete(challengeId + "_" + ownerId); 244 | throw new Error("This Docker does not exist!"); 245 | } 246 | 247 | // stop docker 248 | await compose.stop({ 249 | cwd: docker.path, 250 | composeOptions: [["-p", challengeId + "_" + ownerId]], 251 | }); 252 | await compose.rm({ 253 | cwd: docker.path, 254 | composeOptions: [["-p", challengeId + "_" + ownerId]], 255 | }); 256 | 257 | await dockers.deleteOne({ 258 | dockerId: challengeId + "_" + ownerId, 259 | challengeId: challengeId, 260 | ownerId: ownerId, 261 | }); 262 | 263 | progress.delete(challengeId + "_" + ownerId); 264 | res.send({ state: "success" }); 265 | } catch (err) { 266 | if (err) { 267 | res.send({ state: "error", message: err.message }); 268 | } 269 | } 270 | }; 271 | 272 | exports.getDockers = async function (req, res) { 273 | const ownerId = req.body.ownerId; 274 | 275 | const ownerDockers = (await dockers.find({ ownerId: ownerId })).map( 276 | (docker) => { 277 | let copy = { ...docker._doc, id: docker.id }; 278 | 279 | if (progress.get(copy.dockerId)) { 280 | copy.progress = progress.get(docker.dockerId); 281 | } 282 | 283 | copy.url = `${process.env.DOCKER_URI}:${docker.mappedPort}`; 284 | 285 | return copy; 286 | } 287 | ); 288 | 289 | res.send({ state: "success", dockers: ownerDockers }); 290 | }; 291 | 292 | exports.getAllDockers = async function (req, res) { 293 | const deployedDockers = ( 294 | await dockers 295 | .find({}) 296 | .skip((parseInt(req.body.page) - 1) * 100) 297 | .limit(100) 298 | ).map((docker) => { 299 | let copy = { ...docker._doc, id: docker.id }; 300 | 301 | if (progress.get(copy.dockerId)) { 302 | copy.progress = progress.get(docker.dockerId); 303 | } 304 | 305 | copy.url = `${process.env.DOCKER_URI}:${docker.mappedPort}`; 306 | return copy; 307 | }); 308 | 309 | res.send({ state: "success", dockers: deployedDockers }); 310 | }; 311 | -------------------------------------------------------------------------------- /dockerAPI/dockers/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | */ 3 | !.gitignore -------------------------------------------------------------------------------- /dockerAPI/models/dockerModel.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const mongoose = require('mongoose'); 3 | var Schema = mongoose.Schema; 4 | 5 | var dockerSchema = new Schema({ 6 | dockerId: { 7 | type: String, 8 | required: true 9 | }, 10 | challengeId: { 11 | type: String, 12 | required: true 13 | }, 14 | ownerId: { 15 | type: String, 16 | required: true 17 | }, 18 | mappedPort: { 19 | type: Number, 20 | required: true, 21 | }, 22 | deployTime: { 23 | type: String, 24 | default: new Date().getTime() 25 | }, 26 | githubUrl: { 27 | type: String, 28 | required: true 29 | }, 30 | path: { 31 | type: String, 32 | required: true 33 | }, 34 | randomFlag: { 35 | type: String, 36 | required: false 37 | } 38 | }); 39 | 40 | module.exports = mongoose.model('dockers', dockerSchema); -------------------------------------------------------------------------------- /dockerAPI/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "compose": "^0.1.2", 4 | "docker-compose": "^0.23.18", 5 | "dotenv": "^16.0.3", 6 | "express": "^4.18.2", 7 | "fs": "^0.0.1-security", 8 | "hosted-git-info": "^6.1.1", 9 | "mongoose": "^6.9.0", 10 | "node-cron": "^3.0.2", 11 | "path": "^0.12.7", 12 | "simple-git": "^3.16.0" 13 | }, 14 | "scripts": { 15 | "test": "echo \"Error: no test specified\" && exit 1", 16 | "start": "node server.js" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /dockerAPI/routes/apiRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const dockerController = require("../controllers/dockerController"); 4 | 5 | router.post('/deployDocker', (req, res) => { 6 | dockerController.deployDocker(req, res); 7 | }); 8 | 9 | router.post('/shutdownDocker', (req, res) => { 10 | dockerController.shutdownDocker(req, res); 11 | }); 12 | 13 | router.post('/getDockers', (req, res) => { 14 | dockerController.getDockers(req, res); 15 | }); 16 | 17 | router.post('/getAllDockers', (req, res) => { 18 | dockerController.getAllDockers(req, res); 19 | }); 20 | 21 | module.exports = router; -------------------------------------------------------------------------------- /dockerAPI/server.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const app = express(); 3 | const port = process.env.PORT || 3002; 4 | const mongoose = require("mongoose"); 5 | const db = mongoose.connection; 6 | const dotenv = require("dotenv"); 7 | dotenv.config(); 8 | const bodyparser = require("body-parser"); 9 | const apiRouter = require("./routes/apiRoutes.js"); 10 | 11 | mongoose.connect(process.env.MONGODB_CONNSTRING, { 12 | authSource: "admin", 13 | useNewUrlParser: true, 14 | useUnifiedTopology: true, 15 | }); 16 | 17 | db.once("open", async function () { 18 | console.log("Database Connected successfully"); 19 | }); 20 | 21 | // Body-parser middleware 22 | app.use(bodyparser.urlencoded({ extended: false })); 23 | app.use(bodyparser.json()); 24 | 25 | // Add headers before the routes are defined 26 | app.use(function (req, res, next) { 27 | // Website you wish to allow to connect 28 | res.setHeader("Access-Control-Allow-Origin", process.env.BACKEND_URI); 29 | 30 | // Request methods you wish to allow 31 | res.setHeader( 32 | "Access-Control-Allow-Methods", 33 | "GET, POST, OPTIONS" 34 | ); 35 | 36 | // Request headers you wish to allow 37 | res.setHeader( 38 | "Access-Control-Allow-Headers", 39 | "Set-Cookie,X-Requested-With,content-type" 40 | ); 41 | 42 | // Set to true if you need the website to include cookies in the requests sent 43 | // to the API (e.g. in case you use sessions) 44 | res.setHeader("Access-Control-Allow-Credentials", true); 45 | 46 | // Pass to next layer of middleware 47 | next(); 48 | }); 49 | 50 | function apiKeyCheck(req, res, next) { 51 | if (req.get("X-API-KEY") == process.env.SECRET_TOKEN) { 52 | next(); 53 | } else { 54 | res.send({ state: "Unauthorized!" }); 55 | } 56 | } 57 | 58 | app.use(apiKeyCheck); 59 | 60 | app.use("/api", apiRouter); 61 | 62 | process.on("uncaughtException", function (err) { 63 | console.log("Uncaught exception: " + err.stack); 64 | console.log("NODE NOT EXITING"); 65 | }); 66 | 67 | app.listen(port, () => { 68 | console.log(`dockerAPI listening on port ${port}`); 69 | }); 70 | -------------------------------------------------------------------------------- /frontEnd/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CTF-Cafe/CTF_Cafe_platform/ffaa0b33eccac31d9374204c7dd71d28818619cc/frontEnd/.DS_Store -------------------------------------------------------------------------------- /frontEnd/.env.example: -------------------------------------------------------------------------------- 1 | # Routing 2 | REACT_APP_BACKEND_URI= 3 | 4 | # Config 5 | REACT_APP_CTF_NAME= 6 | GENERATE_SOURCEMAP= 7 | 8 | #Links 9 | REACT_APP_DISCORD_URI= 10 | REACT_APP_GITHUB_URI= 11 | REACT_APP_TWIITER_URI= 12 | -------------------------------------------------------------------------------- /frontEnd/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine 2 | 3 | WORKDIR /app 4 | 5 | COPY . . 6 | 7 | RUN npm install --production 8 | 9 | EXPOSE 3000 10 | 11 | CMD npm start 12 | -------------------------------------------------------------------------------- /frontEnd/bin.js: -------------------------------------------------------------------------------- 1 | const handler = require('serve-handler'); 2 | const http = require('http'); 3 | const dotenv = require("dotenv"); 4 | dotenv.config(); 5 | 6 | const server = http.createServer((request, response) => { 7 | // You pass two more arguments for config and middleware 8 | // More details here: https://github.com/vercel/serve-handler#options 9 | return handler(request, response, { 10 | cleanUrls: true, 11 | public: "build", 12 | directoryListing: false, 13 | redirects: [ 14 | { "source": "!/**/*.@(js|css|png|jpg|otf|txt|gif))", "destination": "/" }, 15 | ] 16 | }); 17 | }); 18 | 19 | const port = process.env.PORT || 3000; 20 | server.listen(port, () => { 21 | console.log('Running at http://localhost:' + port); 22 | }); -------------------------------------------------------------------------------- /frontEnd/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ctf_cafe_frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@ant-design/plots": "^1.2.3", 7 | "axios": "^0.26.1", 8 | "copy-to-clipboard": "^3.3.1", 9 | "ctf_cafe_frontend": "file:release", 10 | "dotenv": "^16.0.3", 11 | "file-saver": "^2.0.5", 12 | "prismjs": "^1.28.0", 13 | "react": "^17.0.2", 14 | "react-ace": "^10.1.0", 15 | "react-alert": "^7.0.3", 16 | "react-alert-template-basic": "^1.0.2", 17 | "react-countdown": "^2.3.5", 18 | "react-dom": "^17.0.2", 19 | "react-inline-script": "^1.2.0", 20 | "react-loading-screen": "^0.0.17", 21 | "react-markdown": "^8.0.7", 22 | "react-router-dom": "^6.2.2", 23 | "react-scripts": "5.0.0", 24 | "serve": "^13.0.2" 25 | }, 26 | "scripts": { 27 | "start": "npm run build && serve -s build", 28 | "start-react": "react-scripts start", 29 | "build": "react-scripts build", 30 | "release": "npm run build && cd ./release && npm i && npx pkg .", 31 | "test": "react-scripts test", 32 | "eject": "react-scripts eject" 33 | }, 34 | "eslintConfig": { 35 | "extends": [ 36 | "react-app", 37 | "react-app/jest" 38 | ] 39 | }, 40 | "browserslist": { 41 | "production": [ 42 | ">0.2%", 43 | "not dead", 44 | "not op_mini all" 45 | ], 46 | "development": [ 47 | "last 1 chrome version", 48 | "last 1 firefox version", 49 | "last 1 safari version" 50 | ] 51 | }, 52 | "devDependencies": { 53 | "gh-pages": "^3.2.3" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /frontEnd/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CTF-Cafe/CTF_Cafe_platform/ffaa0b33eccac31d9374204c7dd71d28818619cc/frontEnd/public/favicon.ico -------------------------------------------------------------------------------- /frontEnd/public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CTF-Cafe/CTF_Cafe_platform/ffaa0b33eccac31d9374204c7dd71d28818619cc/frontEnd/public/icon.png -------------------------------------------------------------------------------- /frontEnd/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | CTF Cafe 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /frontEnd/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CTF-Cafe/CTF_Cafe_platform/ffaa0b33eccac31d9374204c7dd71d28818619cc/frontEnd/public/logo192.png -------------------------------------------------------------------------------- /frontEnd/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CTF-Cafe/CTF_Cafe_platform/ffaa0b33eccac31d9374204c7dd71d28818619cc/frontEnd/public/logo512.png -------------------------------------------------------------------------------- /frontEnd/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "CTF Cafe", 3 | "name": "Capture The Flag Cafe", 4 | "icons": [{ 5 | "src": "favicon.ico", 6 | "sizes": "64x64 32x32 24x24 16x16", 7 | "type": "image/x-icon" 8 | }, 9 | { 10 | "src": "logo192.png", 11 | "type": "image/png", 12 | "sizes": "192x192" 13 | }, 14 | { 15 | "src": "logo512.png", 16 | "type": "image/png", 17 | "sizes": "512x512" 18 | } 19 | ], 20 | "start_url": ".", 21 | "display": "standalone", 22 | "theme_color": "#000000", 23 | "background_color": "#ffffff" 24 | } -------------------------------------------------------------------------------- /frontEnd/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /frontEnd/restart.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | docker build -t ctf_cafe/frontend . 3 | docker stop frontend_1 4 | docker rm frontend_1 5 | docker run -d -p 3000:3000 --name frontend_1 ctf_cafe/frontend -------------------------------------------------------------------------------- /frontEnd/src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CTF-Cafe/CTF_Cafe_platform/ffaa0b33eccac31d9374204c7dd71d28818619cc/frontEnd/src/.DS_Store -------------------------------------------------------------------------------- /frontEnd/src/components/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CTF-Cafe/CTF_Cafe_platform/ffaa0b33eccac31d9374204c7dd71d28818619cc/frontEnd/src/components/.DS_Store -------------------------------------------------------------------------------- /frontEnd/src/components/Admin/Admin.jsx: -------------------------------------------------------------------------------- 1 | import { Outlet, Routes, Route, Link, useLocation } from "react-router-dom"; 2 | import Stats from "./Stats"; 3 | import Challenges from "./Challenges"; 4 | import Assets from "./Assets"; 5 | import Config from "./Config"; 6 | import Users from "./Users"; 7 | import Teams from "./Teams"; 8 | import Theme from "./Theme"; 9 | import Tools from "./Tools"; 10 | import Logs from "./Logs"; 11 | import Dockers from "./Dockers"; 12 | import ConfirmModal from "../Global/ConfirmModal"; 13 | import { useState } from "react"; 14 | 15 | function Admin(props) { 16 | const location = useLocation(); 17 | const pathName = location.pathname.replace("/admin", ""); 18 | const [action, setAction] = useState({}); 19 | 20 | return ( 21 |
22 |
23 |
24 | 125 |
126 |
127 |
128 |
129 | {pathName == "/" || pathName == "" ? ( 130 | 131 | ) : pathName == "/challenges/" || pathName == "/challenges" ? ( 132 | 133 | ) : pathName == "/assets/" || pathName == "/assets" ? ( 134 | 135 | ) : pathName == "/config/" || pathName == "/config" ? ( 136 | 137 | ) : pathName == "/users/" || pathName == "/users" ? ( 138 | 139 | ) : pathName == "/teams/" || pathName == "/teams" ? ( 140 | 141 | ) : pathName == "/theme/" || pathName == "/theme" ? ( 142 | 143 | ) : pathName == "/tools/" || pathName == "/tools" ? ( 144 | 145 | ) : pathName == "/logs/" || pathName == "/logs" ? ( 146 | 147 | ) : pathName == "/dockers/" || pathName == "/dockers" ? ( 148 | 149 | ) : ( 150 |

151 | 404  152 |

153 | )} 154 |
155 |
156 | 157 |
158 | ); 159 | } 160 | 161 | export default Admin; 162 | -------------------------------------------------------------------------------- /frontEnd/src/components/Admin/Assets.jsx: -------------------------------------------------------------------------------- 1 | import { Outlet, Routes, Route, Link } from "react-router-dom"; 2 | import { useState, useEffect, useContext } from "react"; 3 | import axios from "axios"; 4 | import AppContext from "../Data/AppContext"; 5 | 6 | function Assets(props) { 7 | const globalData = useContext(AppContext); 8 | const [assets, setAssets] = useState([]); 9 | 10 | const getAssets = () => { 11 | axios 12 | .get(process.env.REACT_APP_BACKEND_URI + "/api/admin/getAssets", { withCredentials: true }) 13 | .then((response) => { 14 | if (response.data.state == "sessionError") { 15 | globalData.alert.error("Session expired!"); 16 | globalData.setUserData({}); 17 | globalData.setLoggedIn(false); 18 | globalData.navigate("/", { replace: true }); 19 | } else { 20 | setAssets(response.data); 21 | } 22 | }) 23 | .catch((err) => { 24 | console.log(err.message); 25 | }); 26 | }; 27 | 28 | useEffect(() => { 29 | getAssets(); 30 | }, []); 31 | 32 | const deleteAsset = (e, asset) => { 33 | e.preventDefault(); 34 | axios 35 | .post(process.env.REACT_APP_BACKEND_URI + "/api/admin/deleteAsset", { 36 | asset: asset.name, 37 | }, { withCredentials: true }) 38 | .then((response) => { 39 | if (response.data.state == "sessionError") { 40 | globalData.alert.error("Session expired!"); 41 | globalData.setUserData({}); 42 | globalData.setLoggedIn(false); 43 | globalData.navigate("/", { replace: true }); 44 | } else { 45 | if (response.data.state == "success") { 46 | globalData.alert.success("Deleted asset!"); 47 | getAssets(); 48 | } else { 49 | globalData.alert.error(response.data.message); 50 | getAssets(); 51 | } 52 | } 53 | }) 54 | .catch((err) => { 55 | console.log(err.message); 56 | }); 57 | }; 58 | 59 | const fileUpload = (e) => { 60 | var formData = new FormData(); 61 | var file = e.target; 62 | formData.append("file", file.files[0]); 63 | 64 | axios.post(process.env.REACT_APP_BACKEND_URI + "/api/admin/uploadAsset", formData, { 65 | headers: { 66 | 'Content-Type': 'multipart/form-data' 67 | }, 68 | withCredentials: true 69 | }).then((response) => { 70 | if (response.data.state == "sessionError") { 71 | globalData.alert.error("Session expired!"); 72 | globalData.setUserData({}); 73 | globalData.setLoggedIn(false); 74 | globalData.navigate("/", { replace: true }); 75 | } else { 76 | if (response.data.state == "success") { 77 | globalData.alert.success("Uploaded asset!"); 78 | getAssets(); 79 | } else { 80 | globalData.alert.error(response.data.message); 81 | getAssets(); 82 | } 83 | } 84 | }) 85 | .catch((err) => { 86 | console.log(err.message); 87 | }); 88 | } 89 | 90 | return ( 91 |
92 |

96 | ASSETS 97 |

98 |
99 | 105 | 106 |
107 | 108 | 109 | 110 | 113 | 114 | 115 | 116 | 117 | {assets.map((asset, index) => { 118 | return ( 119 | 120 | 123 | 136 | 137 | ); 138 | })} 139 | 140 |
111 | # 112 | File Name
121 | {index} 122 | 124 | { 128 | deleteAsset(e, asset); 129 | }} 130 | style={{ marginRight: "30px" }} 131 | > 132 | 133 | 134 | {asset.name} 135 |
141 |
142 | ); 143 | } 144 | 145 | export default Assets; 146 | -------------------------------------------------------------------------------- /frontEnd/src/components/Admin/Config.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useContext } from "react"; 2 | import axios from "axios"; 3 | import AppContext from "../Data/AppContext"; 4 | const configsToShow = [ 5 | "startTime", 6 | "endTime", 7 | "rules", 8 | "sponsors", 9 | "socialLinks", 10 | "tags", 11 | "tagColors", 12 | "dynamicScoring", 13 | "scoreboardHidden", 14 | "dockerLimit", 15 | "userCategories" 16 | ]; 17 | 18 | function toDatetimeLocal(date) { 19 | let ten = function (i) { 20 | return (i < 10 ? '0' : '') + i; 21 | }, 22 | YYYY = date.getFullYear(), 23 | MM = ten(date.getMonth() + 1), 24 | DD = ten(date.getDate()), 25 | HH = ten(date.getHours()), 26 | II = ten(date.getMinutes()), 27 | SS = ten(date.getSeconds()) 28 | ; 29 | return YYYY + '-' + MM + '-' + DD + 'T' + 30 | HH + ':' + II + ':' + SS; 31 | }; 32 | 33 | function ArrayEdit(props) { 34 | const config = props.config; 35 | const [array, setArray] = useState(config.value); 36 | 37 | const removeFromArray = (item) => { 38 | setArray([...array.filter((x) => x != item)]); 39 | }; 40 | 41 | const addToArray = () => { 42 | setArray([...array, "example"]); 43 | }; 44 | 45 | const updateArray = (e, item) => { 46 | let tmpArray = array; 47 | e.target.style = "width: " + (e.target.value.length + 2) + "ch;"; 48 | tmpArray[tmpArray.indexOf(item)] = e.target.value; 49 | setArray([...tmpArray]); 50 | }; 51 | 52 | return ( 53 | 54 | 61 | {array.map((item) => { 62 | return ( 63 | 70 | updateArray(e, item)} 72 | className="min-input" 73 | defaultValue={item} 74 | type="text" 75 | style={{ width: item.length + 2 + "ch" }} 76 | key={array.length} 77 | /> 78 | 85 | 86 | ); 87 | })} 88 | 89 | ); 90 | } 91 | 92 | function Config(props) { 93 | const globalData = useContext(AppContext); 94 | const [configs, setConfigs] = useState([]); 95 | 96 | const getConfigs = () => { 97 | axios 98 | .get(process.env.REACT_APP_BACKEND_URI + "/api/getConfigs") 99 | .then((response) => { 100 | if (response.data.state == "sessionError") { 101 | globalData.alert.error("Session expired!"); 102 | globalData.setUserData({}); 103 | globalData.setLoggedIn(false); 104 | globalData.navigate("/", { replace: true }); 105 | } else { 106 | response.data.sort((a, b) => { 107 | const aIndex = configsToShow.indexOf(a.name); 108 | const bIndex = configsToShow.indexOf(b.name); 109 | return aIndex - bIndex; 110 | }); 111 | setConfigs(response.data); 112 | } 113 | }) 114 | .catch((err) => { 115 | console.log(err.message); 116 | }); 117 | }; 118 | 119 | useEffect(() => { 120 | getConfigs(); 121 | }, []); 122 | 123 | const saveConfig = () => { 124 | let configsArray = []; 125 | 126 | configs.forEach((config) => { 127 | if (configsToShow.includes(config.name)) { 128 | configsArray.push({ 129 | name: config.name, 130 | value: ["startTime", "endTime"].includes(config.name) 131 | ? new Date( 132 | document.getElementById("config-data" + config._id).value 133 | ).getTime() 134 | : config.name === "tags" 135 | ? document.getElementById("config-data" + config._id).attributes 136 | .value.nodeValue 137 | : config.name === "dockerLimit" 138 | ? parseInt(document.getElementById("config-data" + config._id).value) || 0 139 | : ["dynamicScoring", "scoreboardHidden"].includes(config.name) 140 | ? document.getElementById("config-data" + config._id).value 141 | : document.getElementById("config-data" + config._id).textContent, 142 | }); 143 | } 144 | }); 145 | 146 | axios 147 | .post( 148 | process.env.REACT_APP_BACKEND_URI + "/api/admin/saveConfigs", 149 | { 150 | newConfigs: configsArray, 151 | }, 152 | { withCredentials: true } 153 | ) 154 | .then((response) => { 155 | if (response.data.state === "sessionError") { 156 | globalData.alert.error("Session expired!"); 157 | globalData.setUserData({}); 158 | globalData.setLoggedIn(false); 159 | globalData.navigate("/", { replace: true }); 160 | } else { 161 | if (response.data.state === "success") { 162 | globalData.alert.success("Updated config!"); 163 | getConfigs(); 164 | } else { 165 | globalData.alert.error(response.data.message); 166 | getConfigs(); 167 | } 168 | } 169 | }) 170 | .catch((err) => { 171 | console.log(err.message); 172 | }); 173 | }; 174 | 175 | return ( 176 |
177 |

181 | CONFIG 182 |

183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | {configs.map((config, _) => { 192 | if (configsToShow.includes(config.name)) { 193 | return ( 194 | 195 | 196 | {["startTime", "endTime"].includes(config.name) ? ( 197 | 204 | ) : config.name === "tags" ? ( 205 | 206 | ) : ["dynamicScoring", "scoreboardHidden"].includes( 207 | config.name 208 | ) ? ( 209 | 218 | ) : config.name === "dockerLimit" ? ( 219 | 226 | ) : ( 227 | 232 | )} 233 | 234 | ); 235 | } 236 | })} 237 | 238 |
Config Name Config Data
{config.name} 198 | 203 | 210 | 217 | 220 | 225 | 228 |
229 |                         {JSON.stringify(config.value, null, 2)}
230 |                       
231 |
239 | 249 |
250 | ); 251 | } 252 | 253 | export default Config; 254 | -------------------------------------------------------------------------------- /frontEnd/src/components/Admin/Dockers.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useContext } from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | import axios from "axios"; 5 | import AppContext from "../Data/AppContext"; 6 | 7 | function Dockers(props) { 8 | const globalData = useContext(AppContext); 9 | const [dockers, setDockers] = useState([]); 10 | const [page, setPage] = useState(1); 11 | const [searchQuery, setSearchQuery] = useState(""); 12 | const [editMode, setEditMode] = useState(false); 13 | 14 | const getDockers = (index) => { 15 | axios 16 | .post( 17 | process.env.REACT_APP_BACKEND_URI + "/api/admin/getDockers", 18 | { 19 | page: index, 20 | search: searchQuery, 21 | }, 22 | { withCredentials: true } 23 | ) 24 | .then((response) => { 25 | if (response.data.state == "sessionError") { 26 | globalData.alert.error("Session expired!"); 27 | globalData.setUserData({}); 28 | globalData.setLoggedIn(false); 29 | globalData.navigate("/", { replace: true }); 30 | } else if (response.data.state == "error") { 31 | globalData.alert.error(response.data.message); 32 | } else { 33 | setDockers(response.data); 34 | setPage(index); 35 | } 36 | }) 37 | .catch((err) => { 38 | console.log(err.message); 39 | }); 40 | }; 41 | 42 | useEffect(() => { 43 | getDockers(page); 44 | }, [searchQuery]); 45 | 46 | const shutdownDocker = (docker) => { 47 | docker.progress = "stopping"; 48 | setDockers([...dockers]); 49 | 50 | axios 51 | .post( 52 | process.env.REACT_APP_BACKEND_URI + "/api/admin/shutdownDocker", 53 | { 54 | docker: docker, 55 | }, 56 | { 57 | withCredentials: true, 58 | } 59 | ) 60 | .then((response) => { 61 | if (response.data.state == "sessionError") { 62 | globalData.alert.error("Session expired!"); 63 | globalData.setUserData({}); 64 | globalData.setLoggedIn(false); 65 | globalData.navigate("/", { replace: true }); 66 | } else if (response.data.state == "error") { 67 | globalData.alert.error(response.data.message); 68 | delete docker.progress; 69 | setDockers([...dockers]); 70 | } else { 71 | globalData.alert.success("Docker stopped!"); 72 | getDockers(); 73 | } 74 | }) 75 | .catch((err) => { 76 | console.log(err.message); 77 | }); 78 | }; 79 | 80 | const restartDocker = (docker) => { 81 | docker.progress = "restarting"; 82 | setDockers([...dockers]); 83 | 84 | axios 85 | .post( 86 | process.env.REACT_APP_BACKEND_URI + "/api/admin/restartDocker", 87 | { 88 | docker: docker, 89 | }, 90 | { 91 | withCredentials: true, 92 | } 93 | ) 94 | .then((response) => { 95 | if (response.data.state == "sessionError") { 96 | globalData.alert.error("Session expired!"); 97 | globalData.setUserData({}); 98 | globalData.setLoggedIn(false); 99 | globalData.navigate("/", { replace: true }); 100 | } else if (response.data.state == "error") { 101 | globalData.alert.error(response.data.message); 102 | delete docker.progress; 103 | setDockers([...dockers]); 104 | } else { 105 | globalData.alert.success("Docker restarted!"); 106 | getDockers(); 107 | } 108 | }) 109 | .catch((err) => { 110 | console.log(err.message); 111 | }); 112 | }; 113 | 114 | return ( 115 |
116 |

120 | DOCKERS 121 |

122 |
129 |
130 | 137 | 144 | 151 |
152 |
153 | { 159 | setPage(1); 160 | setSearchQuery(e.target.value); 161 | }} 162 | /> 163 |
164 |
165 | 166 | 167 | 168 | 171 | 172 | 173 | 174 | 175 | 176 | {editMode && } 177 | 178 | 179 | 180 | {dockers.map((docker, index) => { 181 | return ( 182 | 183 | 186 | 187 | 195 | 196 | 197 | 202 | {editMode && ( 203 | 239 | )} 240 | 241 | ); 242 | })} 243 | 244 |
169 | # 170 | ChallengeTeamPortDeploy TimeRandom FlagActions
184 | {index + (page - 1) * 100} 185 | {docker.challenge.name} 188 | 192 | {docker.team.name} 193 | 194 | {docker.mappedPort}{docker.deployTime} 198 | {docker.randomFlag 199 | ? docker.randomFlag.substr(0, 10) + "..." 200 | : "none"} 201 | 204 | 223 | 238 |
245 |
252 | {/* Pagination Div */} 253 |
254 | 261 | 268 |
269 |
270 |
271 | ); 272 | } 273 | 274 | export default Dockers; 275 | -------------------------------------------------------------------------------- /frontEnd/src/components/Admin/Logs.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useContext } from "react"; 2 | import axios from "axios"; 3 | import AppContext from "../Data/AppContext"; 4 | 5 | function Stats() { 6 | const globalData = useContext(AppContext); 7 | const [logs, setLogs] = useState([]); 8 | const [searchQuery, setSearchQuery] = useState(""); 9 | const [page, setPage] = useState(1); 10 | 11 | const getLogs = (index) => { 12 | axios 13 | .post( 14 | process.env.REACT_APP_BACKEND_URI + "/api/admin/getLogs", 15 | { 16 | page: index, 17 | search: searchQuery, 18 | }, 19 | { 20 | withCredentials: true, 21 | } 22 | ) 23 | .then((response) => { 24 | if (response.data.state == "sessionError") { 25 | globalData.alert.error("Session expired!"); 26 | globalData.setUserData({}); 27 | globalData.setLoggedIn(false); 28 | globalData.navigate("/", { replace: true }); 29 | } else if (response.data.state == "error") { 30 | globalData.alert.error(response.data.message); 31 | } else { 32 | setLogs(response.data); 33 | setPage(index); 34 | } 35 | }) 36 | .catch((err) => { 37 | console.log(err.message); 38 | }); 39 | }; 40 | 41 | useEffect(() => { 42 | getLogs(page); 43 | }, [searchQuery]); 44 | 45 | return ( 46 |
47 |

51 | LOGS 52 |

53 |
60 |
61 | 68 | 75 | 83 | 84 | 85 |
86 |
87 | { 93 | setSearchQuery(e.target.value); 94 | }} 95 | /> 96 |
97 |
98 | 99 | 100 | 101 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | {logs.map((log, index) => { 113 | return ( 114 | 115 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | ); 125 | })} 126 | 127 |
102 | # 103 | Author IPAuthor IDAuthor NameFunctionResult
116 | {index + ((page - 1)* 100)} 117 | {log.authorIp}{log.authorId.substring(0, 8)}{log.authorName}{log.function.replace("exports.", "")}{log.result ? JSON.parse(log.result).message : ""}
128 |
135 | {/* Pagination Div */} 136 |
137 | 144 | 151 |
152 |
153 |
154 | ); 155 | } 156 | 157 | export default Stats; 158 | -------------------------------------------------------------------------------- /frontEnd/src/components/Admin/Stats.jsx: -------------------------------------------------------------------------------- 1 | import ColumnChart from "../Charts/ColumnChart"; 2 | import PieChart from "../Charts/PieChart"; 3 | import { useState, useEffect, useContext } from "react"; 4 | import axios from "axios"; 5 | import AppContext from "../Data/AppContext"; 6 | 7 | function Stats(props) { 8 | const globalData = useContext(AppContext); 9 | const [counts, setCounts] = useState({}); 10 | const [challengeSolves, setChallengeSolves] = useState([]); 11 | const [challengeStatsTags, setChallengeStatsTags] = useState([]); 12 | const [challengeStatsDifficulty, setChallengeStatsDifficulty] = useState([]); 13 | 14 | const getStats = () => { 15 | axios 16 | .post( 17 | process.env.REACT_APP_BACKEND_URI + "/api/admin/getStats", 18 | { 19 | name: "counts", 20 | }, 21 | { withCredentials: true } 22 | ) 23 | .then((response) => { 24 | if (response.data.state == "sessionError") { 25 | globalData.alert.error("Session expired!"); 26 | globalData.setUserData({}); 27 | globalData.setLoggedIn(false); 28 | globalData.navigate("/", { replace: true }); 29 | } else { 30 | setCounts(response.data); 31 | } 32 | }) 33 | .catch((err) => { 34 | console.log(err.message); 35 | }); 36 | 37 | axios 38 | .post( 39 | process.env.REACT_APP_BACKEND_URI + "/api/admin/getStats", 40 | { 41 | name: "challenges", 42 | }, 43 | { withCredentials: true } 44 | ) 45 | .then((response) => { 46 | if (response.data.state == "sessionError") { 47 | globalData.alert.error("Session expired!"); 48 | globalData.setUserData({}); 49 | globalData.setLoggedIn(false); 50 | globalData.navigate("/", { replace: true }); 51 | } else { 52 | let finalDataTags = []; 53 | let finalDataDifficulty = []; 54 | let finalDataSolves = []; 55 | 56 | response.data.forEach((data) => { 57 | finalDataSolves.push({ 58 | name: data.name, 59 | solves: data.solveCount, 60 | tag: data.tags, 61 | }); 62 | 63 | var result = finalDataTags.find((obj) => { 64 | return obj.name === data.tags[0]; 65 | }); 66 | 67 | if (result) { 68 | result.value += data.solveCount; 69 | } else { 70 | finalDataTags.push({ 71 | name: data.tags[0], 72 | value: data.solveCount, 73 | }); 74 | } 75 | 76 | var result = finalDataDifficulty.find((obj) => { 77 | return ( 78 | obj.name == 79 | (data.level == 3 80 | ? "Ninja" 81 | : data.level == 2 82 | ? "Hard" 83 | : data.level == 1 84 | ? "Medium" 85 | : "Easy") 86 | ); 87 | }); 88 | 89 | if (result) { 90 | finalDataDifficulty[finalDataDifficulty.indexOf(result)].value += 91 | data.solveCount; 92 | } else { 93 | finalDataDifficulty.push({ 94 | name: 95 | data.level == 3 96 | ? "Ninja" 97 | : data.level == 2 98 | ? "Hard" 99 | : data.level == 1 100 | ? "Medium" 101 | : "Easy", 102 | value: data.solveCount, 103 | }); 104 | } 105 | }); 106 | 107 | setChallengeSolves(finalDataSolves); 108 | setChallengeStatsTags(finalDataTags); 109 | setChallengeStatsDifficulty(finalDataDifficulty); 110 | } 111 | }) 112 | .catch((err) => { 113 | console.log(err.message); 114 | }); 115 | }; 116 | 117 | useEffect(() => { 118 | getStats(); 119 | }, []); 120 | 121 | return ( 122 |
123 |

127 | STATS 128 |

129 |
130 |
131 |

CTF Stats

132 |

Total Users: {counts.usersCount}

133 |

Total Teams: {counts.teamsCount}

134 |

Total Challenges: {counts.challengesCount}

135 |
136 |
137 |

Solve Counts

138 | 139 |
140 |
141 |
142 |
143 |
144 |

Solves by Tags

145 | 146 |
147 |
148 |
149 |
150 |

Solves by Difficulty

151 | 152 |
153 |
154 |
155 |
156 | ); 157 | } 158 | 159 | export default Stats; 160 | -------------------------------------------------------------------------------- /frontEnd/src/components/Admin/Teams.jsx: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | import { useState, useEffect, useContext } from "react"; 3 | import axios from "axios"; 4 | import AppContext from "../Data/AppContext"; 5 | 6 | function Teams(props) { 7 | const globalData = useContext(AppContext); 8 | const [teams, setTeams] = useState([]); 9 | const [page, setPage] = useState(1); 10 | const [searchQuery, setSearchQuery] = useState(""); 11 | const [editMode, setEditMode] = useState(false); 12 | 13 | useEffect(() => { 14 | getTeams(page); 15 | }, [searchQuery]); 16 | 17 | const getTeams = (index) => { 18 | axios 19 | .post( 20 | process.env.REACT_APP_BACKEND_URI + "/api/admin/getTeams", 21 | { 22 | page: index, 23 | search: searchQuery, 24 | }, 25 | { withCredentials: true } 26 | ) 27 | .then((response) => { 28 | if (response.data.state == "sessionError") { 29 | globalData.alert.error("Session expired!"); 30 | globalData.setUserData({}); 31 | globalData.setLoggedIn(false); 32 | globalData.navigate("/", { replace: true }); 33 | } else if (response.data.state == "error") { 34 | globalData.alert.error(response.data.message); 35 | } else { 36 | setTeams(response.data); 37 | setPage(index); 38 | } 39 | }) 40 | .catch((err) => { 41 | console.log(err.message); 42 | }); 43 | }; 44 | 45 | useEffect(() => { 46 | getTeams(1); 47 | }, []); 48 | 49 | const deleteTeam = (e, team) => { 50 | axios 51 | .post( 52 | process.env.REACT_APP_BACKEND_URI + "/api/admin/deleteTeam", 53 | { 54 | team: team, 55 | }, 56 | { withCredentials: true } 57 | ) 58 | .then((response) => { 59 | if (response.data.state == "sessionError") { 60 | globalData.alert.error("Session expired!"); 61 | globalData.setUserData({}); 62 | globalData.setLoggedIn(false); 63 | globalData.navigate("/", { replace: true }); 64 | } else { 65 | if (response.data.state == "success") { 66 | globalData.alert.success("Deleted team!"); 67 | getTeams(page); 68 | } else { 69 | globalData.alert.error(response.data.message); 70 | getTeams(page); 71 | } 72 | } 73 | }) 74 | .catch((err) => { 75 | console.log(err.message); 76 | }); 77 | }; 78 | 79 | const previousPage = () => { 80 | getTeams(page - 1); 81 | }; 82 | 83 | const nextPage = () => { 84 | getTeams(page + 1); 85 | }; 86 | 87 | return ( 88 |
89 |

93 | TEAMS 94 |

95 |
102 |
103 | 110 | 117 | 124 |
125 |
126 | { 132 | setPage(1); 133 | setSearchQuery(e.target.value); 134 | }} 135 | /> 136 |
137 |
138 | 139 | 140 | 141 | 144 | 145 | 146 | 147 | {editMode && } 148 | 149 | 150 | 151 | {teams.map((team, index) => { 152 | return ( 153 | 154 | 157 | 165 | 170 | 171 | {editMode && ( 172 | 189 | )} 190 | 191 | ); 192 | })} 193 | 194 |
142 | # 143 | Team NameTeam UsersTeam CategoryDelete Team
155 | {index + (page - 1) * 100} 156 | 158 | 162 | {team.name} 163 | 164 | 166 | {team.users.map((user) => { 167 | return

{user.username}

; 168 | })} 169 |
{team.category} 173 | 188 |
195 |
202 | {/* Pagination Div */} 203 |
204 | 211 | 218 |
219 |
220 |
221 | ); 222 | } 223 | 224 | export default Teams; 225 | -------------------------------------------------------------------------------- /frontEnd/src/components/Admin/Theme.jsx: -------------------------------------------------------------------------------- 1 | import { Outlet, Routes, Route, Link } from "react-router-dom"; 2 | import { useState, useEffect, useContext } from "react"; 3 | import axios from "axios"; 4 | import AppContext from "../Data/AppContext"; 5 | 6 | function Config(props) { 7 | const globalData = useContext(AppContext); 8 | 9 | const getTheme = () => { 10 | axios 11 | .get(process.env.REACT_APP_BACKEND_URI + "/api/getTheme", { 12 | withCredentials: true, 13 | }) 14 | .then((response) => { 15 | if (response.data.state == "sessionError") { 16 | globalData.alert.error("Session expired!"); 17 | globalData.setUserData({}); 18 | globalData.setLoggedIn(false); 19 | globalData.navigate("/", { replace: true }); 20 | } else { 21 | globalData.setTheme(response.data.theme); 22 | 23 | const root = document.documentElement; 24 | 25 | root.style.setProperty( 26 | "--color-1", 27 | response.data.theme.color_1 28 | ? response.data.theme.color_1 29 | : "#ff3c5c" 30 | ); 31 | root.style.setProperty( 32 | "--color-2", 33 | response.data.theme.color_2 34 | ? response.data.theme.color_2 35 | : "#ff707f" 36 | ); 37 | root.style.setProperty( 38 | "--color-1-50", 39 | response.data.theme.color_1 40 | ? response.data.theme.color_1 + "50" 41 | : "#ff707f50" 42 | ); 43 | root.style.setProperty( 44 | "--bg-img", 45 | response.data.theme.bg_img 46 | ? `url(${response.data.theme.bg_img})` 47 | : "none" 48 | ); 49 | } 50 | }) 51 | .catch((err) => { 52 | console.log(err.message); 53 | }); 54 | }; 55 | 56 | useEffect(() => { 57 | getTheme(); 58 | }, []); 59 | 60 | const saveTheme = () => { 61 | axios 62 | .post( 63 | process.env.REACT_APP_BACKEND_URI + "/api/admin/saveTheme", 64 | { 65 | color_1: document.getElementById("theme_color_1").value, 66 | color_2: document.getElementById("theme_color_2").value, 67 | bg_img: document.getElementById("theme_img").textContent, 68 | top1_icon: document.getElementById("theme_top1").textContent, 69 | top2_icon: document.getElementById("theme_top2").textContent, 70 | top3_icon: document.getElementById("theme_top3").textContent, 71 | }, 72 | { withCredentials: true } 73 | ) 74 | .then((response) => { 75 | if (response.data.state == "sessionError") { 76 | globalData.alert.error("Session expired!"); 77 | globalData.setUserData({}); 78 | globalData.setLoggedIn(false); 79 | globalData.navigate("/", { replace: true }); 80 | } else { 81 | if (response.data.state == "success") { 82 | globalData.alert.success("Updated theme!"); 83 | getTheme(); 84 | } else { 85 | globalData.alert.error(response.data.message); 86 | getTheme(); 87 | } 88 | } 89 | }) 90 | .catch((err) => { 91 | console.log(err.message); 92 | }); 93 | }; 94 | 95 | return ( 96 |
97 |

101 | THEME 102 |

103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 120 | 121 | 122 | 123 | 130 | 131 | 132 | 133 | 136 | 137 | 138 | 139 | 142 | 143 | 144 | 145 | 148 | 149 | 150 | 151 | 154 | 155 | 156 |
Config NameConfig Value
COLOR #1 114 | 119 |
COLOR #2 124 | 129 |
BACKROUND IMAGE 134 | {globalData.theme.bg_img} 135 |
TOP1 Icon 140 | {globalData.theme.top1_icon} 141 |
TOP2 Icon 146 | {globalData.theme.top2_icon} 147 |
TOP3 Icon 152 | {globalData.theme.top3_icon} 153 |
157 | 167 |
168 | ); 169 | } 170 | 171 | export default Config; 172 | -------------------------------------------------------------------------------- /frontEnd/src/components/Admin/Tools.jsx: -------------------------------------------------------------------------------- 1 | import { Outlet, Routes, Route, Link } from "react-router-dom"; 2 | import { useState, useEffect, useContext } from "react"; 3 | import axios from "axios"; 4 | import AppContext from "../Data/AppContext"; 5 | 6 | function Tools(props) { 7 | const globalData = useContext(AppContext); 8 | const [teams, setTeams] = useState([]); 9 | 10 | const getTeams = () => { 11 | axios 12 | .get(process.env.REACT_APP_BACKEND_URI + "/api/getTeams") 13 | .then((response) => { 14 | if (response.data.state == "sessionError") { 15 | globalData.alert.error("Session expired!"); 16 | globalData.setUserData({}); 17 | globalData.setLoggedIn(false); 18 | globalData.navigate("/", { replace: true }); 19 | } else { 20 | response.data.forEach((team) => { 21 | team.users.forEach((user) => { 22 | if (team.totalScore) { 23 | team.totalScore += user.score; 24 | } else { 25 | team.totalScore = user.score; 26 | } 27 | }); 28 | }); 29 | 30 | response.data.sort((a, b) => { 31 | if (a.totalScore < b.totalScore) { 32 | return 1; 33 | } 34 | 35 | if (a.totalScore > b.totalScore) { 36 | return -1; 37 | } 38 | 39 | return 0; 40 | }); 41 | 42 | setTeams(response.data); 43 | } 44 | }) 45 | .catch((err) => { 46 | console.log(err.message); 47 | }); 48 | }; 49 | 50 | useEffect(() => { 51 | getTeams(); 52 | }, []); 53 | 54 | const downloadCert = () => { 55 | var canvas = document.getElementById("canvas"), 56 | ctx = canvas.getContext("2d"); 57 | 58 | canvas.width = document.querySelector("#certImg").width; 59 | 60 | canvas.crossOrigin = "Anonymous"; 61 | 62 | canvas.height = document.querySelector("#certImg").height; 63 | 64 | ctx.drawImage(document.querySelector("#certImg"), 0, 0); 65 | 66 | var selectedTeam = document.querySelector("#cert_team").value; 67 | 68 | var team = teams.find((obj) => { 69 | return obj.name === selectedTeam; 70 | }); 71 | 72 | var index = teams.indexOf(team) + 1; 73 | var teamLength = teams.length; 74 | 75 | if (team === undefined) { 76 | index = 1; 77 | team = { 78 | name: "TEST_TEAM", 79 | }; 80 | } 81 | 82 | if (index < 10) { 83 | index = "00" + index.toString(); 84 | } else if (index < 100) { 85 | index = "0" + index.toString(); 86 | } 87 | 88 | if (teamLength < 10) { 89 | teamLength = "00" + teamLength.toString(); 90 | } else if (index < 100) { 91 | teamLength = "0" + teamLength.toString(); 92 | } 93 | 94 | // CTF Name 95 | ctx.font = "bold 55px Fira Code"; 96 | ctx.fillStyle = "red"; 97 | ctx.fillText(process.env.REACT_APP_CTF_NAME, 1350, 120); 98 | 99 | // Team Nae 100 | ctx.font = "bold 34px Fira Code"; 101 | ctx.fillStyle = "red"; 102 | ctx.fillText(team.name, 410, 178); 103 | 104 | // Team Placement 105 | ctx.font = "bold 80px Fira Code"; 106 | ctx.fillStyle = "red"; 107 | ctx.fillText(index, 1050, 275); 108 | 109 | // Total Teams 110 | ctx.font = "bold 80px Fira Code"; 111 | ctx.fillStyle = "red"; 112 | ctx.fillText(teamLength, 1250, 275); 113 | 114 | var anchor = document.createElement("a"); 115 | anchor.href = canvas.toDataURL("image/png"); 116 | anchor.download = "IMAGE.PNG"; 117 | anchor.click(); 118 | }; 119 | 120 | const sendGlobalMessage = () => { 121 | const globalMessage = document.getElementById("global_message").value; 122 | 123 | axios 124 | .post(process.env.REACT_APP_BACKEND_URI + "/api/admin/sendGlobalMessage", { 125 | globalMessage: globalMessage 126 | }, { withCredentials: true }) 127 | .then((response) => { 128 | if (response.data.state == "sessionError") { 129 | globalData.alert.error("Session expired!"); 130 | globalData.setUserData({}); 131 | globalData.setLoggedIn(false); 132 | globalData.navigate("/", { replace: true }); 133 | } else { 134 | if(response.data.state == "success") { 135 | globalData.alert.success("Message sent!"); 136 | } else { 137 | globalData.alert.error(response.data.message); 138 | } 139 | } 140 | }) 141 | .catch((err) => { 142 | console.log(err.message); 143 | }); 144 | }; 145 | 146 | return ( 147 |
148 |

152 | TOOLS 153 |

154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | {/* Hidden Stuff */} 164 | 170 | 173 | {/* 174 | 175 | 187 | 199 | */} 200 | 201 | 202 | 205 | 217 | 218 | 219 |
NAMEINPUTRUN
Generate Certificate 176 | 186 | 188 | 198 |
Send Global Message 203 | 204 | 206 | 216 |
220 |
221 | ); 222 | } 223 | 224 | export default Tools; 225 | -------------------------------------------------------------------------------- /frontEnd/src/components/Charts/ColumnChart.jsx: -------------------------------------------------------------------------------- 1 | import { Column } from "@ant-design/plots"; 2 | 3 | function ColumnChart(props) { 4 | let data = props.data; 5 | 6 | const config = { 7 | data, 8 | xField: "name", 9 | yField: "solves", 10 | seriesField: "tag", // or seriesField in some cases 11 | color: ({ tag }) => { 12 | return ( 13 | props.tagColors.find((x) => tag.includes(x.name)) || { 14 | color: "#ADD8E6", 15 | } 16 | ).color; 17 | }, 18 | slider: { 19 | start: 0, 20 | end: 1, 21 | }, 22 | }; 23 | 24 | return ( 25 |
26 | 27 |
28 | ); 29 | } 30 | 31 | export default ColumnChart; 32 | -------------------------------------------------------------------------------- /frontEnd/src/components/Charts/LineChart.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { Line } from "@ant-design/plots"; 3 | 4 | const LineChart = (props) => { 5 | const [data, setData] = useState([]); 6 | 7 | useEffect(() => { 8 | let rawData = [...props.data.slice(0, 10)]; 9 | 10 | let structuredData = []; 11 | 12 | if (props.selection === "Users") { 13 | rawData.forEach((user) => { 14 | let currentPoints = 0; 15 | let d = new Date(props.startTime); 16 | let dformat = 17 | [d.getMonth() + 1, d.getDate(), d.getFullYear()].join("/") + 18 | " " + 19 | [d.getHours(), d.getMinutes(), d.getSeconds()].join(":"); 20 | structuredData.push({ 21 | date: dformat, 22 | points: 0, 23 | name: user.username, 24 | }); 25 | 26 | d = new Date(props.endTime); 27 | dformat = 28 | [d.getMonth() + 1, d.getDate(), d.getFullYear()].join("/") + 29 | " " + 30 | [d.getHours(), d.getMinutes(), d.getSeconds()].join(":"); 31 | structuredData.push({ 32 | date: dformat, 33 | points: user.score, 34 | name: user.username, 35 | }); 36 | 37 | user.items = [ 38 | ...user.solved, 39 | ...user.hintsBought.map((x) => { 40 | return { points: -x.cost, timestamp: x.timestamp }; 41 | }), 42 | ]; 43 | 44 | user.items.sort((a, b) => a.timestamp - b.timestamp); 45 | user.items.forEach((item) => { 46 | currentPoints += item.points; 47 | d = new Date(item.timestamp); 48 | dformat = 49 | [d.getMonth() + 1, d.getDate(), d.getFullYear()].join("/") + 50 | " " + 51 | [d.getHours(), d.getMinutes(), d.getSeconds()].join(":"); 52 | structuredData.push({ 53 | date: dformat, 54 | points: currentPoints, 55 | name: user.username, 56 | }); 57 | }); 58 | }); 59 | 60 | structuredData.sort( 61 | (a, b) => new Date(a.date).getTime() - new Date(b.date).getTime() 62 | ); 63 | } else { 64 | rawData.forEach((team) => { 65 | let currentPoints = 0; 66 | let d = new Date(props.startTime); 67 | let dformat = 68 | [d.getMonth() + 1, d.getDate(), d.getFullYear()].join("/") + 69 | " " + 70 | [d.getHours(), d.getMinutes(), d.getSeconds()].join(":"); 71 | structuredData.push({ 72 | date: dformat, 73 | points: 0, 74 | name: team.name, 75 | }); 76 | 77 | d = new Date(props.endTime); 78 | dformat = 79 | [d.getMonth() + 1, d.getDate(), d.getFullYear()].join("/") + 80 | " " + 81 | [d.getHours(), d.getMinutes(), d.getSeconds()].join(":"); 82 | structuredData.push({ 83 | date: dformat, 84 | points: team.totalScore, 85 | name: team.name, 86 | }); 87 | 88 | team.items = [ 89 | ...team.solved, 90 | ...team.hintsBought.map((x) => { 91 | return { points: -x.cost, timestamp: x.timestamp }; 92 | }), 93 | ]; 94 | 95 | team.items.sort((a, b) => a.timestamp - b.timestamp); 96 | team.items.forEach((item) => { 97 | currentPoints += item.points; 98 | d = new Date(item.timestamp); 99 | dformat = 100 | [d.getMonth() + 1, d.getDate(), d.getFullYear()].join("/") + 101 | " " + 102 | [d.getHours(), d.getMinutes(), d.getSeconds()].join(":"); 103 | structuredData.push({ 104 | date: dformat, 105 | points: currentPoints, 106 | name: team.name, 107 | }); 108 | }); 109 | }); 110 | 111 | structuredData.sort( 112 | (a, b) => new Date(a.date).getTime() - new Date(b.date).getTime() 113 | ); 114 | } 115 | 116 | setData(structuredData); 117 | }, [props]); 118 | 119 | // { time: "1850", points: 54, name: "Test" } 120 | 121 | const config = { 122 | data, 123 | xField: "date", 124 | yField: "points", 125 | seriesField: "name", 126 | // point: { 127 | // shape: "circle", 128 | // }, 129 | smooth: true, 130 | theme: "dark", 131 | padding: 50, 132 | }; 133 | 134 | return ; 135 | }; 136 | 137 | export default LineChart; 138 | -------------------------------------------------------------------------------- /frontEnd/src/components/Charts/PieChart.jsx: -------------------------------------------------------------------------------- 1 | import { Pie } from "@ant-design/plots"; 2 | import { useState, useEffect, useCallback } from "react"; 3 | 4 | function PieChart(props) { 5 | const [data, setData] = useState(props.data); 6 | const [, updateState] = useState(); 7 | const forceUpdate = useCallback(() => updateState({}), []); 8 | 9 | useEffect(() => { 10 | setData(props.data); 11 | forceUpdate(); 12 | }, [props]); 13 | 14 | const config = { 15 | appendPadding: 10, 16 | data, 17 | angleField: "value", 18 | colorField: "name", 19 | radius: 0.9, 20 | label: { 21 | type: "inner", 22 | offset: "-30%", 23 | content: ({ percent }) => `${(percent * 100).toFixed(0)}%`, 24 | style: { 25 | fontSize: 14, 26 | textAlign: "center", 27 | }, 28 | }, 29 | interactions: [ 30 | { 31 | type: "element-active", 32 | }, 33 | ], 34 | }; 35 | 36 | return ( 37 |
38 | 39 |
40 | ); 41 | } 42 | 43 | export default PieChart; 44 | -------------------------------------------------------------------------------- /frontEnd/src/components/Data/AppContext.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const AppContext = React.createContext(); 4 | 5 | export default AppContext; -------------------------------------------------------------------------------- /frontEnd/src/components/FourOFour.jsx: -------------------------------------------------------------------------------- 1 | import { Outlet, Routes, Route, Link } from "react-router-dom"; 2 | import { useEffect, useContext } from "react"; 3 | import AppContext from './Data/AppContext'; 4 | import Navbar from './Global/Navbar'; 5 | 6 | function FourOFour(props) { 7 | const globalData = useContext(AppContext); 8 | return ( 9 |
10 | 11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | 19 | 20 | 21 |
22 |
23 |
24 |
25 |

29 | 404 N07 F0UND  30 |

31 |
32 |
33 |
34 |
35 |
36 | ); 37 | } 38 | 39 | export default FourOFour; 40 | -------------------------------------------------------------------------------- /frontEnd/src/components/Global/ConfirmModal.jsx: -------------------------------------------------------------------------------- 1 | function ConfirmModal(props) { 2 | 3 | return ( 4 | 45 | ); 46 | } 47 | 48 | export default ConfirmModal; 49 | -------------------------------------------------------------------------------- /frontEnd/src/components/Global/TradeMark.jsx: -------------------------------------------------------------------------------- 1 | import ctfCafeLogo from "../img/logo.png"; 2 | 3 | function TradeMark() { 4 | return ( 5 | 11 | ); 12 | } 13 | 14 | export default TradeMark; 15 | -------------------------------------------------------------------------------- /frontEnd/src/components/Index.jsx: -------------------------------------------------------------------------------- 1 | import { Outlet, Routes, Route, Link } from "react-router-dom"; 2 | import { useContext, useState } from "react"; 3 | import AppContext from "./Data/AppContext"; 4 | import Navbar from "./Global/Navbar"; 5 | 6 | function Index(props) { 7 | const globalData = useContext(AppContext); 8 | 9 | return ( 10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | 19 | 20 | 21 |
22 |
23 |
24 |
25 |

26 | {process.env.REACT_APP_CTF_NAME.toUpperCase()} 27 |   28 |

29 |

30 | {`${new Date(globalData.startTime).toDateString()} ${new Date( 31 | globalData.startTime 32 | ).getHours()}:${new Date( 33 | globalData.startTime 34 | ).getMinutes()} - ${new Date( 35 | globalData.endTime 36 | ).toDateString()} ${new Date( 37 | globalData.endTime 38 | ).getHours()}:${new Date(globalData.endTime).getMinutes()}`} 39 |

40 |
41 |
42 |
43 |
44 | {globalData.loggedIn ? ( 45 | { 49 | if (element) 50 | element.style.setProperty( 51 | "padding-right", 52 | "14px", 53 | "important" 54 | ); 55 | if (element) 56 | element.style.setProperty( 57 | "padding-left", 58 | "14px", 59 | "important" 60 | ); 61 | }} 62 | > 63 |

Challenges

64 | 65 | ) : ( 66 | { 70 | if (element) 71 | element.style.setProperty( 72 | "padding-right", 73 | "52px", 74 | "important" 75 | ); 76 | if (element) 77 | element.style.setProperty( 78 | "padding-left", 79 | "52px", 80 | "important" 81 | ); 82 | }} 83 | > 84 |

Login

85 | 86 | )} 87 |
88 | {globalData.socialLinks.map((social) => 89 | 97 | 101 | 102 | )} 103 |
104 |
105 | 106 |
107 |
111 | {globalData.sponsors.length > 0 ? ( 112 | <> 113 |

SPONSORS

114 | {globalData.sponsors.map((sponsor) => { 115 | return ( 116 | sponsor 126 | ); 127 | })} 128 | 129 | ) : null} 130 |
131 |
132 |
133 |
134 |
135 | ); 136 | } 137 | 138 | export default Index; 139 | -------------------------------------------------------------------------------- /frontEnd/src/components/Login.jsx: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { useContext } from "react"; 3 | import AppContext from "./Data/AppContext"; 4 | import Navbar from "./Global/Navbar"; 5 | 6 | function Login(props) { 7 | const globalData = useContext(AppContext); 8 | 9 | const checkLogin = () => { 10 | const username = document.getElementById("username").value; 11 | const password = document.getElementById("password").value; 12 | 13 | axios 14 | .post( 15 | process.env.REACT_APP_BACKEND_URI + "/api/login", 16 | { 17 | username: username, 18 | password: password, 19 | }, 20 | { withCredentials: true } 21 | ) 22 | .then((response) => { 23 | if (response.data.state == "success") { 24 | // Logged in, now get all needed data from session 25 | axios 26 | .get(process.env.REACT_APP_BACKEND_URI + "/api/checkSession", { 27 | withCredentials: true, 28 | }) 29 | .then((res) => { 30 | if (res.data.state == "success") { 31 | globalData.alert.success("Logged In!"); 32 | 33 | if (res.data.team) { 34 | const clubArray = (arr) => { 35 | return arr.reduce((acc, val, ind) => { 36 | const index = acc.findIndex( 37 | (el) => el.username === val.username 38 | ); 39 | if (index !== -1) { 40 | acc[index].solved.push(val.solved[0]); 41 | acc[index].score += val.score; 42 | } else { 43 | acc.push(val); 44 | } 45 | return acc; 46 | }, []); 47 | }; 48 | 49 | res.data.team.users = clubArray(res.data.team.users); 50 | 51 | res.data.team.users.forEach((user) => { 52 | user.solved.forEach((solved) => { 53 | user.score += solved.points; 54 | }); 55 | }); 56 | 57 | res.data.user.team = res.data.team; 58 | } 59 | 60 | globalData.setUserData(res.data.user); 61 | globalData.setLoggedIn(true); 62 | globalData.navigate("/", { replace: true }); 63 | } else { 64 | globalData.alert.error(response.data.message); 65 | } 66 | }) 67 | .catch(console.log); 68 | } else { 69 | globalData.alert.error(response.data.message); 70 | } 71 | }) 72 | .catch((err) => { 73 | console.log(err.message); 74 | }); 75 | }; 76 | 77 | return ( 78 |
79 |
80 | 81 | 82 | 83 |
84 |
85 |
86 |
87 |

88 | {process.env.REACT_APP_CTF_NAME.toUpperCase()} 89 |   90 |

91 |

92 | Type your credentials to conquer the world 93 |

94 |
95 |
96 |
97 | 103 |
104 |
105 | 111 | 112 | Make sure nobody's behind you 113 | 114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 | 127 |
128 |
129 |
130 |
131 |
132 | ); 133 | } 134 | 135 | export default Login; 136 | -------------------------------------------------------------------------------- /frontEnd/src/components/Logout.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useContext } from "react"; 2 | import AppContext from "./Data/AppContext"; 3 | import axios from "axios"; 4 | 5 | function Logout(props) { 6 | const globalData = useContext(AppContext); 7 | useEffect(() => { 8 | axios.get(process.env.REACT_APP_BACKEND_URI + "/api/logout", { withCredentials: true }); 9 | globalData.alert.success("Logged Out!"); 10 | globalData.setUserData({}); 11 | globalData.setLoggedIn(false); 12 | globalData.navigate("/", { replace: true }); 13 | }, []); 14 | 15 | return ( 16 | <> 17 |

18 | LOGGING 19 | OUT... 20 |

21 | 22 | ); 23 | } 24 | 25 | export default Logout; 26 | -------------------------------------------------------------------------------- /frontEnd/src/components/Register.jsx: -------------------------------------------------------------------------------- 1 | import { Outlet, Routes, Route, Link } from "react-router-dom"; 2 | import axios from "axios"; 3 | import { useContext } from "react"; 4 | import AppContext from "./Data/AppContext"; 5 | import Navbar from "./Global/Navbar"; 6 | 7 | function Register(props) { 8 | const globalData = useContext(AppContext); 9 | const register = () => { 10 | const username = document.getElementById("username").value; 11 | const password = document.getElementById("password").value; 12 | const confirm_password = document.getElementById("confirm_password").value; 13 | const email = document.getElementById("email").value; 14 | const userCategory = document.getElementById("userCategory").value; 15 | 16 | if (password !== confirm_password) { 17 | globalData.alert.error("Passwords don't match!"); 18 | } else { 19 | axios 20 | .post( 21 | process.env.REACT_APP_BACKEND_URI + "/api/register", 22 | { 23 | username: username, 24 | password: password, 25 | email: email, 26 | userCategory: userCategory, 27 | }, 28 | { withCredentials: true } 29 | ) 30 | .then((response) => { 31 | if (response.data.state === "success") { 32 | globalData.alert.success(response.data.message); 33 | if (response.data.verified) { 34 | // Registered, now get all needed data from session if verified 35 | axios 36 | .get(process.env.REACT_APP_BACKEND_URI + "/api/checkSession", { 37 | withCredentials: true, 38 | }) 39 | .then((res) => { 40 | if (res.data.state === "success") { 41 | if (res.data.team) { 42 | const clubArray = (arr) => { 43 | return arr.reduce((acc, val, ind) => { 44 | const index = acc.findIndex( 45 | (el) => el.username === val.username 46 | ); 47 | if (index !== -1) { 48 | acc[index].solved.push(val.solved[0]); 49 | acc[index].score += val.score; 50 | } else { 51 | acc.push(val); 52 | } 53 | return acc; 54 | }, []); 55 | }; 56 | 57 | res.data.team.users = clubArray(res.data.team.users); 58 | 59 | res.data.team.users.forEach((user) => { 60 | user.solved.forEach((solved) => { 61 | user.score += solved.points; 62 | }); 63 | }); 64 | 65 | res.data.user.team = res.data.team; 66 | } 67 | 68 | globalData.setUserData(res.data.user); 69 | globalData.setLoggedIn(true); 70 | globalData.navigate("/", { replace: true }); 71 | } else { 72 | globalData.alert.error(response.data.message); 73 | } 74 | }) 75 | .catch(console.log); 76 | } 77 | } else { 78 | globalData.alert.error(response.data.message); 79 | } 80 | }) 81 | .catch((err) => { 82 | console.log(err.message); 83 | }); 84 | } 85 | }; 86 | 87 | return ( 88 |
89 |
90 | 91 | 92 | 93 |
97 |
98 |
99 |
100 |

101 | {process.env.REACT_APP_CTF_NAME.toUpperCase()} 102 |   103 |

104 |

105 | Join the worlds leading forces, and battle it out for the win! 106 |

107 |
108 |
109 |
110 | 116 |
117 |
118 | 124 |
125 |
126 | 132 |
133 |
134 | 140 | 141 | Make sure nobody's behind you 142 | 143 |
144 | {globalData.userCategories.length > 0 && ( 145 |
146 | 147 | 152 |
153 | )} 154 |
155 |
156 |
157 |
158 |
159 |
160 | 166 |
167 |
168 |
169 |
170 |
171 | ); 172 | } 173 | 174 | export default Register; 175 | -------------------------------------------------------------------------------- /frontEnd/src/components/Rules.jsx: -------------------------------------------------------------------------------- 1 | import { Outlet, Routes, Route, Link } from "react-router-dom"; 2 | import { useContext, useState, useEffect } from "react"; 3 | import AppContext from "./Data/AppContext"; 4 | import Navbar from "./Global/Navbar"; 5 | import axios from "axios"; 6 | 7 | function Rules(props) { 8 | const globalData = useContext(AppContext); 9 | 10 | return ( 11 |
12 |
13 | 14 | 15 | 16 |
17 |
18 |
19 |
20 |

21 | CTFRULES 22 |

23 |

24 | A community of like minded individuals who support cybersecurity 25 | and Anonymous. 26 |

27 |
28 |
29 |
    30 | {globalData.rules.map((rule) => { 31 | return ( 32 |
  • 33 |

    34 | {rule.text} 35 | {rule.link ? ( 36 | <> 37 |
    38 | 39 | {rule.linkText} 40 | 41 | 42 | ) : null} 43 |

    44 |
  • 45 | ); 46 | })} 47 |
48 |
49 |
50 | {globalData.loggedIn ? ( 51 | 52 | 55 | 56 | ) : ( 57 | 58 | 61 | 62 | )} 63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 | ); 73 | } 74 | 75 | export default Rules; 76 | -------------------------------------------------------------------------------- /frontEnd/src/components/User.jsx: -------------------------------------------------------------------------------- 1 | import { useLocation } from "react-router-dom"; 2 | import axios from "axios"; 3 | import { useEffect, useState, useContext } from "react"; 4 | import Navbar from "./Global/Navbar"; 5 | import PieChart from "./Charts/PieChart"; 6 | import AppContext from "./Data/AppContext"; 7 | 8 | function User(props) { 9 | const globalData = useContext(AppContext); 10 | const location = useLocation(); 11 | const selectedUser = decodeURIComponent( 12 | location.pathname.replace("/user/", "") 13 | ); 14 | const [user, setUser] = useState({}); 15 | const [challengeStatsTags, setChallengeStatsTags] = useState([]); 16 | const [challengeStatsDifficulty, setChallengeStatsDifficulty] = useState([]); 17 | 18 | const getUser = (username) => { 19 | axios 20 | .post( 21 | process.env.REACT_APP_BACKEND_URI + "/api/getUser", 22 | { 23 | username: username, 24 | }, 25 | { 26 | withCredentials: true, 27 | } 28 | ) 29 | .then((response) => { 30 | if (response.data.state !== "error") { 31 | setUser(response.data); 32 | 33 | if (response.data.solved.length > 0) { 34 | let finalDataTags = []; 35 | let finalDataDifficulty = []; 36 | 37 | response.data.solved.forEach((solve) => { 38 | let exists = finalDataTags.find((obj) => { 39 | return obj.name === solve.challenge.tags[0]; 40 | }); 41 | 42 | if (exists) { 43 | exists.value += 1; 44 | } else { 45 | finalDataTags.push({ 46 | name: solve.challenge.tags[0], 47 | value: 1, 48 | }); 49 | } 50 | 51 | setChallengeStatsTags(finalDataTags); 52 | 53 | let difficulty = finalDataDifficulty.find((obj) => { 54 | return ( 55 | obj.name === 56 | (solve.challenge.level === 3 57 | ? "Ninja" 58 | : solve.challenge.level === 2 59 | ? "Hard" 60 | : solve.challenge.level === 1 61 | ? "Medium" 62 | : "Easy") 63 | ); 64 | }); 65 | 66 | console.log(solve.challenge) 67 | 68 | if (difficulty) { 69 | finalDataDifficulty[ 70 | finalDataDifficulty.indexOf(difficulty) 71 | ].value += 1; 72 | } else { 73 | finalDataDifficulty.push({ 74 | name: 75 | solve.challenge.level === 3 76 | ? "Ninja" 77 | : solve.challenge.level === 2 78 | ? "Hard" 79 | : solve.challenge.level === 1 80 | ? "Medium" 81 | : "Easy", 82 | value: 1, 83 | }); 84 | } 85 | 86 | setChallengeStatsDifficulty(finalDataDifficulty); 87 | }); 88 | } 89 | } 90 | }) 91 | .catch((err) => { 92 | console.log(err.message); 93 | }); 94 | }; 95 | 96 | useEffect(() => { 97 | getUser(selectedUser); 98 | }, []); 99 | 100 | return ( 101 |
102 |
103 | 104 | 105 |
109 |
110 | {user.username ? ( 111 |
112 |

116 | {user.username.toUpperCase()} 117 |

118 |
119 |

120 | Score : {user.score}{" "} 121 | {user.adminPoints !== 0 && 122 | `(${ 123 | user.adminPoints > 0 124 | ? "+" + user.adminPoints 125 | : user.adminPoints 126 | })`} 127 |

128 |

Category : {user.category}

129 |
130 | {/* User Solve Stats */} 131 |
132 |
133 |
134 |

Solves by Tags

135 | 136 |
137 |
138 |
139 |
140 |

Solves by Difficulty

141 | 142 |
143 |
144 |
145 | {/* User Solves Table */} 146 |
147 |

User Solves

148 | 149 | 150 | 151 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | {user.solved.map((solve, index) => { 162 | return ( 163 | 164 | 167 | 180 | 185 | 203 | 204 | 205 | ); 206 | })} 207 | 208 |
152 | # 153 | Challenge NameChallenge PointsChallenge TagsTime Solved
165 | {index} 166 | 168 | {solve.challenge.firstBlood == user._id && ( 169 | 177 | )} 178 | {solve.challenge.name} 179 | 181 | {solve.challenge.points}{" "} 182 | {solve.challenge.firstBlood == user._id && 183 | `(+${solve.challenge.firstBloodPoints})`} 184 | 186 | {solve.challenge.tags.map((tag) => ( 187 | tag == x.name 195 | ) || { color: "black" } 196 | ).color, 197 | }} 198 | > 199 | {tag} 200 | 201 | ))} 202 | {solve.timestamp}
209 |
210 | {/* User Hints Bought */} 211 |
212 |

User Hints Bought

213 | 214 | 215 | 216 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | {user.hintsBought.map((hint, index) => { 226 | return ( 227 | 228 | 231 | 232 | 233 | 234 | 235 | ); 236 | })} 237 | 238 |
217 | # 218 | Challenge NameHint CostTime Bought
229 | {index} 230 | {hint.challName}-{hint.cost}{hint.timestamp}
239 |
240 |
241 | ) : ( 242 |
243 |

247 | USER NOT FOUND 248 |

249 |
250 | )} 251 |
252 |
253 |
254 | ); 255 | } 256 | 257 | export default User; 258 | -------------------------------------------------------------------------------- /frontEnd/src/components/UserSettings.jsx: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { useContext, useEffect, useState } from "react"; 3 | import AppContext from "./Data/AppContext"; 4 | import Navbar from "./Global/Navbar"; 5 | import PieChart from "./Charts/PieChart"; 6 | 7 | function User(props) { 8 | const globalData = useContext(AppContext); 9 | 10 | const updateUsername = () => { 11 | const newUsername = document.getElementById("newUsername").value; 12 | 13 | axios 14 | .post( 15 | process.env.REACT_APP_BACKEND_URI + "/api/user/updateUsername", 16 | { 17 | newUsername: newUsername, 18 | }, 19 | { withCredentials: true } 20 | ) 21 | .then((response) => { 22 | if (response.data.state == "sessionError") { 23 | globalData.alert.error("Session expired!"); 24 | globalData.setUserData({}); 25 | globalData.setLoggedIn(false); 26 | globalData.navigate("/", { replace: true }); 27 | } else if (response.data.state == "success") { 28 | globalData.alert.success("Username updated!"); 29 | globalData.navigate("/", { replace: true }); 30 | } else { 31 | globalData.alert.error(response.data.message); 32 | } 33 | }) 34 | .catch((err) => { 35 | console.log(err.message); 36 | }); 37 | }; 38 | 39 | const updatePassword = () => { 40 | const newPassword = document.getElementById("newPassword").value; 41 | const oldPassword = document.getElementById("oldPassword").value; 42 | 43 | axios 44 | .post( 45 | process.env.REACT_APP_BACKEND_URI + "/api/user/updatePassword", 46 | { 47 | newPassword: newPassword, 48 | oldPassword: oldPassword 49 | }, 50 | { withCredentials: true } 51 | ) 52 | .then((response) => { 53 | if (response.data.state == "sessionError") { 54 | globalData.alert.error("Session expired!"); 55 | globalData.setUserData({}); 56 | globalData.setLoggedIn(false); 57 | globalData.navigate("/", { replace: true }); 58 | } else if (response.data.state == "success") { 59 | globalData.alert.success("Password updated!"); 60 | globalData.setUserData({}); 61 | globalData.setLoggedIn(false); 62 | globalData.navigate("/", { replace: true }); 63 | } else { 64 | globalData.alert.error(response.data.message); 65 | } 66 | }) 67 | .catch((err) => { 68 | console.log(err.message); 69 | }); 70 | }; 71 | 72 | return ( 73 |
74 |
75 | 76 |
80 |
81 | {globalData.userData.username ? ( 82 |
83 | <> 84 |
85 |
86 |
87 |

Settings

88 |
89 | 100 | 107 |
108 | 109 |
110 | 122 | 134 | 141 |
142 |
143 |
144 |
145 |
146 |

User Info

147 |
    154 |
  • Username: {globalData.userData.username}
  • 155 | {globalData.userData.team ? ( 156 |
  • Team: {globalData.userData.team.name}
  • 157 | ) : null} 158 |
159 |
160 |
161 |
162 | 163 |
164 | ) : ( 165 |
166 |

170 | USER NOT FOUND 171 |

172 |
173 | )} 174 |
175 |
176 |
177 | ); 178 | } 179 | 180 | export default User; 181 | -------------------------------------------------------------------------------- /frontEnd/src/components/img/bronzeMask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CTF-Cafe/CTF_Cafe_platform/ffaa0b33eccac31d9374204c7dd71d28818619cc/frontEnd/src/components/img/bronzeMask.png -------------------------------------------------------------------------------- /frontEnd/src/components/img/goldMask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CTF-Cafe/CTF_Cafe_platform/ffaa0b33eccac31d9374204c7dd71d28818619cc/frontEnd/src/components/img/goldMask.png -------------------------------------------------------------------------------- /frontEnd/src/components/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CTF-Cafe/CTF_Cafe_platform/ffaa0b33eccac31d9374204c7dd71d28818619cc/frontEnd/src/components/img/logo.png -------------------------------------------------------------------------------- /frontEnd/src/components/img/silverMask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CTF-Cafe/CTF_Cafe_platform/ffaa0b33eccac31d9374204c7dd71d28818619cc/frontEnd/src/components/img/silverMask.png -------------------------------------------------------------------------------- /frontEnd/src/css/prism.css: -------------------------------------------------------------------------------- 1 | /* PrismJS 1.28.0 2 | https://prismjs.com/download.html#themes=prism-tomorrow&languages=markup+clike+javascript+bash+c+csharp+cpp+java+json+json5+markup-templating+perl+php+python+regex&plugins=line-numbers+autolinker+show-language+command-line+toolbar+match-braces */ 3 | code[class*=language-],pre[class*=language-]{color:#ccc;background:0 0;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#2d2d2d}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.block-comment,.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#999}.token.punctuation{color:#ccc}.token.attr-name,.token.deleted,.token.namespace,.token.tag{color:#e2777a}.token.function-name{color:#6196cc}.token.boolean,.token.function,.token.number{color:#f08d49}.token.class-name,.token.constant,.token.property,.token.symbol{color:#f8c555}.token.atrule,.token.builtin,.token.important,.token.keyword,.token.selector{color:#cc99cd}.token.attr-value,.token.char,.token.regex,.token.string,.token.variable{color:#7ec699}.token.entity,.token.operator,.token.url{color:#67cdcc}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.token.inserted{color:green} 4 | pre[class*=language-].line-numbers{position:relative;padding-left:3.8em;counter-reset:linenumber}pre[class*=language-].line-numbers>code{position:relative;white-space:inherit}.line-numbers .line-numbers-rows{position:absolute;pointer-events:none;top:0;font-size:100%;left:-3.8em;width:3em;letter-spacing:-1px;border-right:1px solid #999;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.line-numbers-rows>span{display:block;counter-increment:linenumber}.line-numbers-rows>span:before{content:counter(linenumber);color:#999;display:block;padding-right:.8em;text-align:right} 5 | .token a{color:inherit} 6 | div.code-toolbar{position:relative}div.code-toolbar>.toolbar{position:absolute;z-index:10;top:.3em;right:.2em;transition:opacity .3s ease-in-out;opacity:0}div.code-toolbar:hover>.toolbar{opacity:1}div.code-toolbar:focus-within>.toolbar{opacity:1}div.code-toolbar>.toolbar>.toolbar-item{display:inline-block}div.code-toolbar>.toolbar>.toolbar-item>a{cursor:pointer}div.code-toolbar>.toolbar>.toolbar-item>button{background:0 0;border:0;color:inherit;font:inherit;line-height:normal;overflow:visible;padding:0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none}div.code-toolbar>.toolbar>.toolbar-item>a,div.code-toolbar>.toolbar>.toolbar-item>button,div.code-toolbar>.toolbar>.toolbar-item>span{color:#bbb;font-size:.8em;padding:0 .5em;background:#f5f2f0;background:rgba(224,224,224,.2);box-shadow:0 2px 0 0 rgba(0,0,0,.2);border-radius:.5em}div.code-toolbar>.toolbar>.toolbar-item>a:focus,div.code-toolbar>.toolbar>.toolbar-item>a:hover,div.code-toolbar>.toolbar>.toolbar-item>button:focus,div.code-toolbar>.toolbar>.toolbar-item>button:hover,div.code-toolbar>.toolbar>.toolbar-item>span:focus,div.code-toolbar>.toolbar>.toolbar-item>span:hover{color:inherit;text-decoration:none} 7 | .command-line-prompt{border-right:1px solid #999;display:block;float:left;font-size:100%;letter-spacing:-1px;margin-right:1em;pointer-events:none;text-align:right;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.command-line-prompt>span:before{opacity:.7;content:' ';display:block;padding-right:.8em}.command-line-prompt>span[data-user]:before{content:"[" attr(data-user) "@" attr(data-host) "] $"}.command-line-prompt>span[data-user=root]:before{content:"[" attr(data-user) "@" attr(data-host) "] #"}.command-line-prompt>span[data-prompt]:before{content:attr(data-prompt)}.command-line-prompt>span[data-continuation-prompt]:before{content:attr(data-continuation-prompt)}.command-line span.token.output{opacity:.7} 8 | .token.punctuation.brace-hover,.token.punctuation.brace-selected{outline:solid 1px}.rainbow-braces .token.punctuation.brace-level-1,.rainbow-braces .token.punctuation.brace-level-5,.rainbow-braces .token.punctuation.brace-level-9{color:#e50;opacity:1}.rainbow-braces .token.punctuation.brace-level-10,.rainbow-braces .token.punctuation.brace-level-2,.rainbow-braces .token.punctuation.brace-level-6{color:#0b3;opacity:1}.rainbow-braces .token.punctuation.brace-level-11,.rainbow-braces .token.punctuation.brace-level-3,.rainbow-braces .token.punctuation.brace-level-7{color:#26f;opacity:1}.rainbow-braces .token.punctuation.brace-level-12,.rainbow-braces .token.punctuation.brace-level-4,.rainbow-braces .token.punctuation.brace-level-8{color:#e0e;opacity:1} 9 | -------------------------------------------------------------------------------- /frontEnd/src/fonts/Doctor Glitch.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CTF-Cafe/CTF_Cafe_platform/ffaa0b33eccac31d9374204c7dd71d28818619cc/frontEnd/src/fonts/Doctor Glitch.otf -------------------------------------------------------------------------------- /frontEnd/src/fonts/PhelixBoomgartner.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CTF-Cafe/CTF_Cafe_platform/ffaa0b33eccac31d9374204c7dd71d28818619cc/frontEnd/src/fonts/PhelixBoomgartner.otf -------------------------------------------------------------------------------- /frontEnd/src/images/404.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CTF-Cafe/CTF_Cafe_platform/ffaa0b33eccac31d9374204c7dd71d28818619cc/frontEnd/src/images/404.gif -------------------------------------------------------------------------------- /frontEnd/src/images/bg--world.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CTF-Cafe/CTF_Cafe_platform/ffaa0b33eccac31d9374204c7dd71d28818619cc/frontEnd/src/images/bg--world.png -------------------------------------------------------------------------------- /frontEnd/src/images/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CTF-Cafe/CTF_Cafe_platform/ffaa0b33eccac31d9374204c7dd71d28818619cc/frontEnd/src/images/bg.jpg -------------------------------------------------------------------------------- /frontEnd/src/images/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CTF-Cafe/CTF_Cafe_platform/ffaa0b33eccac31d9374204c7dd71d28818619cc/frontEnd/src/images/bg.png -------------------------------------------------------------------------------- /frontEnd/src/images/bg2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CTF-Cafe/CTF_Cafe_platform/ffaa0b33eccac31d9374204c7dd71d28818619cc/frontEnd/src/images/bg2.png -------------------------------------------------------------------------------- /frontEnd/src/images/ng-background-dot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CTF-Cafe/CTF_Cafe_platform/ffaa0b33eccac31d9374204c7dd71d28818619cc/frontEnd/src/images/ng-background-dot.png -------------------------------------------------------------------------------- /frontEnd/src/index.jsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from "react-dom"; 2 | import App from "./App"; 3 | import { BrowserRouter } from "react-router-dom"; 4 | import { transitions, positions, Provider as AlertProvider } from "react-alert"; 5 | import AlertTemplate from "react-alert-template-basic"; 6 | 7 | // optional configuration 8 | const options = { 9 | // you can also just use 'bottom center' 10 | position: positions.BOTTOM_CENTER, 11 | timeout: 5000, 12 | offset: "10px", 13 | // you can also just use 'scale' 14 | transition: transitions.SCALE, 15 | }; 16 | 17 | ReactDOM.render( 18 | 19 | 20 | 21 | 22 | , 23 | document.getElementById('root') 24 | ); 25 | -------------------------------------------------------------------------------- /nginx_ctf.conf: -------------------------------------------------------------------------------- 1 | limit_conn_zone \$binary_remote_addr zone=addr:10m; 2 | limit_req_zone \$binary_remote_addr zone=high:10m rate=10r/m; 3 | limit_req_zone \$binary_remote_addr zone=low:10m rate=10r/s; 4 | 5 | server { 6 | listen 80; 7 | 8 | client_body_timeout 10s; 9 | client_header_timeout 10s; 10 | 11 | server_name YOUR_SERVER; 12 | 13 | location / { 14 | limit_conn addr 8; 15 | limit_req zone=low burst=20 nodelay; 16 | 17 | proxy_pass http://127.0.0.1:3000; 18 | proxy_http_version 1.1; 19 | proxy_set_header Upgrade \$http_upgrade; 20 | proxy_set_header Connection 'upgrade'; 21 | proxy_set_header Host \$host; 22 | proxy_cache_bypass \$http_upgrade; 23 | proxy_set_header X-Real-IP $remote_addr; 24 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 25 | proxy_set_header X-Forwarded-Proto $scheme; 26 | } 27 | 28 | location /api { 29 | limit_conn addr 8; 30 | 31 | location /api/assets/ { 32 | expires 12h; 33 | add_header Cache-Control public; 34 | proxy_pass http://127.0.0.1:3001; 35 | proxy_http_version 1.1; 36 | proxy_set_header Upgrade \$http_upgrade; 37 | proxy_set_header Connection 'upgrade'; 38 | proxy_set_header Host \$host; 39 | proxy_cache_bypass \$http_upgrade; 40 | proxy_set_header X-Real-IP $remote_addr; 41 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 42 | proxy_set_header X-Forwarded-Proto $scheme; 43 | } 44 | 45 | location /api/submitFlag { 46 | limit_req zone=high burst=5 nodelay; 47 | proxy_pass http://127.0.0.1:3001; 48 | proxy_set_header Upgrade \$http_upgrade; 49 | proxy_set_header Connection 'upgrade'; 50 | proxy_set_header Host \$host; 51 | proxy_cache_bypass \$http_upgrade; 52 | proxy_set_header X-Real-IP $remote_addr; 53 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 54 | proxy_set_header X-Forwarded-Proto $scheme; 55 | } 56 | 57 | 58 | location /api/login { 59 | limit_req zone=high burst=2 nodelay; 60 | proxy_pass http://127.0.0.1:3001; 61 | proxy_set_header Upgrade \$http_upgrade; 62 | proxy_set_header Connection 'upgrade'; 63 | proxy_set_header Host \$host; 64 | proxy_cache_bypass \$http_upgrade; 65 | proxy_set_header X-Real-IP $remote_addr; 66 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 67 | proxy_set_header X-Forwarded-Proto $scheme; 68 | } 69 | 70 | location /api/register { 71 | limit_req zone=high; 72 | proxy_pass http://127.0.0.1:3001; 73 | proxy_http_version 1.1; 74 | proxy_set_header Upgrade \$http_upgrade; 75 | proxy_set_header Connection 'upgrade'; 76 | proxy_set_header Host \$host; 77 | proxy_cache_bypass \$http_upgrade; 78 | proxy_set_header X-Real-IP $remote_addr; 79 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 80 | proxy_set_header X-Forwarded-Proto $scheme; 81 | } 82 | 83 | proxy_pass http://127.0.0.1:3001; 84 | proxy_http_version 1.1; 85 | proxy_set_header Upgrade \$http_upgrade; 86 | proxy_set_header Connection 'upgrade'; 87 | proxy_set_header Host \$host; 88 | proxy_cache_bypass \$http_upgrade; 89 | proxy_set_header X-Real-IP $remote_addr; 90 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 91 | proxy_set_header X-Forwarded-Proto $scheme; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /template.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CTF-Cafe/CTF_Cafe_platform/ffaa0b33eccac31d9374204c7dd71d28818619cc/template.jpg --------------------------------------------------------------------------------