├── .editorconfig ├── .env.example ├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ └── nodejs.yml ├── .gitignore ├── .mocharc.json ├── .prettierrc.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── apidoc.json ├── app ├── config.ts ├── constants │ └── index.ts ├── controllers │ ├── activity.controller.ts │ ├── authentication.controller.ts │ ├── badge.controller.ts │ ├── contact.controller.ts │ ├── email.controller.ts │ ├── error.controller.ts │ ├── game.controller.ts │ ├── gameApplication.controller.ts │ ├── health.controller.ts │ ├── leaderboard.controller.ts │ ├── linkedAccount.controller.ts │ ├── liveGame.controller.ts │ ├── newGame.controller.ts │ ├── search.controller.ts │ ├── user.controller.ts │ ├── userAvatar.controller.ts │ ├── userGameStats.controller.ts │ ├── userProfile.controller.ts │ └── userStats.controller.ts ├── index.ts ├── middleware │ ├── authentication.middleware.ts │ ├── gameApplication.middleware.ts │ └── user.middleware.ts ├── models │ ├── activity.model.ts │ ├── badge.model.ts │ ├── base.model.ts │ ├── emailOptIn.model.ts │ ├── emailVerification.model.ts │ ├── game.model.ts │ ├── gameApplication.model.ts │ ├── gameSource.model.ts │ ├── linkedAccount.model.ts │ ├── newGame.model.ts │ ├── passwordReset.model.ts │ ├── rank.model.ts │ ├── user.model.ts │ ├── userBadges.model.ts │ ├── userGameStats.model.ts │ ├── userProfile.model.ts │ └── userStats.model.ts ├── repository │ ├── activity.repository.ts │ ├── badge.repository.ts │ ├── emailOptIn.repository.ts │ ├── emailVerification.repository.ts │ ├── game.repository.ts │ ├── gameApplication.repository.ts │ ├── gameSource.repository.ts │ ├── linkedAccount.repository.ts │ ├── passwordReset.repository.ts │ ├── rank.repository.ts │ ├── user.repository.ts │ ├── userBadges.repository.ts │ ├── userGameStats.repository.ts │ └── userStatistics.repository.ts ├── request │ ├── archiveGameRequest.ts │ ├── endGameRequest.ts │ ├── loginRequest.ts │ ├── profileRequest.ts │ ├── registrationRequest.ts │ ├── requests.ts │ ├── updateEmailPermissionRequest.ts │ └── updateGameRequest.ts ├── routes │ ├── auth.routes.ts │ ├── badges.routes.ts │ ├── contact.routes.ts │ ├── docs.routes.ts │ ├── game.routes.ts │ ├── gameApplication.routes.ts │ ├── handlers │ │ └── index.ts │ ├── health.routes.ts │ ├── index.ts │ ├── leaderboard.routes.ts │ ├── linkedAccount.routes.ts │ ├── search.routes.ts │ ├── user.routes.ts │ └── validators │ │ ├── authentication.validator.ts │ │ ├── contact.validator.ts │ │ ├── email.validator.ts │ │ ├── game.validator.ts │ │ ├── index.ts │ │ ├── linkedAccount.validator.ts │ │ └── user.validator.ts ├── seeding │ ├── activity.seeding.ts │ ├── badge.seeding.ts │ ├── emailOptIn.seeding.ts │ ├── emailVerification.seeding.ts │ ├── game.seeding.ts │ ├── gameApplication.seeding.ts │ ├── index.ts │ ├── rank.seeding.ts │ ├── user.seeding.ts │ ├── userGameStats.seeding.ts │ ├── userProfile.seeding.ts │ └── userStats.seeding.ts ├── services │ ├── auth.service.ts │ ├── avatar.service.ts │ ├── badge.service.ts │ ├── connection.service.ts │ ├── discord.service.ts │ ├── mail.service.ts │ ├── pagination.service.ts │ ├── ranking.service.ts │ ├── reset.service.ts │ ├── server.service.ts │ ├── twitch.service.ts │ └── verification.service.ts ├── types │ ├── common.ts │ ├── game.ts │ ├── mailgun.js │ │ └── index.d.ts │ ├── newGame.ts │ └── process.d.ts └── utils │ ├── apiError.ts │ ├── hash.ts │ ├── helpers.ts │ └── logger.ts ├── cli ├── seed.emails.ts ├── seed.passwords.ts └── seeder.ts ├── package-lock.json ├── package.json ├── templates ├── connect-discord.mjml ├── contact-us.mjml ├── game-application-resign.mjml ├── game-application.mjml ├── layouts │ ├── footer.mjml │ ├── header.mjml │ ├── simple-footer.mjml │ └── unsubscribe.mjml ├── linked-account-disconnect.mjml ├── linked-account.mjml ├── new-contact.mjml ├── reset-password.mjml └── welcome.mjml ├── test ├── authentication.test.ts ├── contactUs.test.ts ├── email.test.ts ├── game.actions.test.ts ├── game.application.test.ts ├── game.player.test.ts ├── game.test.ts ├── health.test.ts ├── linkedAccount.test.ts ├── pagination.test.ts ├── search.test.ts ├── user.activity.test.ts ├── user.applications.test.ts ├── user.badges.test.ts ├── user.connections.test.ts ├── user.games.test.ts ├── user.profile.test.ts ├── user.statistics.test.ts └── user.test.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # 'Required' must be specified for local development. 2 | # 'Optional' will provide additional functionality during development and production. 3 | 4 | ################################# 5 | # Master Configuration Options # 6 | ################################# 7 | 8 | # NODE_ENV (Required) - Executing node enviroment. 9 | # development or production (case matters). 10 | NODE_ENV=development 11 | 12 | # PORT and HOST (Optional) - Port and host the server bind to 13 | # defaults to 0.0.0.0:8000. 14 | PORT=8000 15 | HOST=0.0.0.0 16 | 17 | # COOKIE_DOMAIN (OptionaL) - Leave this blank unless you have a good 18 | # understanding of a cookie domain and its functionality. Leaving blank 19 | # will allow local development without any constraints. 20 | COOKIE_DOMAIN= 21 | 22 | # CORS_ALLOWED_ORIGINS (Optional) - List of allowed origins split by whitespace. 23 | CORS_ORIGINS=http://localhost:3000 24 | 25 | # API_KEY (Optional) - Specified authentication token used for the 26 | # DevWars Twitch bot to communicate with the API. This can be left 27 | # as the default in local development. 28 | API_KEY=secret 29 | 30 | # DB_ (Required) - Postgres connection details that are used throughout 31 | # the API to store, read and distribute DevWars information and data. 32 | 33 | # TYPEORM_ (Required) - Database connection options. 34 | TYPEORM_HOST=127.0.0.1 35 | TYPEORM_PORT=5432 36 | TYPEORM_DATABASE=devwars 37 | TYPEORM_USERNAME=postgres 38 | TYPEORM_PASSWORD=postgres 39 | TYPEORM_SYNCHRONIZE=true 40 | TYPEORM_LOGGING=false 41 | 42 | # TYPEORM_TEST_ (Required) - Database options used when running tests 43 | TYPEORM_TEST_HOST=127.0.0.1 44 | TYPEORM_TEST_PORT=5432 45 | TYPEORM_TEST_DATABASE=devwars 46 | TYPEORM_TEST_USERNAME=postgres 47 | TYPEORM_TEST_PASSWORD=postgres 48 | TYPEORM_TEST_SYNCHRONIZE=true 49 | TYPEORM_TEST_LOGGING=false 50 | 51 | # URLs (Required) - The website and API URLs that are used in redirection, 52 | # links and emails. 53 | FRONT_URL=http://localhost:3000 54 | API_URL=http://localhost:8080 55 | 56 | # AUTH_SECRET (Required) - A base 64 converted list of cryptographic 57 | # generated list of bytes which are used for generating JWT tokens 58 | # during authentication. Can be left as default 'secret' during local 59 | # development. 60 | AUTH_SECRET=secret 61 | 62 | # LOG_LEVEL (Optional) - A specified logging level for what will be 63 | # written to the console and 'all.log' file. Defaults to 'info' if 64 | # not specified. 65 | # 66 | # Options: error, warn, info, verbose, debug, silly. 67 | LOG_LEVEL=info 68 | 69 | # DISCORD (Optional) - Discord variables can be left blank unless local 70 | # development would require making account connections to a discord account. 71 | # If required, a discord application client and secret would need to be specified. 72 | # More can be found here: https://discordapp.com/developers/applications/ 73 | DISCORD_CLIENT= 74 | DISCORD_SECRET= 75 | 76 | # Twitch (Optional) - Twitch variables can be left blank unless local 77 | # development would require making account connections to a twitch account. 78 | # If required, a twitch application client and secret would need to be specified. 79 | # More can be found here: https://dev.twitch.tv/console 80 | TWITCH_CLIENT= 81 | TWITCH_SECRET= 82 | 83 | # MAILGUN_KEY (Optional) - Api key used to connect to mail gun which is 84 | # used to distribute emails out to DevWars users. Is not required for 85 | # local development. 86 | MAILGUN_KEY= 87 | 88 | # AWS_ (Optional) - Connection variables used to connection to AWS for 89 | # uploading profile pictures/images. Typically can be left blank for 90 | # local development unless testing of the profile uploading process is 91 | # being done. 92 | AWS_ENDPOINT_URL= 93 | AWS_ACCESS_KEY= 94 | AWS_SECRET_KEY= 95 | AWS_BUCKET_NAME= 96 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | dist 4 | 5 | coverage 6 | 7 | docs 8 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-undef 2 | module.exports = { 3 | root: true, 4 | parser: '@typescript-eslint/parser', 5 | rules: { 6 | 'no-console': 0, 7 | quotes: [2, 'single', { avoidEscape: true }], 8 | 'max-len': [2, 120], 9 | 'sort-imports': 0, 10 | 'sort-keys': 0, 11 | curly: 0, 12 | 'no-bitwise': 0, 13 | '@typescript-eslint/no-explicit-any': 0, 14 | '@typescript-eslint/camelcase': 0, 15 | '@typescript-eslint/explicit-function-return-type': 0, 16 | '@typescript-eslint/ban-ts-ignore': 0, 17 | '@typescript-eslint/explicit-module-boundary-types': 0, 18 | }, 19 | plugins: [ 20 | '@typescript-eslint', 21 | // 'jest', 22 | ], 23 | extends: [ 24 | 'eslint:recommended', 25 | 'plugin:@typescript-eslint/eslint-recommended', 26 | 'plugin:@typescript-eslint/recommended', 27 | 'prettier', 28 | // 'plugin:jest/recommended' 29 | ], 30 | }; 31 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [12.x] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | - run: npm ci 28 | - run: npm run lint 29 | - run: npm run build --if-present 30 | # - run: npm test 31 | env: 32 | CI: true 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | dist 4 | 5 | .nyc_output 6 | coverage/ 7 | 8 | .idea 9 | 10 | *.sqlite 11 | 12 | docs 13 | 14 | *.iml 15 | 16 | .env 17 | .env.* 18 | 19 | .vscode 20 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extension": [ 3 | "ts" 4 | ], 5 | "spec": "test/**/*.test.ts", 6 | "transpileOnly": true, 7 | "require": "ts-node/register", 8 | "exit": true 9 | } 10 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 4, 4 | "singleQuote": true, 5 | "arrowParens": "always", 6 | "printWidth": 120 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 DevWars, LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |
3 | DEV 4 |

DevWars API

5 | The Human Layer of the Stack 6 |
7 |
8 |

9 | 10 | nodejs version 11 | 12 | 13 | expressjs version 14 | 15 | 16 | typescript version 17 | 18 | 19 | typeorm version 20 | 21 | 22 | postgres version 23 | 24 | Dependabot Badge 25 |

26 | 27 | Welcome to the [DevWars](https://DevWars.tv) API codebase. This is the core backbone and interface for the day to day running of the DevWars platform. 28 | 29 | ## What is DevWars? 30 | 31 | [DevWars.tv](https://www.devwars.tv/) is a live game show for developers that is currently streamed on [Twitch](https://www.twitch.tv/devwars). People of all levels participate in an exhilarating battle to create the best website they can within 60 minutes. Teams are formed of 3 people, with the team's members each controlling a single language - HTML, CSS and JavaScript. 32 | 33 | ## Getting Started 34 | 35 | ### Prerequisites 36 | 37 | - [Nodejs](https://nodejs.org/en/): 10.0 or higher 38 | - [PostgreSQL](https://www.postgresql.org/): 9.4 or higher. 39 | 40 | ### Dependency Installation 41 | 42 | Run `npm install` to install dependent node_modules. 43 | 44 | ### Environment Variables 45 | 46 | Make a copy of the `.env.example` file in the same directory and rename the given file to `.env`. This will be loaded up into the application when it first starts running. These are required configuration settings to ensure correct function. Process through the newly created file and make the required changes if needed. 47 | 48 | ### Seeding The Database 49 | 50 | Once you have everything setup in the environment variables folder, you will be able to seed the database. This process will generate fake data that will allow testing and usage of the API. 51 | 52 | run `npm run seed` 53 | 54 | ### Testing 55 | 56 | Running `npm run test` will start the testing process, using the second set of connection details within the `.env` file. This process is enforced as a git hook before committing code. 57 | 58 | ### Development 59 | 60 | Running `npm run dev` will start a development server that will automatically restart when changes occur. Additionally running `npm run dev:break` will allow development with the inspector attached. 61 | 62 | ## Contributors 63 | 64 | This project exists thanks to all the people who [contribute](https://github.com/DevWars/devwars-api/graphs/contributors). We encourage you to contribute to DevWars but ensure to open a related issue first. Please check out the [contributing](CONTRIBUTING.md) to DevWars guide for guidelines about how to proceed. 65 | 66 | ## License 67 | 68 | > You can check out the full license [here](https://github.com/DevWars/devwars-api/blob/master/LICENSE) 69 | 70 | This project is licensed under the terms of the **MIT** license. 71 | -------------------------------------------------------------------------------- /apidoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "DevWars", 3 | "version": "1.0.0", 4 | "description": "Official Documentation for DevWars API", 5 | "apidoc": { 6 | "title": "DevWars API Docs", 7 | "url" : "https://github.com/DevWars/devwars-api" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /app/config.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as dotenv from 'dotenv'; 3 | import * as AWS from 'aws-sdk'; 4 | import { ConnectionOptions } from 'typeorm'; 5 | 6 | dotenv.config(); 7 | 8 | const booleanEnv = (value?: string): boolean => { 9 | return value?.toLowerCase() === 'true' ?? false; 10 | } 11 | 12 | const { 13 | NODE_ENV = 'development', 14 | PORT = '8000', 15 | HOST = '0.0.0.0', 16 | CORS_ORIGINS = '', 17 | AWS_ACCESS_KEY, 18 | AWS_SECRET_KEY, 19 | }: { [key: string]: string | undefined } = process.env; 20 | 21 | // Prefix for TypeORM environment variables. 22 | // TODO: Test environment should use a separate .env.test file intead. 23 | const TYPEORM = NODE_ENV === 'test' ? 'TYPEORM_TEST_' : 'TYPEORM_'; 24 | 25 | const { 26 | [TYPEORM + 'HOST']: TYPEORM_HOST, 27 | [TYPEORM + 'PORT']: TYPEORM_PORT, 28 | [TYPEORM + 'DATABASE']: TYPEORM_DATABASE, 29 | [TYPEORM + 'USERNAME']: TYPEORM_USERNAME, 30 | [TYPEORM + 'PASSWORD']: TYPEORM_PASSWORD, 31 | [TYPEORM + 'SYNCHRONIZE']: TYPEORM_SYNCHRONIZE = 'true', 32 | [TYPEORM + 'LOGGING']: TYPEORM_LOGGING = 'false', 33 | }: { [key: string]: string | undefined } = process.env; 34 | 35 | const databaseOptions: ConnectionOptions = { 36 | type: 'postgres', 37 | database: TYPEORM_DATABASE, 38 | host: TYPEORM_HOST, 39 | port: Number(TYPEORM_PORT), 40 | username: TYPEORM_USERNAME, 41 | password: TYPEORM_PASSWORD, 42 | synchronize: booleanEnv(TYPEORM_SYNCHRONIZE), 43 | logging: booleanEnv(TYPEORM_LOGGING), 44 | 45 | entities: [path.join(__dirname, 'models/*{.ts,.js}')], 46 | }; 47 | 48 | const config = { 49 | env: NODE_ENV, 50 | port: Number(PORT), 51 | host: HOST, 52 | databaseOptions, 53 | cors: { 54 | credentials: true, 55 | origin: CORS_ORIGINS.split(' '), 56 | }, 57 | }; 58 | 59 | AWS.config.update({ 60 | accessKeyId: AWS_ACCESS_KEY, 61 | secretAccessKey: AWS_SECRET_KEY, 62 | }); 63 | 64 | export { config }; 65 | -------------------------------------------------------------------------------- /app/constants/index.ts: -------------------------------------------------------------------------------- 1 | // The reserved usernames that cannot be taken by any newly created accounts. 2 | export const RESERVED_USERNAMES = ['admin', 'devwars', 'administrator', 'administration', 'competitor', 'eval']; 3 | 4 | // The minimum and maximum length a new/existing users username must respect for 5 | // the user to be registered or authorized with the site. 6 | export const USERNAME_MIN_LENGTH = 4; 7 | export const USERNAME_MAX_LENGTH = 25; 8 | 9 | // The minimum number of days required for someone to wait to change there 10 | // username again after the first change. 11 | export const USERNAME_CHANGE_MIN_DAYS = 7; 12 | 13 | // Does not allow special characters in the beginning or end of username 14 | export const USERNAME_REGEX = /^[a-zA-Z0-9.-][A-z0-9.-_]{2,23}[a-zA-Z0-9.-]$/; 15 | 16 | // currently postgres max int is 4 bytes and any attempt to go over this limit should be rejected 17 | // http://www.postgresqltutorial.com/postgresql-integer/ 18 | export const DATABASE_MAX_ID = 2 ** 31 - 1; 19 | 20 | // Username limits in regards for twitch when processing requests that is directly related to the 21 | // twitch username. e.g updating twitch coins. 22 | export const TWITCH_USERNAME_MIN_LENGTH = 4; 23 | export const TWITCH_USERNAME_MAX_LENGTH = 25; 24 | 25 | // The minimum and maximum length a new/existing users password must respect for 26 | // the user to be registered or authorized with the site. 27 | export const PASSWORD_MIN_LENGTH = 6; 28 | export const PASSWORD_MAX_LENGTH = 128; 29 | 30 | // The minimum and maximum number of coins that a user can have assigned during the statistics 31 | // generation/creation. 32 | export const STATS_COINS_MIN_AMOUNT = 0; 33 | export const STATS_COINS_MAX_AMOUNT = Infinity; 34 | 35 | // The minimum and maximum xp that a user can have at anyone time during the statistics 36 | // generation/creation. 37 | export const STATS_XP_MIN_AMOUNT = 0; 38 | export const STATS_XP_MAX_AMOUNT = Infinity; 39 | 40 | // The minimum and maximum level that a user can have at anyone time during the statistics 41 | // generation/creation. 42 | export const STATS_LEVEL_MIN_AMOUNT = 0; 43 | export const STATS_LEVEL_MAX_AMOUNT = Infinity; 44 | 45 | // Upper and lower limits of adding and updating twitch coins on a given twitch users account. The 46 | // limits are high but ensuring limits removes the chance of something going wrong with int32 max, 47 | // etc. 48 | export const TWITCH_COINS_MIN_UPDATE = -1000000; 49 | export const TWITCH_COINS_MAX_UPDATE = 1000000; 50 | 51 | // Game creation title min and max lengths. 52 | export const GAME_TITLE_MIN_LENGTH = 5; 53 | export const GAME_TITLE_MAX_LENGTH = 124; 54 | 55 | // The minimum number the game season can currently be in. 56 | export const GAME_SEASON_MIN = 1; 57 | 58 | // The minimal and max title length when creating a new game schedule. 59 | export const GAME_SCHEDULE_TITLE_MIN_LENGTH = 5; 60 | export const GAME_SCHEDULE_TITLE_MAX_LENGTH = 124; 61 | 62 | // The min and max length of a given description of a objective of the game. 63 | export const GAME_SCHEDULE_OBJECTIVE_DESCRIPTION_MIN_LENGTH = 5; 64 | export const GAME_SCHEDULE_OBJECTIVE_DESCRIPTION_MAX_LENGTH = 124; 65 | 66 | // The min and max length of the contact us name length 67 | export const CONTACT_US_NAME_MIN = 3; 68 | export const CONTACT_US_NAME_MAX = 64; 69 | 70 | // the min and max length of the contact message 71 | export const CONTACT_US_MESSAGE_MIN = 24; 72 | export const CONTACT_US_MESSAGE_MAX = 500; 73 | 74 | // XP - The experience that can be earned for different actions. 75 | export const EXPERIENCE = { 76 | // The total amount fo experience earned for participating within a game or 77 | // event within devwars. 78 | PARTICIPATION: 800, 79 | // The total amount of experience gained for winning a game within devwars. 80 | GAME_WIN: 4000, 81 | // The total amount of experience lost for losing a game within devwars. 82 | GAME_LOST: -2400, 83 | // The total amount of experience gained for getting all objectives within 84 | // any given event or game that is held within devwars. 85 | ALL_OBJECTIVES: 2400, 86 | // The total amount of experience lost when forfeiting a game or event 87 | // within devwars. 88 | FORFEIT: -8000, 89 | // The total amount of experience gained for being the best answer for a 90 | // form for question asked to the community. 91 | BEST_FORM_ANSWER: 800, 92 | }; 93 | 94 | // Id's of all the badges that can be awarded currently in devwars. 95 | export const BADGES = { 96 | EMAIL_VERIFICATION: 1, 97 | SINGLE_SOCIAL_ACCOUNT: 2, 98 | ALL_SOCIAL_ACCOUNT: 3, 99 | DEVWARS_COINS_5000: 4, 100 | DEVWARS_COINS_25000: 5, 101 | BETTING_EARN_10000: 6, 102 | SUBMIT_IDEA_GET_IMPLEMENTED: 7, 103 | FIND_BUG_AND_REPORT: 8, 104 | REFERRAL_5_PEOPLE: 9, 105 | REFERRAL_25_PEOPLE: 10, 106 | REFERRAL_50_PEOPLE: 11, 107 | COMPLETE_ALL_OBJECTIVES: 12, 108 | WATCH_FIRST_GAME: 13, 109 | WATCH_5_GAMES: 14, 110 | WATCH_25_GAMES: 15, 111 | WATCH_50_GAMES: 16, 112 | WIN_FIRST_GAME: 17, 113 | WIN_5_GAMES: 18, 114 | WIN_10_GAMES: 19, 115 | WIN_25_GAMES: 20, 116 | WIN_3_IN_ROW: 21, 117 | ANSWER_TWITCH_QUIZ_QUESTION: 22, 118 | ANSWER_10_TWITCH_QUIZ_QUESTION: 23, 119 | BET_ALL_DEV_COINS_AND_WIN: 24, 120 | VISIT_ON_BIRTHDAY: 25, 121 | COMPLETE_POLL: 26, 122 | COMPLETE_25_POLL: 27, 123 | BUY_FROM_STORE: 28, 124 | }; 125 | -------------------------------------------------------------------------------- /app/controllers/activity.controller.ts: -------------------------------------------------------------------------------- 1 | import { Response } from 'express'; 2 | import { getCustomRepository } from 'typeorm'; 3 | import * as _ from 'lodash'; 4 | 5 | import ActivityRepository from '../repository/activity.repository'; 6 | 7 | import { parseIntWithDefault } from '../utils/helpers'; 8 | import { UserRequest } from '../request/requests'; 9 | import { DATABASE_MAX_ID } from '../constants'; 10 | import ApiError from '../utils/apiError'; 11 | 12 | /****************************** 13 | * Activities 14 | ******************************/ 15 | 16 | /** 17 | * @api {get} users/:user/activities Get all users activities 18 | * @apiName GetUsersActivities 19 | * @apiGroup Activity 20 | * @apiPermission moderator, owner 21 | * 22 | * @apiSuccess {Activity[]} Activities The users activities. 23 | * 24 | * @apiSuccess {Date} activity.createdAt Time created 25 | * @apiSuccess {Date} activity.updatedAt Time updated 26 | * @apiSuccess {String} activity.description Description of activity 27 | * @apiSuccess {Number} activity.coins Amount of coins rewarded 28 | * @apiSuccess {Number} activity.xp Amount of XP rewarded 29 | * @apiSuccess {Number} activity.userId User ID activity belongs to 30 | * 31 | * @apiSuccessExample Success-Response: 32 | * HTTP/1.1 200 OK 33 | * [ 34 | * { 35 | * "id": 1, 36 | * "createdAt": "2018-10-21T21:45:45.000Z", 37 | * "updatedAt": "2018-10-21T21:45:45.000Z", 38 | * "description": "You validated your email" 39 | * "coins": 100, 40 | * "xp": 0, 41 | * "userId": 1 42 | * }, 43 | * { 44 | * "id": 2, 45 | * "createdAt": "2018-10-21T21:45:45.000Z", 46 | * "updatedAt": "2018-10-21T21:45:45.000Z", 47 | * "description": "Connected your Twitch account" 48 | * "coins": 500, 49 | * "xp": 10, 50 | * "userId": 1 51 | * } 52 | * ] 53 | */ 54 | export async function gatherAllUsersActivitiesById(request: UserRequest, response: Response) { 55 | const activityRepository = getCustomRepository(ActivityRepository); 56 | const activities = await activityRepository.find({ user: request.boundUser }); 57 | 58 | return response.json(activities); 59 | } 60 | 61 | /** 62 | * @api {get} users/:user/activities/:activity Get a activity for a user. 63 | * @apiName GetUsersActivityById 64 | * @apiGroup Activity 65 | * @apiPermission moderator, owner 66 | * 67 | * @apiSuccess {Activity} Activity The users activity. 68 | * 69 | * @apiSuccess {Date} createdAt Time created 70 | * @apiSuccess {Date} updatedAt Time updated 71 | * @apiSuccess {String} description Description of activity 72 | * @apiSuccess {Number} coins Amount of coins rewarded 73 | * @apiSuccess {Number} xp Amount of XP rewarded 74 | * @apiSuccess {Number} userId User ID activity belongs to 75 | * 76 | * @apiSuccessExample Success-Response: 77 | * HTTP/1.1 200 OK 78 | * { 79 | * "id": 1, 80 | * "createdAt": "2018-10-21T21:45:45.000Z", 81 | * "updatedAt": "2018-10-21T21:45:45.000Z", 82 | * "description": "You validated your email" 83 | * "coins": 100, 84 | * "xp": 0, 85 | * "userId": 1 86 | * } 87 | */ 88 | export async function gatherUserActivityById(request: UserRequest, response: Response) { 89 | const activityId = parseIntWithDefault(request.params.activity, null, 1, DATABASE_MAX_ID); 90 | 91 | if (_.isNil(activityId)) { 92 | throw new ApiError({ 93 | message: 'Invalid activity id was provided.', 94 | code: 400, 95 | }); 96 | } 97 | 98 | const activityRepository = getCustomRepository(ActivityRepository); 99 | 100 | const activity = await activityRepository.findOne({ user: request.boundUser, id: activityId }); 101 | if (_.isNil(activity)) { 102 | throw new ApiError({ 103 | message: 'The activity does not exist by the provided id.', 104 | code: 404, 105 | }); 106 | } 107 | 108 | return response.json(activity); 109 | } 110 | -------------------------------------------------------------------------------- /app/controllers/badge.controller.ts: -------------------------------------------------------------------------------- 1 | import { getCustomRepository } from 'typeorm'; 2 | import { Request, Response } from 'express'; 3 | 4 | import UserBadgesRepository from '../repository/userBadges.repository'; 5 | import BadgeRepository from '../repository/badge.repository'; 6 | import { UserRequest } from '../request/requests'; 7 | 8 | /** 9 | * @api {get} users/:user/badges Get the users assigned badges. 10 | * @apiName GetUserAssignedBadges 11 | * @apiGroup Badges 12 | * @apiPermission moderator, owner 13 | * 14 | * @apiSuccess {string} name The name of the badge. 15 | * @apiSuccess {string} description The description of the badge. 16 | * @apiSuccess {number} awardingExperience The awarded experience from the badge. 17 | * @apiSuccess {number} awardingCoins The amount of coins awarded. 18 | * @apiSuccess {number} variant The id of the variant of the badge. 19 | * @apiSuccess {number} id The id of the badge. 20 | * @apiSuccess {Date} createdAt Time created 21 | * @apiSuccess {Date} updatedAt Time updated 22 | * 23 | * @apiSuccessExample Success-Response: 24 | * HTTP/1.1 200 OK 25 | * [ 26 | * { 27 | * "name": "Authentic", 28 | * "description": "Verify your e-mail address", 29 | * "awardingExperience": 0, 30 | * "awardingCoins": 500, 31 | * "variant": 0, 32 | * "id": 1, 33 | * "updatedAt": "2020-10-18T10:34:40.046Z", 34 | * "createdAt": "2020-10-18T10:34:40.046Z" 35 | * } 36 | * ] 37 | */ 38 | export async function gatherUserBadgeById(request: UserRequest, response: Response) { 39 | const userBadgesRepository = getCustomRepository(UserBadgesRepository); 40 | 41 | const badges = await userBadgesRepository.find({ where: { user: request.boundUser }, relations: ['badge'] }); 42 | const usersBadges = badges.map((e) => e.badge); 43 | 44 | return response.json(usersBadges); 45 | } 46 | 47 | /** 48 | * @api {get} /badges Get the current list of possible badges. 49 | * @apiName GetUserPossibleBadges 50 | * @apiGroup Badges 51 | * 52 | * @apiSuccess {string} name The name of the badge. 53 | * @apiSuccess {string} description The description of the badge. 54 | * @apiSuccess {number} awardingExperience The awarded experience from the badge. 55 | * @apiSuccess {number} awardingCoins The amount of coins awarded. 56 | * @apiSuccess {number} variant The id of the variant of the badge. 57 | * @apiSuccess {number} id The id of the badge. 58 | * @apiSuccess {Date} createdAt Time created 59 | * @apiSuccess {Date} updatedAt Time updated 60 | * 61 | * @apiSuccessExample Success-Response: 62 | * HTTP/1.1 200 OK 63 | * [ 64 | * { 65 | * "name": "Authentic", 66 | * "description": "Verify your e-mail address", 67 | * "awardingExperience": 0, 68 | * "awardingCoins": 500, 69 | * "variant": 0, 70 | * "id": 1, 71 | * "updatedAt": "2020-10-18T10:34:40.046Z", 72 | * "createdAt": "2020-10-18T10:34:40.046Z" 73 | * } 74 | * ] 75 | */ 76 | export async function getAllCurrentBadges(request: Request, response: Response) { 77 | const badgeRepository = getCustomRepository(BadgeRepository); 78 | 79 | const implementedBadges = [1, 2, 4, 5, 17, 18, 19, 20, 21]; 80 | 81 | // Since only some of the badges have been implemented, only gather the once 82 | // that have been and ignore the rest, this list will grow over time. 83 | const badges = await badgeRepository.find(); 84 | 85 | badges.forEach((badge: any) => { 86 | badge.implemented = implementedBadges.includes(badge.id); 87 | }) 88 | 89 | return response.json(badges); 90 | } 91 | -------------------------------------------------------------------------------- /app/controllers/contact.controller.ts: -------------------------------------------------------------------------------- 1 | import { Response } from 'express'; 2 | import { ContactRequest } from '../request/requests'; 3 | import { sendContactUsEmail } from '../services/mail.service'; 4 | 5 | /** 6 | * @api {post} /contact Endpoint for contact us forms to be posted too. 7 | * @apiVersion 1.0.0 8 | * @apiName ContactUs 9 | * @apiGroup Contact 10 | * 11 | * @apiParam {string} name The name of the requesting contact us person. 12 | * @apiParam {string} email The email of the contact us person who will be getting the reply. 13 | * @apiParam {string} message The message in the contact us request. 14 | * 15 | * * @apiParamExample {json} Request-Example: 16 | * { 17 | * "name": "John Doe", 18 | * "email": "example@example.com", 19 | * "message": "Hi, I was wondering if you could help me... " 20 | * } 21 | */ 22 | export async function handleContactPost(request: ContactRequest, response: Response) { 23 | const { name, email, message } = request.body; 24 | 25 | await sendContactUsEmail(name, email, message); 26 | return response.send(); 27 | } 28 | -------------------------------------------------------------------------------- /app/controllers/email.controller.ts: -------------------------------------------------------------------------------- 1 | import { Response } from 'express'; 2 | import { getCustomRepository } from 'typeorm'; 3 | import { isNil } from 'lodash'; 4 | 5 | import { UpdateEmailPermissionRequest } from '../request/updateEmailPermissionRequest'; 6 | import { UserRequest } from '../request/requests'; 7 | import EmailRepository from '../repository/emailOptIn.repository'; 8 | import EmailOptIn from '../models/emailOptIn.model'; 9 | import ApiError from '../utils/apiError'; 10 | 11 | /** 12 | * @api {get} /users/:user/emails/permissions Gather user related email permissions 13 | * @apiVersion 1.0.0 14 | * @apiName GatherUserRelatedEmailPermissions 15 | * @apiGroup Emails 16 | * 17 | * @apiParam {number} user The id of the user. 18 | * 19 | * @apiSuccess {EmailOptIn} Permissions The current email permissions for the given user. 20 | * @apiSuccessExample Success-Response: HTTP/1.1 200 OK 21 | * { 22 | * "news": true, 23 | * "gameApplications": true, 24 | * "schedules": true, 25 | * "linkedAccounts": true, 26 | * "id": 1, 27 | * "updatedAt": "2019-11-23T17:30:25.884Z", 28 | * "createdAt": "2019-11-23T17:02:58.886Z" 29 | * } 30 | * 31 | * @apiError EmailPermissionsDontExist No user email permissions exist for the given user. 32 | */ 33 | export async function gatherEmailPermissionsById(request: UserRequest, response: Response) { 34 | const emailOptInRepository = getCustomRepository(EmailRepository); 35 | const permissions = await emailOptInRepository.getEmailOptInPermissionForUser(request.boundUser); 36 | 37 | if (isNil(permissions)) { 38 | throw new ApiError({ 39 | error: `No email permission exist for the given user, ${request.boundUser.username}`, 40 | code: 404, 41 | }); 42 | } 43 | 44 | return response.json(permissions); 45 | } 46 | 47 | /** 48 | * @api {patch} /users/:user/emails/permissions Update user related email permissions 49 | * @apiVersion 1.0.0 50 | * @apiName UpdateUserRelatedEmailPermissions 51 | * @apiGroup Emails 52 | * 53 | * @apiParam {Number} user Users unique ID. 54 | * @apiParam {string} [news] If the user is allowing news emails. 55 | * @apiParam {string} [gameApplications] If the user is allowing game application emails. 56 | * @apiParam {string} [schedules] If the user is allowing schedule emails. 57 | * @apiParam {string} [linkedAccounts] If the user is allowing linked account emails. 58 | * 59 | * @apiParamExample {json} Request-Example: 60 | * { 61 | * "news": true, 62 | * "gameApplications": false 63 | * } 64 | * 65 | * @apiSuccess {EmailOptIn} Permissions The current email permissions for the given user after being updated. 66 | * @apiSuccessExample Success-Response: HTTP/1.1 200 OK 67 | * { 68 | * "news": true, 69 | * "gameApplications": true, 70 | * "schedules": true, 71 | * "linkedAccounts": true, 72 | * "id": 1, 73 | * "updatedAt": "2019-11-23T17:30:25.884Z", 74 | * "createdAt": "2019-11-23T17:02:58.886Z" 75 | * } 76 | * 77 | * @apiError EmailPermissionsDontExist No user email permissions exist for the given user. 78 | */ 79 | export async function updateEmailPermissionsById(request: UserRequest, response: Response) { 80 | const emailOptInRepository = getCustomRepository(EmailRepository); 81 | const permissions: any = await emailOptInRepository.getEmailOptInPermissionForUser(request.boundUser); 82 | 83 | const emailUpdate: UpdateEmailPermissionRequest = request.body; 84 | 85 | if (isNil(permissions)) { 86 | throw new ApiError({ 87 | error: `No email permission exist for the given user, ${request.boundUser.username}`, 88 | code: 404, 89 | }); 90 | } 91 | 92 | for (const key of Object.keys(permissions)) { 93 | if (!isNil(emailUpdate[key])) { 94 | permissions[key] = emailUpdate[key]; 95 | } 96 | } 97 | 98 | await (permissions as EmailOptIn).save(); 99 | return response.json(permissions); 100 | } 101 | -------------------------------------------------------------------------------- /app/controllers/error.controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | import { isNil, isNumber } from 'lodash'; 3 | 4 | import logger from '../utils/logger'; 5 | import ApiError from '../utils/apiError'; 6 | import { AuthService } from '../services/auth.service'; 7 | 8 | /** 9 | * Handles catches in which the next response of a given controller is a error 10 | * but was not caught by anything. Ensuring that regardless of the result, that 11 | * the user still gets a response back from the server. 12 | */ 13 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 14 | export function handleError(error: any, request: Request, response: Response, next: NextFunction) { 15 | const apiError = error as ApiError; 16 | 17 | // If specified or ont a api error, log the error. 18 | if (!(error instanceof ApiError) || (!isNil(apiError.code) && apiError.log)) { 19 | const { protocol, originalUrl } = request; 20 | 21 | const message = ['test', 'development'].includes(process.env.NODE_ENV) ? error.stack : error; 22 | logger.error(`error on request: ${protocol}://${request.get('host')}${originalUrl}, ${message}`); 23 | } 24 | 25 | // If we have thrown a instance of a apiError and it was not a 500, then process the 26 | // expected error message with the expected code + error message. 27 | if (!isNil(apiError.code) && isNumber(apiError.code)) { 28 | return response.status(apiError.code).json({ error: apiError.message }); 29 | } 30 | 31 | // if we are in production and a internal server error occurs, just let the user know. We 32 | // don't want to be exposing any additional information that would help someone trying to 33 | // gather internal information about the system. But during development, ignore this and 34 | // send back the error and the stack that caused it. 35 | if (process.env.NODE_ENV === 'production') { 36 | return response.sendStatus(500).json({ error: 'Internal server error, something went wrong.' }); 37 | } 38 | 39 | return response.status(500).json({ error: error.message, stack: error.stack }); 40 | } 41 | 42 | /** 43 | * Handles cases in which the route does not exist, e.g /authentication/missing 44 | */ 45 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 46 | export function handleMissing(request: Request, response: Response, next: NextFunction) { 47 | const { token } = request.cookies; 48 | 49 | // If the user who has been redirected to a invalid endpoint that does not 50 | // exist for any reason and that user is not authenticated, just respond as 51 | // if they are not authenticated, otherwise return 404 (not found). 52 | if (isNil(token) || isNil(AuthService.VerifyAuthenticationToken(token))) return response.status(401).send(); 53 | return response.status(404).send(); 54 | } 55 | -------------------------------------------------------------------------------- /app/controllers/health.controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import * as fs from 'fs'; 3 | 4 | import { canAccessPath, pathExists } from '../utils/helpers'; 5 | import path = require('path'); 6 | 7 | // eslint-disable-next-line @typescript-eslint/no-var-requires 8 | const packageJson = require('../../package'); 9 | 10 | /** 11 | * @api {get} /health Health status of server & its current version. 12 | * @apiVersion 1.0.0 13 | * @apiName Health 14 | * @apiGroup Health 15 | * 16 | * @apiSuccess {string} health.status Status of the server 17 | * @apiSuccess {string} health.version The current server api version. 18 | * 19 | * @apiSuccessExample Success-Response: 20 | * HTTP/1.1 200 OK 21 | * { 22 | * "status": "Healthy", 23 | * "version": "0.1.0", 24 | * } 25 | */ 26 | export function getBasicServerHealth(request: Request, response: Response): Response { 27 | return response.status(200).json({ 28 | status: 'Healthy', 29 | version: packageJson.version, 30 | }); 31 | } 32 | 33 | /** 34 | * @api {get} /logs Gets the server standard logs 35 | * @apiVersion 1.0.0 36 | * @apiName ServerLogs 37 | * @apiGroup Health 38 | * @apiPermission Moderator, Admin 39 | * 40 | * @apiSuccess {string[]} logs The standard logs of the server. 41 | * 42 | * @apiSuccessExample Success-Response: 43 | * HTTP/1.1 200 OK 44 | * { 45 | * "logs": [...] 46 | * } 47 | */ 48 | export function getAllServerLogs(request: Request, response: Response): Response { 49 | const allLogsPath = path.resolve(__dirname, '../../logs/all.log'); 50 | 51 | const logs: { logs: string[] } = { logs: [] }; 52 | 53 | if (pathExists(allLogsPath) && canAccessPath(allLogsPath, fs.constants.R_OK)) { 54 | logs.logs = fs.readFileSync(allLogsPath).toString().split('\n'); 55 | } 56 | 57 | return response.json(logs); 58 | } 59 | 60 | /** 61 | * @api {get} /logs/error Gets the server error logs 62 | * @apiVersion 1.0.0 63 | * @apiName ServerErrorLogs 64 | * @apiGroup Health 65 | * @apiPermission Moderator, Admin 66 | * 67 | * @apiSuccess {string[]} logs The error logs of the server. 68 | * 69 | * @apiSuccessExample Success-Response: 70 | * HTTP/1.1 200 OK 71 | * { 72 | * "logs": [...] 73 | * } 74 | */ 75 | export function getErrorServerLogs(request: Request, response: Response): Response { 76 | const errorLogsPath = path.resolve(__dirname, '../../logs/error.log'); 77 | 78 | const logs: { logs: string[] } = { logs: [] }; 79 | 80 | if (pathExists(errorLogsPath) && canAccessPath(errorLogsPath, fs.constants.R_OK)) { 81 | logs.logs = fs.readFileSync(errorLogsPath).toString().split('\n'); 82 | } 83 | 84 | return response.json(logs); 85 | } 86 | -------------------------------------------------------------------------------- /app/controllers/leaderboard.controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import { getCustomRepository } from 'typeorm'; 3 | 4 | import UserRepository from '../repository/user.repository'; 5 | import RankRepository from '../repository/rank.repository'; 6 | 7 | 8 | /** 9 | * @api {get} /users/leaderboards Get the current win based leaderboards for all users. 10 | * @apiDescription Gathers the current win leaderboard statistics for all users in a paging fashion. 11 | * @apiName GetLeaderboardsForUser 12 | * @apiGroup User 13 | * 14 | * @apiParam {string} limit The number of users to gather from the offset. (limit: 100) 15 | * @apiParam {string} offset The offset of which place to start gathering users from. 16 | * 17 | * @apiSuccess {json} Leaderboards The users leaderboards within the limit and offset. 18 | * 19 | * @apiSuccessExample Success-Response: 20 | * HTTP/1.1 200 OK 21 | * { 22 | * "data": [ 23 | * { 24 | * "userId": 46, 25 | * "username": "Sigurd.Harber", 26 | * "wins": 5, 27 | * "loses": 17, 28 | * "xp": 15039, 29 | * "coins": 18316, 30 | * "rank": { name: "Hacker I"}, 31 | * "level": 3 32 | * ] 33 | * } 34 | * ], 35 | * "pagination": { 36 | * "next": "bmV4dF9fQWxleGFubmVfQWx0ZW53ZXJ0aA==", 37 | * "previous": null 38 | * } 39 | * } 40 | */ 41 | export async function getUsersLeaderboards(request: Request, response: Response) { 42 | const userRepository = getCustomRepository(UserRepository); 43 | 44 | const results = await userRepository 45 | .createQueryBuilder('user') 46 | .innerJoinAndSelect('user.gameStats', 'gameStats') 47 | .innerJoinAndSelect('user.stats', 'stats') 48 | .orderBy('gameStats.wins', 'DESC') 49 | .take(30) 50 | .getMany(); 51 | 52 | const rankRepository = getCustomRepository(RankRepository); 53 | 54 | for (const result of results) { 55 | const rank = await rankRepository.getRankFromExperience(result.stats.xp); 56 | (result as any).rank = rank; 57 | } 58 | 59 | return response.json({ 60 | data: results.map((u) => u.sanitize('email', 'lastSignIn', 'createdAt', 'updatedAt', 'lastUsernameUpdateAt')), 61 | }); 62 | } 63 | -------------------------------------------------------------------------------- /app/controllers/userAvatar.controller.ts: -------------------------------------------------------------------------------- 1 | import { Response } from 'express'; 2 | 3 | import { AvatarService } from '../services/avatar.service'; 4 | import { UserRequest } from '../request/requests'; 5 | import ApiError from '../utils/apiError'; 6 | 7 | /** 8 | * @api {put} /users/:user/avatar Update the users avatar 9 | * 10 | * @apiVersion 1.0.0 11 | * @apiName UpdateUserAvatar 12 | * @apiGroup Users 13 | */ 14 | export async function updateUserAvatarById(request: UserRequest, response: Response) { 15 | try { 16 | await AvatarService.updateAvatarForUser(request.boundUser, request.file.path); 17 | } catch (e) { 18 | throw new ApiError({ error: "We couldn't upload your avatar", code: 400 }); 19 | } 20 | 21 | return response.json(request.boundUser); 22 | } 23 | -------------------------------------------------------------------------------- /app/controllers/userGameStats.controller.ts: -------------------------------------------------------------------------------- 1 | import { Response } from 'express'; 2 | import { getCustomRepository } from 'typeorm'; 3 | 4 | import UserRepository from '../repository/user.repository'; 5 | import { UserRequest } from '../request/requests'; 6 | 7 | /** 8 | * @api {get} /users/:user/statistics/game Get the game statistics of a user. 9 | * @apiName GetGameStatsOfUser 10 | * @apiGroup User 11 | * 12 | * @apiParam {string} user The id of the user. 13 | * 14 | * @apiSuccess {number} id The id of the user. 15 | * @apiSuccess {datetime} updatedAt The time the user was last updated. 16 | * @apiSuccess {datetime} createdAt The time the user was created at. 17 | * @apiSuccess {number} wins The number of wins the user has. 18 | * @apiSuccess {number} loses The number of losses the user has. 19 | * 20 | * @apiSuccessExample {json} Success-Response: 21 | * { 22 | * "id": 1, 23 | * "updatedAt": "1969-12-31T17:00:00.000Z", 24 | * "createdAt": "1969-12-31T17:00:00.000Z", 25 | * "wins": 1, 26 | * "loses": 0 27 | * } 28 | */ 29 | export async function getUserGameStatisticsById(request: UserRequest, response: Response) { 30 | const userRepository = getCustomRepository(UserRepository); 31 | const stats = await userRepository.findGameStatsByUser(request.boundUser); 32 | return response.json(stats); 33 | } 34 | -------------------------------------------------------------------------------- /app/controllers/userStats.controller.ts: -------------------------------------------------------------------------------- 1 | import { Response } from 'express'; 2 | import { getCustomRepository } from 'typeorm'; 3 | import { defaultTo } from 'lodash'; 4 | 5 | import UserRepository from '../repository/user.repository'; 6 | import LinkedAccountRepository from '../repository/linkedAccount.repository'; 7 | import { UserRequest } from '../request/requests'; 8 | 9 | /** 10 | * @api {get} /users/:user/statistics/ Get the statistics of a user. 11 | * @apiName GetStatsOfUser 12 | * @apiGroup User 13 | * 14 | * @apiParam {string} user The id of the user. 15 | * 16 | * @apiSuccess {number} id The id of the user. 17 | * @apiSuccess {datetime} updatedAt the time the user was last updated. 18 | * @apiSuccess {datetime} createdAt the time the user was created at. 19 | * @apiSuccess {number} coins The number of coins the user has. default is 0. 20 | * @apiSuccess {number} xp The amount of xp the user has. default is 0. 21 | * @apiSuccess {string} twitchId The Twitch id of the user. 22 | * @apiSuccess {object} game The game stats of the user. 23 | * @apiSuccess {number} game.id The id of the user. 24 | * @apiSuccess {datetime} game.updatedAt The time the user was last updated. 25 | * @apiSuccess {datetime} game.createdAt The time the user was created at. 26 | * @apiSuccess {number} game.wins The number of wins the user has. 27 | * @apiSuccess {number} game.loses The number of losses the user has. 28 | * 29 | * @apiSuccessExample {json} Success-Response: 30 | * { 31 | * "id": 1, 32 | * "updatedAt": "1969-12-31T17:00:00.000Z", 33 | * "createdAt": "1969-12-31T17:00:00.000Z", 34 | * "coins": 48837, 35 | * "xp": 600, 36 | * "twitchId": null, 37 | * "game": { 38 | * "id": 1, 39 | * "updatedAt": "1969-12-31T17:00:00.000Z", 40 | * "createdAt": "1969-12-31T17:00:00.000Z", 41 | * "wins": 1, 42 | * "loses": 0 43 | * } 44 | * } 45 | */ 46 | export async function getUserStatisticsById(request: UserRequest, response: Response) { 47 | const { boundUser: user } = request; 48 | 49 | const userRepository = getCustomRepository(UserRepository); 50 | const linkedAccountRepository = getCustomRepository(LinkedAccountRepository); 51 | 52 | const statistics = await userRepository.findStatisticsForUser(user); 53 | 54 | // gather all related link account coins. 55 | const linkedAccounts = await linkedAccountRepository.findAllByUserId(user.id); 56 | linkedAccounts.forEach((account) => (statistics.coins += defaultTo(account.storage?.coins, 0))); 57 | 58 | return response.json(statistics); 59 | } 60 | -------------------------------------------------------------------------------- /app/index.ts: -------------------------------------------------------------------------------- 1 | import { config } from './config'; 2 | import logger from './utils/logger'; 3 | import Server from './services/server.service'; 4 | 5 | // eslint-disable-next-line @typescript-eslint/no-var-requires 6 | const packageJson = require('../package.json'); 7 | 8 | /** 9 | * Called when a error occurring within the http server. 10 | * @param {Error} error The error that occurred. 11 | */ 12 | const handleServerError = (error: any) => { 13 | if (error.syscall !== 'listen') throw error; 14 | 15 | switch (error.code) { 16 | case 'EACCES': 17 | logger.error('Port requires elevated privileges'); 18 | process.exit(1); 19 | break; 20 | case 'EADDRINUSE': 21 | logger.error('Port is already in use'); 22 | process.exit(1); 23 | break; 24 | default: 25 | throw error; 26 | } 27 | }; 28 | 29 | (async () => { 30 | const appServer = new Server(); 31 | const server = await appServer.Start(); 32 | 33 | server.on('error', handleServerError); 34 | 35 | server.listen(config.port, config.host, () => { 36 | logger.info([ 37 | `${packageJson.name} v${packageJson.version}`, 38 | process.env.NODE_ENV, 39 | `listening: http://${config.host}:${config.port}`, 40 | `pid: ${process.pid}`, 41 | ].join(' | ')); 42 | }); 43 | })(); 44 | -------------------------------------------------------------------------------- /app/middleware/authentication.middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Response } from 'express'; 2 | import { getCustomRepository } from 'typeorm'; 3 | import * as _ from 'lodash'; 4 | 5 | import User, { UserRole } from '../models/user.model'; 6 | import UserRepository from '../repository/user.repository'; 7 | import { AuthService } from '../services/auth.service'; 8 | import { AuthorizedRequest } from '../request/requests'; 9 | import { wrapAsync } from '../routes/handlers'; 10 | import ApiError from '../utils/apiError'; 11 | 12 | import { isRoleOrHigher } from '../controllers/authentication.controller'; 13 | 14 | export const mustBeAuthenticated = wrapAsync( 15 | async (request: AuthorizedRequest, response: Response, next: NextFunction) => { 16 | const { token } = request.cookies; 17 | 18 | // Allow and assign dummy user if API_KEY was provided. 19 | if (process.env.API_KEY && request.headers?.apikey === process.env.API_KEY) { 20 | // Dummy user used to bypass middleware down the stack. The mustBeMinimumRole has a special flag for 21 | // allowing apiKeys so the user role is kept to a minimum just in case. 22 | request.user = new User('API_KEY', process.env.API_KEY, 'api@devwars.tv', UserRole.USER); 23 | request.user.id = -1; 24 | 25 | return next(); 26 | } 27 | 28 | if (_.isNil(token)) { 29 | // If the token was not not provided then return that the given user 30 | // is not authenticated. 31 | throw new ApiError({ 32 | error: 'Authentication token was not provided.', 33 | code: 401, 34 | }); 35 | } 36 | // Decode the given token, if the token is null, then the given token is no longer valid and should be rejected. 37 | const decodedToken = AuthService.VerifyAuthenticationToken(token); 38 | 39 | if (_.isNil(decodedToken)) { 40 | throw new ApiError({ code: 401, error: 'Invalid authentication token was provided.' }); 41 | } 42 | 43 | const userRepository = getCustomRepository(UserRepository); 44 | const user = await userRepository.findById(decodedToken.id); 45 | 46 | if (_.isNil(user) || user.token !== token) { 47 | throw new ApiError({ code: 401, error: 'Expired authentication token was provided.' }); 48 | } 49 | 50 | // Ensure that the user is correctly sanitized to remove the token, password and other core 51 | // properties. Since this is the current authenticated user, there is no need to remove any more 52 | // additional properties. 53 | request.user = user; 54 | 55 | return next(); 56 | } 57 | ); 58 | 59 | export const mustBeMinimumRole = (role?: UserRole, allowApiKey = false) => 60 | wrapAsync(async (request: AuthorizedRequest, response: Response, next: NextFunction) => { 61 | // The apiKey dummy user should have the apiKey set on the password field. 62 | if (allowApiKey && request?.user?.password === process.env.API_KEY) { 63 | return next(); 64 | } 65 | 66 | if (_.isNil(role) && allowApiKey) { 67 | throw new ApiError({ code: 403, error: 'Unauthorized, invalid api key specified.' }); 68 | } 69 | 70 | // If the authorized user does meet the minimal requirement of the role or greater, then the 71 | // request can continue as expected. 72 | if (!_.isNil(request.user) && !_.isNil(role) && isRoleOrHigher(request.user, role)) { 73 | return next(); 74 | } 75 | 76 | // Otherwise ensure that the user is made aware that they are not meeting the minimal 77 | // requirements of the role. 78 | throw new ApiError({ code: 403, error: "Unauthorized, you currently don't meet the minimal requirement." }); 79 | }); 80 | 81 | /** 82 | * mustOwnUser ensures that the current authenticated is the same entity as the one the following 83 | * request is being performed on. e.g updating their own profile but not owners. 84 | */ 85 | export const mustBeRoleOrOwner = (role?: UserRole, bot = false) => 86 | wrapAsync(async (request: AuthorizedRequest, response: Response, next: NextFunction) => { 87 | const requestedUserId = Number(request.params.user); 88 | 89 | // Ensure that the requesting user is the entity they are also trying to perform the following 90 | // request on. For example: you can only update your own profile and not others (unless your a admin). 91 | if (!_.isNil(request.user) && request.user.id === requestedUserId) { 92 | return next(); 93 | } 94 | 95 | return mustBeMinimumRole(role, bot)(request, response, next); 96 | }); 97 | -------------------------------------------------------------------------------- /app/middleware/gameApplication.middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Response } from 'express'; 2 | import { getCustomRepository } from 'typeorm'; 3 | import * as _ from 'lodash'; 4 | 5 | import { GameRequest } from '../request/requests'; 6 | import GameRepository from '../repository/game.repository'; 7 | import { DATABASE_MAX_ID } from '../constants'; 8 | import { wrapAsync } from '../routes/handlers'; 9 | import ApiError from '../utils/apiError'; 10 | import { parseIntWithDefault } from '../utils/helpers'; 11 | 12 | export const bindGameById = (id: any) => 13 | wrapAsync(async (request: GameRequest, response: Response, next: NextFunction) => { 14 | const gameId = parseIntWithDefault(id, null, 1, DATABASE_MAX_ID); 15 | 16 | if (_.isNil(gameId)) throw new ApiError({ code: 400, error: 'Invalid game id provided.' }); 17 | 18 | const gameRepository = getCustomRepository(GameRepository); 19 | const game = await gameRepository.findOne(gameId); 20 | 21 | if (_.isNil(game)) throw new ApiError({ code: 404, error: 'A game does not exist by the provided game id.' }); 22 | 23 | request.game = game; 24 | return next(); 25 | }); 26 | 27 | /** 28 | * A middleware designed to automatically bind a given game that was specified 29 | * in the url paramter, this is used as a point of entry so that future 30 | * requests do not have to go through the process of ensuring that a given game 31 | * exists or not. If it made it to the request then the game exits. 32 | */ 33 | export const bindGameByParamId = (identifier = 'game') => async ( 34 | request: GameRequest, 35 | response: Response, 36 | next: NextFunction 37 | ) => bindGameById(request.params[identifier])(request, response, next); 38 | 39 | /** 40 | * A middleware designed to automatically bind a given game that was specified 41 | * in the url query, this is used as a point of entry so that future 42 | * requests do not have to go through the process of ensuring that a given game 43 | * exists or not. If it made it to the request then the game exits. 44 | */ 45 | export const bindGameByQueryId = (identifer = 'game') => async ( 46 | request: GameRequest, 47 | response: Response, 48 | next: NextFunction 49 | ) => bindGameById(request.query[identifer])(request, response, next); 50 | -------------------------------------------------------------------------------- /app/middleware/user.middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Response } from 'express'; 2 | import { getCustomRepository } from 'typeorm'; 3 | import * as _ from 'lodash'; 4 | 5 | import { UserRequest } from '../request/requests'; 6 | import UserRepository from '../repository/user.repository'; 7 | import { DATABASE_MAX_ID } from '../constants'; 8 | import { wrapAsync } from '../routes/handlers'; 9 | import ApiError from '../utils/apiError'; 10 | 11 | export const bindUserById = (id: any, optional = false) => 12 | wrapAsync(async (request: UserRequest, response: Response, next: NextFunction) => { 13 | if (_.isNil(id) && optional) return next(); 14 | 15 | if (_.isNil(id) || isNaN(_.toNumber(id)) || Number(id) > DATABASE_MAX_ID) 16 | throw new ApiError({ code: 400, error: 'Invalid user id provided.' }); 17 | 18 | const userRepository = getCustomRepository(UserRepository); 19 | const user = await userRepository.findById(id); 20 | 21 | // if the user does not exist, we cannot fulfil the complete request. SO lets 22 | // go and let the user know that the user does not exist and return out of 23 | // the request. 24 | if (_.isNil(user)) { 25 | throw new ApiError({ 26 | error: 'A user does not exist for the given id.', 27 | code: 404, 28 | }); 29 | } 30 | 31 | request.boundUser = user; 32 | next(); 33 | }); 34 | 35 | /** 36 | * A middleware designed to automatically bind a given user that was specified 37 | * in the url paramter, this is used as a point of entry so that future 38 | * requests do not have to go through the process of ensuring that a given user 39 | * exists or not. If it made it to the request then the user exits. 40 | */ 41 | export const bindUserByParamId = (identifier = 'user', optional = false) => async ( 42 | request: UserRequest, 43 | response: Response, 44 | next: NextFunction 45 | ) => bindUserById(request.params[identifier], optional)(request, response, next); 46 | 47 | /** 48 | * A middleware designed to automatically bind a given user that was specified 49 | * in the url query, this is used as a point of entry so that future 50 | * requests do not have to go through the process of ensuring that a given user 51 | * exists or not. If it made it to the request then the user exits. 52 | */ 53 | export const bindUserByQueryId = (identifer = 'user', optional = false) => async ( 54 | request: UserRequest, 55 | response: Response, 56 | next: NextFunction 57 | ) => bindUserById(request.query[identifer], optional)(request, response, next); 58 | -------------------------------------------------------------------------------- /app/models/activity.model.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, Index, ManyToOne } from 'typeorm'; 2 | import BaseModel from './base.model'; 3 | import User from './user.model'; 4 | 5 | @Entity('activity') 6 | export default class Activity extends BaseModel { 7 | /** 8 | * Short description of the activity 9 | */ 10 | @Column() 11 | public description: string; 12 | 13 | /** 14 | * The amount of coins received by the user 15 | */ 16 | @Column({ default: 0 }) 17 | public coins: number; 18 | 19 | /** 20 | * The amount of xp received by the user 21 | */ 22 | @Column({ default: 0 }) 23 | public xp: number; 24 | 25 | /** 26 | * Receiving user of the activity 27 | */ 28 | 29 | @Index() 30 | @ManyToOne(() => User, (user) => user.activities) 31 | public user: User; 32 | } 33 | -------------------------------------------------------------------------------- /app/models/badge.model.ts: -------------------------------------------------------------------------------- 1 | import { Entity, Column } from 'typeorm'; 2 | import BaseModel from './base.model'; 3 | 4 | export enum BadgeVariant { 5 | Bronze = 0, 6 | Silver = 1, 7 | Gold = 2, 8 | Diamond = 3, 9 | } 10 | 11 | @Entity('badge') 12 | export default class Badge extends BaseModel { 13 | @Column({ name: 'badge_name' }) 14 | public name: string; 15 | 16 | @Column({ name: 'badge_description' }) 17 | public description: string; 18 | 19 | @Column({ name: 'badge_awarding_experience' }) 20 | public awardingExperience: number; 21 | 22 | @Column({ name: 'badge_awarding_coins' }) 23 | public awardingCoins: number; 24 | 25 | @Column({ name: 'badge_variant' }) 26 | public variant: BadgeVariant; 27 | 28 | /** 29 | * Create a new instance of the badge. 30 | * 31 | * @param name The name of the badge. 32 | * @param description The description of the badge. 33 | * @param awardingExperience The awarding experience. 34 | * @param awardingCoins The awarding coins. 35 | * @param variant The variant. 36 | */ 37 | constructor( 38 | name: string, 39 | description: string, 40 | awardingExperience: number, 41 | awardingCoins: number, 42 | variant: BadgeVariant 43 | ) { 44 | super(); 45 | 46 | this.name = name; 47 | this.description = description; 48 | this.awardingExperience = awardingExperience; 49 | this.awardingCoins = awardingCoins; 50 | this.variant = variant; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/models/base.model.ts: -------------------------------------------------------------------------------- 1 | import { BaseEntity, CreateDateColumn, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'; 2 | 3 | export default class BaseModel extends BaseEntity { 4 | @PrimaryGeneratedColumn() 5 | public id: number; 6 | 7 | @UpdateDateColumn() 8 | public updatedAt: Date; 9 | 10 | @CreateDateColumn() 11 | public createdAt: Date; 12 | } 13 | -------------------------------------------------------------------------------- /app/models/emailOptIn.model.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, Index, JoinColumn, OneToOne } from 'typeorm'; 2 | import BaseModel from './base.model'; 3 | import User from './user.model'; 4 | 5 | @Entity('email_opt_in') 6 | export default class EmailOptIn extends BaseModel { 7 | @Column({ name: 'email_opt_in_news', default: true }) 8 | public news: boolean; 9 | 10 | @Column({ name: 'email_opt_in_applications', default: true }) 11 | public gameApplications: boolean; 12 | 13 | @Column({ name: 'email_opt_in_schedules', default: true }) 14 | public schedules: boolean; 15 | 16 | @Column({ name: 'email_opt_in_linked_accounts', default: true }) 17 | public linkedAccounts: boolean; 18 | 19 | // ------------------------------------------------------------ 20 | // Relations 21 | 22 | @Index() 23 | @OneToOne(() => User) 24 | @JoinColumn() 25 | public user: User; 26 | 27 | /** 28 | * Creates a new instance of the email opt in model. 29 | * 30 | * @param user The user who's related to these email settings. 31 | * @param news If the user is accepting emails related to news/updates. 32 | * @param applications If the user is accepting emails related to change in game applications. 33 | * @param schedules If the user is accepting emails in relation to schedule updates/changes. 34 | * @param linkedAccounts If the user is accepting emails related to linked accounts. 35 | */ 36 | public constructor( 37 | user?: User, 38 | news?: boolean, 39 | applications?: boolean, 40 | schedules?: boolean, 41 | linkedAccounts?: boolean 42 | ) { 43 | super(); 44 | 45 | this.user = user; 46 | this.news = news; 47 | this.gameApplications = applications; 48 | this.schedules = schedules; 49 | this.linkedAccounts = linkedAccounts; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/models/emailVerification.model.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, JoinColumn, OneToOne } from 'typeorm'; 2 | import BaseModel from './base.model'; 3 | import User from './user.model'; 4 | 5 | @Entity('email_verification') 6 | export default class EmailVerification extends BaseModel { 7 | @Column() 8 | public token: string; 9 | 10 | // ------------------------------------------------------------ 11 | // Relations 12 | @OneToOne(() => User) 13 | @JoinColumn() 14 | public user: User; 15 | } 16 | -------------------------------------------------------------------------------- /app/models/game.model.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, OneToMany } from 'typeorm'; 2 | import * as _ from 'lodash'; 3 | 4 | import GameApplication from './gameApplication.model'; 5 | import { GameStorage } from '../types/game'; 6 | import BaseModel from './base.model'; 7 | 8 | export enum GameStatus { 9 | SCHEDULED = 0, 10 | ACTIVE = 1, 11 | ENDED = 2, 12 | } 13 | 14 | export enum GameMode { 15 | ZenGarden = 'Zen Garden', 16 | Classic = 'Classic', 17 | Blitz = 'Blitz', 18 | } 19 | 20 | @Entity('game') 21 | export default class Game extends BaseModel { 22 | @Column() 23 | // The title of the given game, this is the display name used when showing 24 | // users of the site players. 25 | public title: string; 26 | 27 | // The expected start time of the given game. 28 | @Column() 29 | public startTime: Date; 30 | 31 | // Season number game was broadcasted 32 | @Column() 33 | public season: number; 34 | 35 | // Represents which game mode we are playing 36 | @Column() 37 | public mode: GameMode; 38 | 39 | // Link to the video recording for this game 40 | @Column({ nullable: true }) 41 | public videoUrl: string; 42 | 43 | // TEMPORARY: Status on game until Editor refactor is completed 44 | @Column({ default: GameStatus.SCHEDULED }) 45 | public status: GameStatus; 46 | 47 | // Big json object with all game information 48 | @Column({ type: 'jsonb', default: {} }) 49 | public storage: GameStorage; 50 | 51 | // ------------------------------------------------------------ 52 | // Relations 53 | // ------------------------------------------------------------ 54 | 55 | @OneToMany(() => GameApplication, (applications) => applications.game) 56 | public applications: GameApplication[]; 57 | 58 | /** 59 | * Creates a new instance of the games model. 60 | * @param season Season number game was broadcasted 61 | * @param mode Represents which game mode we are playing 62 | * @param title The name or theme of the game. 63 | * @param videoUrl The video url of the games recording. 64 | * @param status The status of the game. 65 | * @param storage Any additional storage of the game. 66 | */ 67 | public constructor( 68 | season?: number, 69 | mode?: GameMode, 70 | title?: string, 71 | videoUrl?: string, 72 | status?: GameStatus, 73 | startTime?: Date, 74 | storage?: GameStorage 75 | ) { 76 | super(); 77 | 78 | this.season = season; 79 | this.mode = mode; 80 | this.title = title; 81 | this.videoUrl = videoUrl; 82 | this.status = status; 83 | this.startTime = startTime; 84 | this.storage = storage; 85 | } 86 | 87 | /** 88 | * Adds a template to the given game by the language. 89 | * @param language The language of the template being added. 90 | * @param template The raw template string that is being assigned to that template. 91 | */ 92 | public addTemplate(language: 'html' | 'css' | 'js', template: string) { 93 | if (_.isNil(this.storage.templates)) this.storage.templates = {}; 94 | this.storage.templates[language] = template; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /app/models/gameApplication.model.ts: -------------------------------------------------------------------------------- 1 | import { Entity, ManyToOne, Column, JoinColumn } from 'typeorm'; 2 | 3 | import BaseModel from './base.model'; 4 | import User from './user.model'; 5 | import Game from './game.model'; 6 | 7 | @Entity('game_application') 8 | export default class GameApplication extends BaseModel { 9 | // The id of the team the given user has been assigned too. 10 | @Column({ nullable: true }) 11 | public team: number; 12 | 13 | // The assigned language of the given user. 14 | @Column('simple-array', { nullable: true }) 15 | public assignedLanguages: string[]; 16 | 17 | // ------------------------------------------------------------ 18 | // Relations 19 | // ------------------------------------------------------------ 20 | 21 | // The id of the game. 22 | @Column({ nullable: true }) 23 | gameId: number; 24 | 25 | // The game that the user is applying too. 26 | @ManyToOne(() => Game, (game) => game.applications, { onDelete: 'CASCADE' }) 27 | public game: Game; 28 | 29 | // The id of the user. 30 | @Column({ nullable: true }) 31 | userId: number; 32 | 33 | // The user who applied to the given game. 34 | @JoinColumn() 35 | @ManyToOne(() => User, (user) => user.applications) 36 | public user: User; 37 | 38 | /** 39 | * Creates a new instance of the game application instance. 40 | * @param game The game that the user is applying to. 41 | * @param user The user who is applying to the game schedule. 42 | */ 43 | constructor(game?: Game, user?: User) { 44 | super(); 45 | 46 | this.game = game; 47 | this.user = user; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/models/gameSource.model.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, JoinColumn, ManyToOne } from 'typeorm'; 2 | 3 | import BaseModel from './base.model'; 4 | import Game from './game.model'; 5 | 6 | @Entity('game_source') 7 | export default class GameSource extends BaseModel { 8 | // The language of the source. 9 | @Column() 10 | public file: string; 11 | 12 | // The source text of the given game language. 13 | @Column() 14 | public source: string; 15 | 16 | // The allocated team the source came from. 17 | @Column() 18 | public team: number; 19 | 20 | // ------------------------------------------------------------ 21 | // Relations 22 | // ------------------------------------------------------------ 23 | 24 | @ManyToOne(() => Game) 25 | @JoinColumn() 26 | public game: Game; 27 | 28 | /** 29 | * .Creates a new instance of the Game Source model. 30 | * 31 | * @param team The team the source came from. 32 | * @param file The file and the extension tag, e.g game.js 33 | * @param source The raw source text. 34 | * @param game The game that owns the source 35 | */ 36 | public constructor(team?: number, file?: string, source?: string, game?: Game) { 37 | super(); 38 | 39 | this.team = team; 40 | this.file = file; 41 | this.source = source; 42 | this.game = game; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/models/linkedAccount.model.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, Index, JoinTable, ManyToOne } from 'typeorm'; 2 | import BaseModel from './base.model'; 3 | import User from './user.model'; 4 | 5 | export enum Provider { 6 | TWITCH = 'TWITCH', 7 | DISCORD = 'DISCORD', 8 | } 9 | 10 | @Entity('linked_account') 11 | export default class LinkedAccount extends BaseModel { 12 | /** 13 | * Given username from provider 14 | */ 15 | @Column() 16 | public username: string; 17 | 18 | /** 19 | * Used to store information about a linked account 20 | * before the account has been linked to DevWars 21 | */ 22 | @Column({ type: 'jsonb', default: {} }) 23 | public storage: any; 24 | 25 | /** 26 | * Third-party account provider name 27 | */ 28 | @Column() 29 | public provider: string; 30 | 31 | /** 32 | * UUID given from the third-party provider 33 | */ 34 | @Index() 35 | @Column() 36 | public providerId: string; 37 | 38 | // ------------------------------------------------------------ 39 | // Relations 40 | 41 | @Index() 42 | @ManyToOne(() => User, (user) => user.connections) 43 | @JoinTable() 44 | public user: User; 45 | 46 | /** 47 | * Creates a new instance of a linked account model. 48 | * @param user The user who is the owner of the linked account. 49 | * @param linkUsername The username of the linked account. 50 | * @param provider The provider of the linked account (twitch, discord). 51 | * @param providerId The users id of the given linked account. 52 | */ 53 | constructor(user?: User, linkUsername?: string, provider?: string, providerId?: string) { 54 | super(); 55 | 56 | this.user = user; 57 | this.username = linkUsername; 58 | this.provider = provider; 59 | this.providerId = providerId; 60 | this.storage = {}; 61 | } 62 | 63 | public toJSON(): LinkedAccount { 64 | const { username, provider } = { ...this }; 65 | return { username, provider: provider.toLowerCase() } as LinkedAccount; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app/models/newGame.model.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity } from 'typeorm'; 2 | 3 | import { GameStorage } from '../types/newGame'; 4 | import BaseModel from './base.model'; 5 | 6 | @Entity('new_game') 7 | export default class NewGame extends BaseModel { 8 | @Column() 9 | public title: string; 10 | 11 | @Column() 12 | public season: number; 13 | 14 | @Column() 15 | public mode: string; 16 | 17 | @Column({ nullable: true }) 18 | public videoUrl: string; 19 | 20 | @Column({ type: 'jsonb', default: {} }) 21 | public storage: GameStorage; 22 | } 23 | -------------------------------------------------------------------------------- /app/models/passwordReset.model.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, JoinColumn, OneToOne } from 'typeorm'; 2 | import BaseModel from './base.model'; 3 | import User from './user.model'; 4 | 5 | @Entity('password_reset') 6 | export default class PasswordReset extends BaseModel { 7 | @Column() 8 | public expiresAt: Date; 9 | 10 | @Column() 11 | public token: string; 12 | 13 | // ------------------------------------------------------------ 14 | // Relations 15 | @OneToOne(() => User) 16 | @JoinColumn() 17 | public user: User; 18 | 19 | /** 20 | * Creates a new instance of the password reset model. 21 | * @param user The user of the password reset process. 22 | * @param token The associated verification token of the reset process. 23 | * @param expiresAt When the password reset process will expire. 24 | */ 25 | constructor(user?: User, token?: string, expiresAt?: Date) { 26 | super(); 27 | 28 | this.user = user; 29 | this.token = token; 30 | this.expiresAt = expiresAt; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/models/rank.model.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity } from 'typeorm'; 2 | import BaseModel from './base.model'; 3 | 4 | @Entity('rank') 5 | export default class Rank extends BaseModel { 6 | /** 7 | * The level for the given rank being stored. 8 | */ 9 | @Column() 10 | public level: number; 11 | 12 | /** 13 | * The name of the given rank that will be created. 14 | */ 15 | @Column() 16 | public name: string; 17 | 18 | /** 19 | * The total amount of experience required to get to the given rank 20 | */ 21 | @Column({ name: 'total_experience' }) 22 | public totalExperience: number; 23 | 24 | /** 25 | * Creates a new instance of the rank model. 26 | * 27 | * @param level The level of the given rank. 28 | * @param name The name of the given rank. 29 | * @param totalExperience The total required experience of the rank. 30 | */ 31 | constructor(level?: number, name?: string, totalExperience?: number) { 32 | super(); 33 | 34 | this.level = level; 35 | this.name = name; 36 | this.totalExperience = totalExperience; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/models/user.model.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, OneToMany, OneToOne } from 'typeorm'; 2 | import { isArray, isNil } from 'lodash'; 3 | 4 | import BaseModel from './base.model'; 5 | import LinkedAccount from './linkedAccount.model'; 6 | import Activity from './activity.model'; 7 | import UserProfile from './userProfile.model'; 8 | import UserStats from './userStats.model'; 9 | import EmailVerification from './emailVerification.model'; 10 | import GameApplication from './gameApplication.model'; 11 | import EmailOptIn from './emailOptIn.model'; 12 | import PasswordReset from './passwordReset.model'; 13 | import UserGameStats from './userGameStats.model'; 14 | import UserBadges from './userBadges.model'; 15 | 16 | export enum UserRole { 17 | BANNED = 'BANNED', 18 | 19 | PENDING = 'PENDING', 20 | USER = 'USER', 21 | MODERATOR = 'MODERATOR', 22 | ADMIN = 'ADMIN', 23 | } 24 | 25 | @Entity('user') 26 | export default class User extends BaseModel { 27 | // The time at which the user actually updated there username, by default 28 | // this is going to be null since it will allow the user to update there 29 | // username straight after registering. 30 | @Column({ default: null }) 31 | public lastUsernameUpdateAt: Date; 32 | 33 | @Column() 34 | public lastSignIn: Date; 35 | 36 | @Column({ unique: true }) 37 | public email: string; 38 | 39 | @Column({ unique: true }) 40 | public username: string; 41 | 42 | @Column() 43 | public password: string; 44 | 45 | @Column() 46 | public role: UserRole; 47 | 48 | @Column({ nullable: true }) 49 | public token: string; 50 | 51 | @Column({ nullable: true }) 52 | public avatarUrl: string; 53 | 54 | // ------------------------------------------------------------ 55 | // Relations 56 | 57 | @OneToOne(() => UserProfile, (profile) => profile.user) 58 | public profile: UserProfile; 59 | 60 | @OneToOne(() => UserStats, (stats) => stats.user) 61 | public stats: UserStats; 62 | 63 | @OneToOne(() => UserGameStats, (stats) => stats.user) 64 | public gameStats: UserGameStats; 65 | 66 | @OneToOne(() => EmailVerification) 67 | public verification: EmailVerification; 68 | 69 | @OneToOne(() => EmailOptIn) 70 | public emailOptIn: EmailOptIn; 71 | 72 | @OneToOne(() => PasswordReset) 73 | public passwordReset: PasswordReset; 74 | 75 | @OneToMany(() => Activity, (activities) => activities.user) 76 | public activities: Activity; 77 | 78 | @OneToMany(() => GameApplication, (applications) => applications.user) 79 | public applications: GameApplication[]; 80 | 81 | @OneToMany(() => LinkedAccount, (accounts) => accounts.user) 82 | public connections: LinkedAccount[]; 83 | 84 | @OneToMany(() => UserBadges, (userBadges) => userBadges.user) 85 | public badges: UserBadges[]; 86 | 87 | /** 88 | * Creates a new instance of the user model. 89 | * @param username The username of the user. 90 | * @param password The already hashed password of the user. 91 | * @param email The email of the user. 92 | * @param role The role of the user. 93 | */ 94 | constructor(username?: string, password?: string, email?: string, role?: UserRole) { 95 | super(); 96 | 97 | this.username = username; 98 | this.password = password; 99 | this.email = email; 100 | this.role = role; 101 | } 102 | 103 | /** 104 | * Returns true if the given user is banned or not. 105 | */ 106 | public isBanned = (): boolean => this.role === UserRole.BANNED; 107 | 108 | /** 109 | * Returns true if the given user is in pending state or not. 110 | */ 111 | public isPending = (): boolean => this.role === UserRole.PENDING; 112 | 113 | /** 114 | * Returns true if the given user is a moderator or not. 115 | */ 116 | public isModerator = (): boolean => this.role === UserRole.MODERATOR; 117 | 118 | /** 119 | * Returns true if the given user is a administrator or not. 120 | */ 121 | public isAdministrator = (): boolean => this.role === UserRole.ADMIN; 122 | 123 | /** 124 | * Returns true if the given user is a staff member or not. 125 | */ 126 | public isStaff = (): boolean => this.isAdministrator() || this.isModerator(); 127 | 128 | /** 129 | * Removes a collection of properties from the current user. 130 | * @param fields The fields (not including password, token) that is also being removed. 131 | */ 132 | public sanitize(...fields: string[]): User { 133 | if (isNil(fields) || !isArray(fields)) fields = []; 134 | 135 | fields.push('password', 'token'); 136 | const user = { ...this }; 137 | 138 | // remove all the properties specified in the fields list. Ensuring to also delete the users 139 | // password and token regardless if the user also specified it in the fields listings. 140 | for (const field of fields) { 141 | delete user[field as keyof User]; 142 | } 143 | 144 | return user; 145 | } 146 | 147 | public toJSON(): User { 148 | const user = { ...this }; 149 | 150 | for (const field of ['token', 'password']) { 151 | delete user[field as keyof User]; 152 | } 153 | 154 | return user; 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /app/models/userBadges.model.ts: -------------------------------------------------------------------------------- 1 | import { Entity, ManyToOne, JoinColumn, Index } from 'typeorm'; 2 | 3 | import Badge from './badge.model'; 4 | import User from './user.model'; 5 | import BaseModel from './base.model'; 6 | 7 | @Entity('user_badges_badge') 8 | export default class UserBadges extends BaseModel { 9 | // ------------------------------------------------------------ 10 | // Relations 11 | // ------------------------------------------------------------ 12 | 13 | @ManyToOne(() => Badge, (badge) => badge.id) 14 | @JoinColumn({ name: 'badgeId' }) 15 | public badge: Badge; 16 | 17 | 18 | @Index() 19 | @ManyToOne(() => User, (user) => user.id) 20 | @JoinColumn({ name: 'userId' }) 21 | public user: User; 22 | 23 | constructor(user: User, badge: Badge) { 24 | super(); 25 | 26 | this.user = user; 27 | this.badge = badge; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/models/userGameStats.model.ts: -------------------------------------------------------------------------------- 1 | import { Entity, Column, OneToOne, JoinColumn, Index } from 'typeorm'; 2 | import BaseModel from './base.model'; 3 | import User from './user.model'; 4 | 5 | @Entity('user_game_stats') 6 | export default class UserGameStats extends BaseModel { 7 | /** 8 | * The total number of wins the user has occurred on the platform. 9 | */ 10 | @Column({ default: 0, nullable: false }) 11 | public wins: number; 12 | 13 | /** 14 | * The total number of wins the given user is currently had in a row. 15 | */ 16 | @Column({ default: 0, nullable: false, name: 'win_streak' }) 17 | public winStreak: number; 18 | 19 | /** 20 | * The total number of loses the given player has occurred on the platform. 21 | */ 22 | @Column({ default: 0, nullable: false }) 23 | public loses: number; 24 | 25 | /** 26 | * The last played game for any game for the given user. 27 | */ 28 | // @Column({ name: 'last_played', default: new Date('0001-01-01T00:00:00Z'), nullable: false }) 29 | // public lastPlayed: Date; 30 | 31 | // ------------------------------------------------------------ 32 | // Relations 33 | // ------------------------------------------------------------ 34 | 35 | /** 36 | * The user who owns this user game state. 37 | */ 38 | 39 | @Index() 40 | @OneToOne(() => User, (user) => user.id) 41 | @JoinColumn() 42 | public user: User; 43 | 44 | /** 45 | * Creates a new instance of the UserGameStats model. 46 | * @param user The user who owns the UserGameStats model. 47 | */ 48 | constructor(user?: User) { 49 | super(); 50 | 51 | this.user = user; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/models/userProfile.model.ts: -------------------------------------------------------------------------------- 1 | import { Entity, Column, OneToOne, JoinColumn, Index } from 'typeorm'; 2 | import BaseModel from './base.model'; 3 | import User from './user.model'; 4 | 5 | export enum Sex { 6 | MALE = 0, 7 | FEMALE = 1, 8 | OTHER = 2, 9 | } 10 | 11 | @Entity('user_profile') 12 | export default class UserProfile extends BaseModel { 13 | // ------------------------------------------------------------ 14 | // Columns 15 | @Column({ nullable: true }) 16 | public firstName: string; 17 | 18 | @Column({ nullable: true }) 19 | public lastName: string; 20 | 21 | @Column({ nullable: true }) 22 | public dob: Date; 23 | 24 | @Column({ nullable: true }) 25 | public sex: Sex; 26 | 27 | @Column({ type: 'text', nullable: true }) 28 | public about: string; 29 | 30 | @Column({ default: false }) 31 | public forHire: boolean; 32 | 33 | @Column({ nullable: true }) 34 | public company: string; 35 | 36 | @Column({ nullable: true }) 37 | public websiteUrl: string; 38 | 39 | @Column({ nullable: true }) 40 | public addressOne: string; 41 | 42 | @Column({ nullable: true }) 43 | public addressTwo: string; 44 | 45 | @Column({ nullable: true }) 46 | public city: string; 47 | 48 | @Column({ nullable: true }) 49 | public state: string; 50 | 51 | @Column({ nullable: true }) 52 | public zip: string; 53 | 54 | @Column({ nullable: true }) 55 | public country: string; 56 | 57 | @Column({ type: 'jsonb', nullable: true }) 58 | public skills: any; 59 | 60 | // ------------------------------------------------------------ 61 | // Relations 62 | 63 | 64 | @Index() 65 | @OneToOne(() => User) 66 | @JoinColumn() 67 | public user: User; 68 | 69 | /** 70 | * Creates a new instance of the UserProfile model. 71 | * @param user The user who owns the UserProfile model. 72 | */ 73 | constructor(user?: User) { 74 | super(); 75 | 76 | this.user = user; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /app/models/userStats.model.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, Index, JoinColumn, OneToOne } from 'typeorm'; 2 | import BaseModel from './base.model'; 3 | import User from './user.model'; 4 | 5 | @Entity('user_stats') 6 | export default class UserStats extends BaseModel { 7 | // ------------------------------------------------------------ 8 | // Columns 9 | @Column({ default: 0 }) 10 | public coins: number; 11 | 12 | @Column({ default: 0 }) 13 | public xp: number; 14 | 15 | // ------------------------------------------------------------ 16 | // Relations 17 | 18 | @Index() 19 | @OneToOne(() => User) 20 | @JoinColumn() 21 | public user: User; 22 | 23 | /** 24 | * Creates a new instance of the UserStats model. 25 | * @param user The user who owns the UserStats model. 26 | */ 27 | constructor(user?: User) { 28 | super(); 29 | 30 | this.user = user; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/repository/activity.repository.ts: -------------------------------------------------------------------------------- 1 | import { EntityRepository, Repository } from 'typeorm'; 2 | import Activity from '../models/activity.model'; 3 | import User from '../models/user.model'; 4 | 5 | @EntityRepository(Activity) 6 | export default class ActivityRepository extends Repository { 7 | public findByUser(user: User): Promise { 8 | return this.find({ where: { user } }); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /app/repository/badge.repository.ts: -------------------------------------------------------------------------------- 1 | import { EntityRepository, Repository } from 'typeorm'; 2 | import Badge from '../models/badge.model'; 3 | 4 | @EntityRepository(Badge) 5 | export default class BadgeRepository extends Repository {} 6 | -------------------------------------------------------------------------------- /app/repository/emailOptIn.repository.ts: -------------------------------------------------------------------------------- 1 | import { Repository, EntityRepository } from 'typeorm'; 2 | 3 | import EmailOptIn from '../models/emailOptIn.model'; 4 | import User from '../models/user.model'; 5 | 6 | @EntityRepository(EmailOptIn) 7 | export default class EmailOptInRepository extends Repository { 8 | /** 9 | * Gather the email opt-in permission for a given user. 10 | * @param user The user who's permissions are being gathered. 11 | */ 12 | public async getEmailOptInPermissionForUser(user: User): Promise { 13 | return await this.findOne({ where: { user } }); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/repository/emailVerification.repository.ts: -------------------------------------------------------------------------------- 1 | import { Repository, EntityRepository } from 'typeorm'; 2 | 3 | import User from '../models/user.model'; 4 | import EmailVerification from '../models/emailVerification.model'; 5 | import * as _ from 'lodash'; 6 | 7 | @EntityRepository(EmailVerification) 8 | export default class EmailVerificationRepository extends Repository { 9 | /** 10 | * Deletes the existing email verification token if it exists for the user. 11 | * @param user The user who is email verification will be removed. 12 | */ 13 | public async removeForUser(user: User) { 14 | // If the given user object has the verification link directly, go and remove it and return 15 | // out, otherwise we will continue to attempt to find and locate it. 16 | if (!_.isNil(user.verification)) return await this.delete(user.verification); 17 | 18 | const emailVerification = await this.findOne({ where: { user } }); 19 | 20 | // If the user does not already have a email verification token, just return early as if the 21 | // action was performed fully as expected. 22 | if (_.isNil(emailVerification)) return; 23 | 24 | // Delete the email verification for the user. 25 | await emailVerification.remove(); 26 | } 27 | 28 | public forUser(user: User): Promise { 29 | return this.find({ where: { user } }); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/repository/game.repository.ts: -------------------------------------------------------------------------------- 1 | import { EntityRepository, Repository } from 'typeorm'; 2 | import * as _ from 'lodash'; 3 | 4 | import Game from '../models/game.model'; 5 | 6 | @EntityRepository(Game) 7 | export default class GameRepository extends Repository { 8 | /** 9 | * Attempts to find all the games that have a title.. 10 | * 11 | * @param title The title of the game being looked up.j 12 | * @param limit The upper limit of the number of games. 13 | */ 14 | public async getGamesLikeTitle(title: string, limit = 50, relations: string[] = []): Promise { 15 | let query = this.createQueryBuilder('game'); 16 | 17 | if (!_.isEmpty(title)) 18 | query = query.where('LOWER(game.title) LIKE :title', { title: `%${title.toLowerCase()}%` }); 19 | 20 | _.forEach(relations, (relation) => (query = query.leftJoinAndSelect(`game.${relation}`, relation))); 21 | return query.take(limit).getMany(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/repository/gameApplication.repository.ts: -------------------------------------------------------------------------------- 1 | import { EntityRepository, Repository, IsNull, Not } from 'typeorm'; 2 | import * as _ from 'lodash'; 3 | 4 | import GameApplication from '../models/gameApplication.model'; 5 | import User from '../models/user.model'; 6 | import Game from '../models/game.model'; 7 | 8 | @EntityRepository(GameApplication) 9 | export default class GameApplicationRepository extends Repository { 10 | /** 11 | * Finds all applications for the given user. 12 | * @param user The user who owns the given game. 13 | */ 14 | public findByUser(user: User): Promise { 15 | return this.find({ where: { user }, relations: ['game'] }); 16 | } 17 | 18 | /** 19 | * Checks to see if a given application exists for a given user and game. 20 | * @param user The user who owns the application. 21 | * @param game The game that the application is within. 22 | */ 23 | public async existsByUserAndGame(user: User, game: Game): Promise { 24 | const found = await this.count({ 25 | where: { user, game }, 26 | select: ['id'], 27 | }); 28 | 29 | return found >= 1; 30 | } 31 | 32 | /** 33 | * Finds a given game application for a given user for a given game. 34 | * @param user The user who owns the application. 35 | * @param game The game the application is related too. 36 | * @param relations The additional relations on the game application object 37 | * to expand. 38 | */ 39 | public findByUserAndGame(user: User, game: Game, relations: string[] = []): Promise { 40 | return this.findOne({ where: { user, game }, relations }); 41 | } 42 | 43 | /** 44 | * Returns a range of the game applications for a given game. 45 | * @param game The game that will be used to find the applications. 46 | * @param relations The additional relations on the game application object 47 | * to expand. 48 | */ 49 | public async findByGame(game: Game, relations: string[] = []): Promise { 50 | return this.find({ where: { game }, relations }); 51 | } 52 | 53 | /** 54 | * Returns a range of the game applications for a given game that have been selected to play. 55 | * @param game The game that will be used to find the applications. 56 | * @param relations The additional relations on the game application object 57 | * to expand. 58 | */ 59 | public async findAssignedPlayersForGame(game: Game, relations: string[] = []): Promise { 60 | return this.find({ where: { game, team: Not(IsNull()) }, relations }); 61 | } 62 | 63 | /** 64 | * Returns true if and only if the language is in use for the given game and 65 | * the given team. 66 | * 67 | * @param game The game that the language is being checked on. 68 | * @param team The team that the language is being checked on. 69 | * @param language The language that is being checked. 70 | */ 71 | public async isGameLanguageAssigned(game: Game, team: number, language: string): Promise { 72 | const result = await this.createQueryBuilder('application') 73 | .where('application.game = :game', { game: game.id }) 74 | .andWhere('application.team = :team', { team: team }) 75 | .andWhere('application.assignedLanguages LIKE :language', { language: `%${language}%` }) 76 | .getCount(); 77 | 78 | return result >= 1; 79 | } 80 | 81 | /** 82 | * Returns true if the given player is already assigned. 83 | * @param user The user who is checking to be assigned. 84 | * @param game The game which is being checked. 85 | */ 86 | public async isPlayerAlreadyAssignedToAnotherTeam(user: User, game: Game, teamId: number): Promise { 87 | const result = await this.count({ where: { user, game, team: Not(teamId) } }); 88 | return result >= 1; 89 | } 90 | 91 | /** 92 | * assign the player to the given game. 93 | * @param user The user who is being unassigned. 94 | * @param game The game the user is being unassigned from. 95 | * @param team The team the player has been assigned too. 96 | * @param languages The languages the user has been applied too. 97 | */ 98 | public async assignUserToGame(user: User, game: Game, team: number, languages: string[]): Promise { 99 | await this.update({ user, game }, { team: team, assignedLanguages: languages }); 100 | } 101 | 102 | /** 103 | * removes the player from the given game. 104 | * @param user The user who is being unassigned. 105 | * @param game The game the user is being unassigned from. 106 | */ 107 | 108 | public async removeUserFromGame(user: User, game: Game) { 109 | this.update({ user, game }, { team: null, assignedLanguages: [] }); 110 | } 111 | 112 | /** 113 | * Get all the applications for the given game and team. 114 | * @param game The game in which the players are within. 115 | * @param team The team id the players where assigned too. 116 | * @param relations The additional relations to pull back. 117 | */ 118 | public async getAssignedPlayersForTeam( 119 | game: Game, 120 | team: number, 121 | relations: string[] = [] 122 | ): Promise { 123 | if (_.isNil(team)) return []; 124 | 125 | return this.find({ where: { game, team }, relations }); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /app/repository/gameSource.repository.ts: -------------------------------------------------------------------------------- 1 | import { EntityRepository, Repository } from 'typeorm'; 2 | import Game from '../models/game.model'; 3 | import GameSource from '../models/gameSource.model'; 4 | 5 | @EntityRepository(GameSource) 6 | export default class GameSourceRepository extends Repository { 7 | /** 8 | * Get all the related games source code. 9 | * @param game The game which owns the sources. 10 | */ 11 | public findByGame(game: Game): Promise { 12 | return this.find({ where: { game } }); 13 | } 14 | 15 | /** 16 | * Gets all the sources for the given game that came from the specified team. 17 | * 18 | * @param game The game which owns the sources. 19 | * @param team THe id of the team. 20 | */ 21 | public findByGameAndTeam(game: Game, team: number | number): Promise { 22 | return this.find({ where: { game, team } }); 23 | } 24 | 25 | /** 26 | * Returns true if and only if the source exists for the given team, game and file. 27 | * 28 | * @param game The game which owns the sources. 29 | * @param team THe id of the team. 30 | * @param file The selected game file of the team. 31 | */ 32 | public async existsByTeamAndFile(game: Game, team: number | number, file: string): Promise { 33 | const exists = await this.count({ where: { game, team, file } }); 34 | return exists >= 1; 35 | } 36 | 37 | /** 38 | * Gets all the sources for the given game that came from the specified language and team. 39 | * 40 | * @param game The game which owns the sources. 41 | * @param team THe id of the team. 42 | * @param file The selected game file of the team. 43 | */ 44 | public findByGameTeamAndFile(game: Game, team: number | number, file: string): Promise { 45 | return this.findOne({ where: { game, team, file } }); 46 | } 47 | 48 | /** 49 | * Get all the related games source code. 50 | * @param game The game which owns the sources. 51 | */ 52 | public async deleteByGame(game: Game) { 53 | await this.delete({ game }); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/repository/linkedAccount.repository.ts: -------------------------------------------------------------------------------- 1 | import { EntityRepository, Repository } from 'typeorm'; 2 | import LinkedAccount, { Provider } from '../models/linkedAccount.model'; 3 | import * as _ from 'lodash'; 4 | 5 | @EntityRepository(LinkedAccount) 6 | export default class LinkedAccountRepository extends Repository { 7 | public findAllByUserId(userId: number, relations?: string[]): Promise { 8 | return this.find({ where: { user: userId }, relations }); 9 | } 10 | 11 | public findByUserIdAndProvider(userId: number, provider: string, relations?: string[]): Promise { 12 | return this.findOne({ where: { user: userId, provider: provider.toUpperCase() }, relations }); 13 | } 14 | 15 | public findByProviderAndProviderId(provider: string, providerId: string): Promise { 16 | return this.findOne({ where: { provider, providerId }, relations: ['user'] }); 17 | } 18 | 19 | /** 20 | * Creates the missing provider account. 21 | * @param username The username of the account being created. 22 | * @param providerId The provider id of the account. 23 | * @param provider The provider of the account, e.g twitch. 24 | */ 25 | public async createOrFindMissingAccount( 26 | username: string, 27 | providerId: string, 28 | provider: Provider, 29 | relations: string[] = [] 30 | ): Promise { 31 | const existingAccount = await this.findOne({ where: { providerId, provider }, relations }); 32 | 33 | if (!_.isNil(existingAccount)) return existingAccount; 34 | 35 | const newAccount = new LinkedAccount(null, username, provider, providerId); 36 | return this.save(newAccount); 37 | } 38 | 39 | public async findWithPaging({ 40 | first, 41 | after, 42 | orderBy = 'createdAt', 43 | relations = [], 44 | }: { 45 | first: number; 46 | after: number; 47 | orderBy: string; 48 | relations?: string[]; 49 | }): Promise { 50 | return this.find({ 51 | skip: after, 52 | take: first, 53 | order: { 54 | [orderBy]: 'DESC', 55 | }, 56 | relations, 57 | }); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/repository/passwordReset.repository.ts: -------------------------------------------------------------------------------- 1 | import { EntityRepository, Repository } from 'typeorm'; 2 | import PasswordReset from '../models/passwordReset.model'; 3 | 4 | @EntityRepository(PasswordReset) 5 | export default class PasswordResetRepository extends Repository { 6 | public findByToken(token: string): Promise { 7 | return this.findOne({ where: { token }, relations: ['user'] }); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /app/repository/rank.repository.ts: -------------------------------------------------------------------------------- 1 | import { EntityRepository, Repository, LessThanOrEqual } from 'typeorm'; 2 | import Rank from '../models/rank.model'; 3 | 4 | @EntityRepository(Rank) 5 | export default class RankRepository extends Repository { 6 | /** 7 | * Locate the closet rank to a given experience (closest min) 8 | * @param experience The experience to locate the rank.k 9 | */ 10 | public async getRankFromExperience(experience: number) { 11 | return this.findOne({ 12 | select: ['level', 'name'], 13 | where: { totalExperience: LessThanOrEqual(experience) }, 14 | order: { totalExperience: 'DESC' }, 15 | }); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/repository/userBadges.repository.ts: -------------------------------------------------------------------------------- 1 | import { EntityRepository, Repository } from 'typeorm'; 2 | import UserBadges from '../models/userBadges.model'; 3 | 4 | @EntityRepository(UserBadges) 5 | export default class UserBadgesRepository extends Repository {} 6 | -------------------------------------------------------------------------------- /app/repository/userGameStats.repository.ts: -------------------------------------------------------------------------------- 1 | import { EntityRepository, Repository } from 'typeorm'; 2 | import User from '../models/user.model'; 3 | 4 | import UserGameStats from '../models/userGameStats.model'; 5 | 6 | @EntityRepository(UserGameStats) 7 | export default class UserGameStatsRepository extends Repository { 8 | /** 9 | * Marks all the users within the given list as having a additional loss on 10 | * there record. 11 | * @param losers The list of user id's which will be marked as a loss. 12 | */ 13 | public async incrementUsersLosesByIds(losers: User[]): Promise { 14 | await this.createQueryBuilder() 15 | .leftJoinAndSelect('user', 'user') 16 | .update(UserGameStats) 17 | .set({ 18 | loses: () => 'loses + 1', 19 | winStreak: 0, 20 | }) 21 | .where('user IN (:...users)', { users: losers.map((e) => e.id) }) 22 | .execute(); 23 | } 24 | 25 | /** 26 | * Marks all the users within the given list as having a additional win on 27 | * there record. 28 | * @param winners The list of user id's which will be marked as a win. 29 | */ 30 | public async incrementUsersWinsByIds(winners: User[]): Promise { 31 | await this.createQueryBuilder() 32 | .leftJoinAndSelect('user', 'user') 33 | .update(UserGameStats) 34 | .set({ 35 | wins: () => 'wins + 1', 36 | winStreak: () => 'win_streak + 1', 37 | }) 38 | .where('user IN (:...users)', { users: winners.map((e) => e.id) }) 39 | .execute(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/repository/userStatistics.repository.ts: -------------------------------------------------------------------------------- 1 | import { EntityRepository, Repository } from 'typeorm'; 2 | import { isNil } from 'lodash'; 3 | 4 | import UserStats from '../models/userStats.model'; 5 | import User from '../models/user.model'; 6 | import { BadgeService } from '../services/badge.service'; 7 | import { BADGES } from '../constants'; 8 | 9 | @EntityRepository(UserStats) 10 | export default class UserStatisticsRepository extends Repository { 11 | /** 12 | * Updates a given users coins by the provided amount (plus or negative) 13 | * @param user The user who's coin count is going to be updated. 14 | * @param amount The amount the coins are going to be updated by. 15 | */ 16 | public async updateCoinsForUser(user: User, amount: number) { 17 | if (!isFinite(amount)) return; 18 | 19 | const userStats = await this.findOne({ where: { user } }); 20 | if (isNil(userStats)) return; 21 | 22 | // Since the updating amount could be negative and we don't want to 23 | // allow having a total amount of negative coins, if the new total coins 24 | // is less than zero, set the coins to zero (otherwise the result). 25 | userStats.coins = Math.max(0, userStats.coins + amount); 26 | 27 | 28 | await userStats.save(); 29 | 30 | if (userStats.coins >= 5000) { 31 | await BadgeService.awardBadgeToUserById(user, BADGES.DEVWARS_COINS_5000); 32 | } 33 | 34 | if (userStats.coins >= 25000) { 35 | await BadgeService.awardBadgeToUserById(user, BADGES.DEVWARS_COINS_25000); 36 | } 37 | } 38 | 39 | /** 40 | * Increase the given amount of users total experience by the specified amount. 41 | * @param amount The amount of experience increasing by. 42 | * @param users The users gaining the experience. 43 | */ 44 | public async increaseExperienceForUsers(amount: number, users: User[]) { 45 | if (!isFinite(amount) || users.length <= 0) return; 46 | 47 | await this.createQueryBuilder() 48 | .leftJoinAndSelect('user', 'user') 49 | .update(UserStats) 50 | .set({ xp: () => `xp + ${amount}` }) 51 | .where('user IN (:...users)', { users: users.map((e) => e.id) }) 52 | .execute(); 53 | } 54 | 55 | /** 56 | * Decrease the given amount of users total experience by the specified 57 | * amount. 58 | * 59 | * Note: This will ensure that the total amount of experience does not go 60 | * lower than zero. 61 | * 62 | * @param amount The amount of experience increasing by. 63 | * @param users The users gaining the experience. 64 | */ 65 | public async decreaseExperienceForUsers(amount: number, users: User[]) { 66 | if (!isFinite(amount) || users.length <= 0) return; 67 | 68 | await this.createQueryBuilder() 69 | .leftJoinAndSelect('user', 'user') 70 | .update(UserStats) 71 | .set({ xp: () => `GREATEST(0, xp + ${amount})` }) 72 | .where('user IN (:...users)', { users: users.map((e) => e.id) }) 73 | .execute(); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /app/request/archiveGameRequest.ts: -------------------------------------------------------------------------------- 1 | export interface ArchiveGameRequest { 2 | mode: string; 3 | 4 | title: string; 5 | 6 | runTime: number; // in milliseconds 7 | 8 | objectives: Array<{ 9 | id: number; 10 | description: string; 11 | bonus: boolean, 12 | }>; 13 | 14 | teams: Array<{ 15 | id: number; 16 | name: string; 17 | completeObjectives: number[]; 18 | objectiveScore: number; 19 | }>; 20 | 21 | editors: Array<{ 22 | id: number; 23 | language: string; // html, css, js 24 | fileName: string; 25 | fileText: string; 26 | locked: false; 27 | teamId: number; 28 | playerId?: number; 29 | }>; 30 | 31 | players: Array<{ 32 | id: number; 33 | username: string; 34 | role: string; 35 | avatarUrl: string; 36 | teamId: number; 37 | }>; 38 | 39 | teamVoteResults: Array<{ 40 | category: string; // design, function, responsive 41 | teamId: number; 42 | votes: number; 43 | total: number; 44 | score: number; 45 | }>; 46 | } 47 | -------------------------------------------------------------------------------- /app/request/endGameRequest.ts: -------------------------------------------------------------------------------- 1 | export interface EndGameRequest { 2 | // The id of the game that has ended. 3 | id: number; 4 | 5 | // The mode of the game. 6 | mode: string; 7 | 8 | // The title of the game. 9 | title: string; 10 | 11 | // The stage the game is in at time of end. 12 | stage: string; 13 | 14 | // The possible stages of the game. 15 | stages: Array; 16 | 17 | // End date time in epoc time. 18 | stageEndAt: number; 19 | 20 | // total runtime in epoc time. 21 | runTime: number; 22 | 23 | // The objectives that the game had at time of completion. all though sent, 24 | // the central source, is the database. 25 | objectives: Array<{ 26 | id: number; 27 | description: string; 28 | }>; 29 | 30 | // The teams that the game had at the time of start. all though sent, the 31 | // central source should be the database. 32 | teams: Array<{ 33 | id: number; 34 | name: string; 35 | completeObjectives: Array; 36 | objectiveScore: number; 37 | enabled: boolean; 38 | }>; 39 | 40 | // The assigned editors that exist during the games runtime. These exist 41 | // regardless if anyone was assigned. It will be required to filter out 42 | // to ones allocated to players. 43 | editors: Array<{ 44 | id: number; 45 | language: 'html' | 'css' | 'js'; 46 | fileName: 'index.html' | 'game.css' | 'game.js'; 47 | locked: false; 48 | teamId: number; 49 | playerId?: number; 50 | // Internal to the live editor. 51 | connection?: { 52 | socketId: string; 53 | user: { 54 | id: number; 55 | username: string; 56 | role: string; 57 | avatarUrl: string; 58 | }; 59 | }; 60 | }>; 61 | 62 | // Active players within the live editor. This does not mean these are the 63 | // assigned players, but active. All assigned players should be gathered 64 | // from the database. 65 | players: Array<{ 66 | id: number; 67 | username: string; 68 | role: string; 69 | avatarUrl: string; 70 | ready: boolean; 71 | teamId: number; 72 | }>; 73 | 74 | // THe voting outcome for each stage of the process, including the design, 75 | // and should be determined for each team based on the team id. 76 | teamVoteResults: Array<{ 77 | category: 'design' | 'function'; 78 | teamId: number; 79 | votes: number; 80 | total: number; 81 | score: number; 82 | }>; 83 | } 84 | -------------------------------------------------------------------------------- /app/request/loginRequest.ts: -------------------------------------------------------------------------------- 1 | export default interface LoginRequest { 2 | identifier: string; 3 | password: string; 4 | } 5 | -------------------------------------------------------------------------------- /app/request/profileRequest.ts: -------------------------------------------------------------------------------- 1 | import { Sex } from '../models/userProfile.model'; 2 | 3 | export interface Skills { 4 | html: number; 5 | css: number; 6 | js: number; 7 | } 8 | 9 | export interface ProfileRequest { 10 | firstName: string; 11 | lastName: string; 12 | dob: Date; 13 | sex: Sex; 14 | about: string; 15 | forHire: boolean; 16 | company: string; 17 | websiteUrl: string; 18 | addressOne: string; 19 | addressTwo: string; 20 | city: string; 21 | state: string; 22 | zip: string; 23 | country: string; 24 | skills: Skills; 25 | } 26 | -------------------------------------------------------------------------------- /app/request/registrationRequest.ts: -------------------------------------------------------------------------------- 1 | export default interface RegistrationRequest { 2 | email: string; 3 | username: string; 4 | password: string; 5 | } 6 | -------------------------------------------------------------------------------- /app/request/requests.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'express'; 2 | 3 | import User from '../models/user.model'; 4 | import Game, { GameMode, GameStatus } from '../models/game.model'; 5 | import { GameEditorTemplates } from '../types/common'; 6 | 7 | /** 8 | * Extends the default express request to contain a localized object of the DevWars user, this will 9 | * be pushed on during the authentication process. And accessible if required. 10 | */ 11 | export interface AuthorizedRequest extends Request { 12 | user: User; 13 | } 14 | 15 | /** 16 | * Extends the default express request to contain a localized object of the DevWars user, this will 17 | * be pushed on during the binding middleware stage when specified as a middleware and the user param 18 | * is specified. process. And accessible if required. 19 | */ 20 | export interface UserRequest extends Request { 21 | boundUser: User; 22 | } 23 | 24 | /** 25 | * Extends the default express request to contain a localized object of the DevWars game, this will 26 | * be pushed on during the requests that specify the game id in the url. And accessible if required. 27 | */ 28 | export interface GameRequest extends Request { 29 | game: Game; 30 | } 31 | 32 | /** 33 | * Extends the default express request to contain the request information to 34 | * create a new game, this is contained on the body. 35 | */ 36 | export interface CreateGameRequest extends Omit { 37 | body: { 38 | // The start time the game is going to take place. 39 | startTime: Date; 40 | 41 | // The season the game that is being created will be associated with. 42 | season: number; 43 | 44 | // The the mode the game is going to be played as, e.g Classic. 45 | mode: GameMode; 46 | 47 | // The title of the game that is being created. 48 | title: string; 49 | 50 | // The video url of the game that is being created. 51 | videoUrl?: string; 52 | 53 | // The status the game is going to be in on the creation of the game. 54 | status?: GameStatus; 55 | 56 | // The related templates for the given game. 57 | templates?: GameEditorTemplates; 58 | }; 59 | } 60 | 61 | /** 62 | * Extends the default express request to contain a localized object of the DevWars contact us request, this will 63 | * include the name, email and message the user is sending with the contact us page. 64 | */ 65 | export interface ContactRequest extends Omit { 66 | body: { 67 | name: string; 68 | email: string; 69 | message: string; 70 | }; 71 | } 72 | -------------------------------------------------------------------------------- /app/request/updateEmailPermissionRequest.ts: -------------------------------------------------------------------------------- 1 | export interface UpdateEmailPermissionRequest { 2 | [key: string]: boolean; 3 | 4 | news: boolean; 5 | gameApplications: boolean; 6 | schedules: boolean; 7 | linkedAccounts: boolean; 8 | } 9 | -------------------------------------------------------------------------------- /app/request/updateGameRequest.ts: -------------------------------------------------------------------------------- 1 | import { GameStatus, GameMode } from '../models/game.model'; 2 | import { GameStorageMeta } from '../types/game'; 3 | import { GameObjective, GameEditorTemplates } from '../types/common'; 4 | 5 | export interface UpdateGameRequest { 6 | // The updated start time. 7 | startTime: Date; 8 | 9 | // The updated status of the game.. 10 | status: GameStatus; 11 | 12 | // any related meta information about hte game, typically containing all the 13 | // related results and scores of the finished game. 14 | meta?: GameStorageMeta; 15 | 16 | // The objectives of the given game, what the teams must do to be win. 17 | objectives?: { [index: string]: GameObjective }; 18 | 19 | // The templates of the given game. 20 | templates?: GameEditorTemplates; 21 | 22 | // The title of the given game, this is the display name used when showing 23 | // users of the site players. 24 | title: string; 25 | 26 | // The season the game is apart of. 27 | season: number; 28 | 29 | // The mode the game is currently playing, e.g Classic, Blitz. 30 | mode: GameMode; 31 | 32 | // The updated video url of the game being played. e.g on twitch. 33 | videoUrl: string; 34 | } 35 | -------------------------------------------------------------------------------- /app/routes/auth.routes.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | 3 | import * as authValidator from './validators/authentication.validator'; 4 | import * as AuthController from '../controllers/authentication.controller'; 5 | import { mustBeAuthenticated } from '../middleware/authentication.middleware'; 6 | 7 | import { bodyValidation } from './validators'; 8 | import { wrapAsync } from './handlers'; 9 | 10 | const AuthRoute: express.Router = express.Router(); 11 | 12 | AuthRoute.get('/user', [mustBeAuthenticated], wrapAsync(AuthController.getCurrentAuthenticatedUser)); 13 | AuthRoute.post('/login', [bodyValidation(authValidator.loginSchema)], wrapAsync(AuthController.loginUser)); 14 | AuthRoute.post('/logout', [mustBeAuthenticated], wrapAsync(AuthController.logoutUser)); 15 | 16 | AuthRoute.post( 17 | '/register', 18 | [bodyValidation(authValidator.registrationSchema)], 19 | wrapAsync(AuthController.registerNewUser) 20 | ); 21 | 22 | AuthRoute.get('/verify', wrapAsync(AuthController.verifyUser)); 23 | AuthRoute.post('/reverify', [mustBeAuthenticated], wrapAsync(AuthController.reverifyUser)); 24 | 25 | AuthRoute.post( 26 | '/forgot/password', 27 | [bodyValidation(authValidator.forgotPasswordSchema)], 28 | wrapAsync(AuthController.initiatePasswordReset) 29 | ); 30 | 31 | AuthRoute.post( 32 | '/reset/password', 33 | [bodyValidation(authValidator.resetPasswordSchema)], 34 | wrapAsync(AuthController.resetPassword) 35 | ); 36 | 37 | AuthRoute.put( 38 | '/reset/password', 39 | [mustBeAuthenticated, bodyValidation(authValidator.updatePasswordSchema)], 40 | wrapAsync(AuthController.updatePassword) 41 | ); 42 | 43 | AuthRoute.post( 44 | '/reset/email', 45 | [mustBeAuthenticated, bodyValidation(authValidator.resetEmailSchema)], 46 | wrapAsync(AuthController.initiateEmailReset) 47 | ); 48 | 49 | export { AuthRoute }; 50 | -------------------------------------------------------------------------------- /app/routes/badges.routes.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import { wrapAsync } from './handlers'; 3 | 4 | import * as BadgeController from '../controllers/badge.controller'; 5 | import { mustBeAuthenticated } from '../middleware/authentication.middleware'; 6 | 7 | export const BadgeRoute: express.Router = express.Router(); 8 | 9 | BadgeRoute.get( 10 | '/', 11 | [mustBeAuthenticated], 12 | wrapAsync(BadgeController.getAllCurrentBadges), 13 | ); 14 | -------------------------------------------------------------------------------- /app/routes/contact.routes.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | 3 | import { wrapAsync } from './handlers'; 4 | import * as contactController from '../controllers/contact.controller'; 5 | import { contactUsSchema } from './validators/contact.validator'; 6 | import { bodyValidation } from './validators'; 7 | 8 | const ContactRoute: express.Router = express.Router(); 9 | 10 | ContactRoute.post('/', [bodyValidation(contactUsSchema)], wrapAsync(contactController.handleContactPost)); 11 | 12 | export { ContactRoute }; 13 | -------------------------------------------------------------------------------- /app/routes/docs.routes.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | 3 | const DocsRoute: express.Router = express.Router(); 4 | 5 | DocsRoute.use(express.static('docs')); 6 | 7 | export { DocsRoute }; 8 | -------------------------------------------------------------------------------- /app/routes/gameApplication.routes.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | 3 | const GameApplicationRoute: express.Router = express.Router(); 4 | 5 | // TODO: move to /users/:user/applications 6 | GameApplicationRoute.get('/mine'); 7 | 8 | // TODO: move to games/:game/applications 9 | GameApplicationRoute.post('/schedule/:schedule/twitch'); 10 | 11 | export { GameApplicationRoute }; 12 | -------------------------------------------------------------------------------- /app/routes/handlers/index.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express'; 2 | 3 | export const wrapAsync = (fn: any) => async (request: Request, response: Response, next: NextFunction) => { 4 | // Make sure to `.catch()` any errors and pass them along to the `next()` 5 | // middleware in the chain, in this case the error handler. 6 | fn(request, response, next).catch(next); 7 | }; 8 | -------------------------------------------------------------------------------- /app/routes/health.routes.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | 3 | import { mustBeAuthenticated, mustBeMinimumRole } from '../middleware/authentication.middleware'; 4 | import * as HealthController from '../controllers/health.controller'; 5 | import { UserRole } from '../models/user.model'; 6 | 7 | const HealthRoute = express.Router(); 8 | 9 | HealthRoute.get('/', HealthController.getBasicServerHealth); 10 | 11 | HealthRoute.get( 12 | '/logs/', 13 | [mustBeAuthenticated, mustBeMinimumRole(UserRole.MODERATOR)], 14 | HealthController.getAllServerLogs 15 | ); 16 | 17 | HealthRoute.get( 18 | '/logs/error', 19 | [mustBeAuthenticated, mustBeMinimumRole(UserRole.MODERATOR)], 20 | HealthController.getErrorServerLogs 21 | ); 22 | 23 | export { HealthRoute }; 24 | -------------------------------------------------------------------------------- /app/routes/index.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import { AuthRoute } from './auth.routes'; 3 | import { GameRoute } from './game.routes'; 4 | import { HealthRoute } from './health.routes'; 5 | import { LeaderboardRoute } from './leaderboard.routes'; 6 | import { LinkedAccountRoute } from './linkedAccount.routes'; 7 | import { UserRoute } from './user.routes'; 8 | import { ContactRoute } from './contact.routes'; 9 | import { DocsRoute } from './docs.routes'; 10 | import { SearchRoute } from './search.routes'; 11 | import { BadgeRoute } from './badges.routes'; 12 | 13 | interface Route { 14 | path: string; 15 | handler: express.Router; 16 | } 17 | 18 | export const Routes: Route[] = [ 19 | { 20 | handler: HealthRoute, 21 | path: '/health', 22 | }, 23 | { 24 | handler: AuthRoute, 25 | path: '/auth', 26 | }, 27 | { 28 | handler: GameRoute, 29 | path: '/games', 30 | }, 31 | { 32 | handler: LeaderboardRoute, 33 | path: '/leaderboards', 34 | }, 35 | { 36 | handler: LinkedAccountRoute, 37 | path: '/oauth', 38 | }, 39 | { 40 | handler: UserRoute, 41 | path: '/users', 42 | }, 43 | { 44 | handler: ContactRoute, 45 | path: '/contact', 46 | }, 47 | { 48 | handler: BadgeRoute, 49 | path: '/badges' 50 | }, 51 | { 52 | handler: SearchRoute, 53 | path: '/search', 54 | }, 55 | { 56 | handler: DocsRoute, 57 | path: '/docs', 58 | }, 59 | ]; 60 | -------------------------------------------------------------------------------- /app/routes/leaderboard.routes.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import * as LeaderboardController from '../controllers/leaderboard.controller'; 3 | import { wrapAsync } from './handlers'; 4 | 5 | export const LeaderboardRoute: express.Router = express 6 | .Router() 7 | .get('/users', wrapAsync(LeaderboardController.getUsersLeaderboards)); 8 | -------------------------------------------------------------------------------- /app/routes/linkedAccount.routes.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | 3 | import * as LinkedAccountController from '../controllers/linkedAccount.controller'; 4 | import { mustBeAuthenticated, mustBeMinimumRole } from '../middleware/authentication.middleware'; 5 | import { wrapAsync } from './handlers'; 6 | import { UserRole } from '../models/user.model'; 7 | 8 | import { bodyValidation } from './validators'; 9 | import { updateTwitchCoinsSchema } from './validators/linkedAccount.validator'; 10 | 11 | const LinkedAccountRoute: express.Router = express.Router(); 12 | 13 | /****************************** 14 | * CONNECTIONS 15 | ******************************/ 16 | 17 | LinkedAccountRoute.get('/:provider', mustBeAuthenticated, wrapAsync(LinkedAccountController.connectToProvider)); 18 | LinkedAccountRoute.delete('/:provider', mustBeAuthenticated, wrapAsync(LinkedAccountController.disconnectFromProvider)); 19 | 20 | /****************************** 21 | * COINS 22 | ******************************/ 23 | 24 | LinkedAccountRoute.get( 25 | '/:provider/:id/coins', 26 | [mustBeAuthenticated, mustBeMinimumRole(UserRole.MODERATOR, true)], 27 | wrapAsync(LinkedAccountController.getCoinsForUserByProviderAndUserId) 28 | ); 29 | 30 | LinkedAccountRoute.patch( 31 | '/:provider/:id/coins', 32 | [mustBeAuthenticated, mustBeMinimumRole(UserRole.ADMIN, true), bodyValidation(updateTwitchCoinsSchema)], 33 | wrapAsync(LinkedAccountController.updateCoinsForUserByProviderAndUserId) 34 | ); 35 | 36 | export { LinkedAccountRoute }; 37 | -------------------------------------------------------------------------------- /app/routes/search.routes.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import { wrapAsync } from './handlers'; 3 | 4 | import * as SearchController from '../controllers/search.controller'; 5 | import { mustBeMinimumRole, mustBeAuthenticated } from '../middleware/authentication.middleware'; 6 | import { UserRole } from '../models/user.model'; 7 | 8 | export const SearchRoute: express.Router = express.Router(); 9 | 10 | SearchRoute.get( 11 | '/users', 12 | [mustBeAuthenticated, mustBeMinimumRole(UserRole.MODERATOR, true)], 13 | wrapAsync(SearchController.searchForUsers) 14 | ); 15 | 16 | SearchRoute.get( 17 | '/users/connections', 18 | [mustBeAuthenticated, mustBeMinimumRole(UserRole.MODERATOR, true)], 19 | wrapAsync(SearchController.searchForUsersByConnections) 20 | ); 21 | 22 | SearchRoute.get( 23 | '/games', 24 | [mustBeAuthenticated, mustBeMinimumRole(UserRole.MODERATOR)], 25 | wrapAsync(SearchController.searchForGames) 26 | ); 27 | -------------------------------------------------------------------------------- /app/routes/validators/authentication.validator.ts: -------------------------------------------------------------------------------- 1 | import * as Joi from '@hapi/joi'; 2 | 3 | import * as constants from '../../constants'; 4 | 5 | export const registrationSchema = Joi.object() 6 | .keys({ 7 | email: Joi.string().email().required(), 8 | 9 | username: Joi.string() 10 | .min(constants.USERNAME_MIN_LENGTH) 11 | .max(constants.USERNAME_MAX_LENGTH) 12 | .regex(constants.USERNAME_REGEX) 13 | .required(), 14 | 15 | password: Joi.string().min(constants.PASSWORD_MIN_LENGTH).max(constants.PASSWORD_MAX_LENGTH).required(), 16 | }) 17 | .unknown(true); 18 | 19 | export const loginSchema = Joi.object() 20 | .keys({ 21 | identifier: Joi.alternatives( 22 | Joi.string().email().required(), 23 | Joi.string().min(constants.USERNAME_MIN_LENGTH).max(constants.USERNAME_MAX_LENGTH).required() 24 | ).required(), 25 | 26 | password: Joi.string().min(constants.PASSWORD_MIN_LENGTH).max(constants.PASSWORD_MAX_LENGTH).required(), 27 | }) 28 | .unknown(true); 29 | 30 | export const forgotPasswordSchema = Joi.object() 31 | .keys({ 32 | username_or_email: Joi.alternatives( 33 | Joi.string().email().required(), 34 | Joi.string() 35 | .min(constants.USERNAME_MIN_LENGTH) 36 | .max(constants.USERNAME_MAX_LENGTH) 37 | .regex(constants.USERNAME_REGEX) 38 | .required() 39 | ).required(), 40 | }) 41 | .unknown(true); 42 | 43 | /** 44 | * The reset password schema for when a password is being reset. It must contain a valid 45 | * token (which will be validated by the endpoint) and a valid password that meets the 46 | * system requirements 47 | */ 48 | export const resetPasswordSchema = Joi.object() 49 | .keys({ 50 | token: Joi.string().required(), 51 | 52 | password: Joi.string().min(constants.PASSWORD_MIN_LENGTH).max(constants.PASSWORD_MAX_LENGTH).required(), 53 | }) 54 | .unknown(true); 55 | 56 | /** 57 | * The update password schema for when a password is being updated. It must contain a valid old 58 | * password and a valid new password that meets the system requirements. 59 | */ 60 | export const updatePasswordSchema = Joi.object().keys({ 61 | oldPassword: Joi.string().min(constants.PASSWORD_MIN_LENGTH).max(constants.PASSWORD_MAX_LENGTH).required(), 62 | 63 | newPassword: Joi.string().min(constants.PASSWORD_MIN_LENGTH).max(constants.PASSWORD_MAX_LENGTH).required(), 64 | }); 65 | 66 | /** 67 | * The reset password schema for when a password is being updated. It must contain a valid 68 | * token (which will be validated by the endpoint) and a valid password that meets the 69 | * system requirements 70 | */ 71 | export const resetEmailSchema = Joi.object() 72 | .keys({ 73 | email: Joi.string().email().required(), 74 | 75 | password: Joi.string().min(constants.PASSWORD_MIN_LENGTH).max(constants.PASSWORD_MAX_LENGTH).required(), 76 | }) 77 | .unknown(true); 78 | -------------------------------------------------------------------------------- /app/routes/validators/contact.validator.ts: -------------------------------------------------------------------------------- 1 | import * as Joi from '@hapi/joi'; 2 | import { 3 | CONTACT_US_NAME_MIN, 4 | CONTACT_US_NAME_MAX, 5 | CONTACT_US_MESSAGE_MIN, 6 | CONTACT_US_MESSAGE_MAX, 7 | } from '../../constants'; 8 | 9 | export const contactUsSchema = Joi.object().keys({ 10 | name: Joi.string() 11 | .min(CONTACT_US_NAME_MIN) 12 | .max(CONTACT_US_NAME_MAX) 13 | .required(), 14 | 15 | email: Joi.string() 16 | .email() 17 | .required(), 18 | 19 | message: Joi.string() 20 | .min(CONTACT_US_MESSAGE_MIN) 21 | .max(CONTACT_US_MESSAGE_MAX) 22 | .required(), 23 | }); 24 | -------------------------------------------------------------------------------- /app/routes/validators/email.validator.ts: -------------------------------------------------------------------------------- 1 | import * as Joi from '@hapi/joi'; 2 | export const emailPermissionSchema = Joi.object() 3 | .keys({ 4 | news: Joi.boolean() 5 | .strict() 6 | .optional(), 7 | 8 | gameApplications: Joi.boolean() 9 | .strict() 10 | .optional(), 11 | 12 | schedules: Joi.boolean() 13 | .strict() 14 | .optional(), 15 | 16 | linkedAccounts: Joi.boolean() 17 | .strict() 18 | .optional(), 19 | }) 20 | .unknown(true); 21 | -------------------------------------------------------------------------------- /app/routes/validators/game.validator.ts: -------------------------------------------------------------------------------- 1 | import * as Joi from '@hapi/joi'; 2 | 3 | import * as constants from '../../constants'; 4 | import { GameStatus } from '../../models/game.model'; 5 | 6 | export const createGameSchema = Joi.object().keys({ 7 | startTime: Joi.date().required(), 8 | 9 | season: Joi.number().min(constants.GAME_SEASON_MIN).required(), 10 | 11 | mode: Joi.string().required(), 12 | 13 | title: Joi.string().min(constants.GAME_TITLE_MIN_LENGTH).max(constants.GAME_TITLE_MAX_LENGTH).required(), 14 | 15 | videoUrl: Joi.string().allow(null).optional(), 16 | 17 | status: Joi.string().valid(...Object.values(GameStatus)), 18 | 19 | templates: Joi.object() 20 | .keys({ 21 | html: Joi.string().allow(null).optional(), 22 | css: Joi.string().allow(null).optional(), 23 | js: Joi.string().allow(null).optional(), 24 | }) 25 | .optional(), 26 | }); 27 | 28 | export const PatchGameSchema = Joi.object().keys({ 29 | startTime: Joi.date().optional(), 30 | 31 | status: Joi.string().valid(...Object.values(GameStatus)), 32 | 33 | meta: Joi.object().optional(), 34 | 35 | objectives: Joi.object().optional(), 36 | 37 | title: Joi.string().min(constants.GAME_TITLE_MIN_LENGTH).max(constants.GAME_TITLE_MAX_LENGTH).optional(), 38 | 39 | season: Joi.number().min(constants.GAME_SEASON_MIN).optional(), 40 | 41 | mode: Joi.string().required(), 42 | 43 | videoUrl: Joi.string().allow(null).optional(), 44 | 45 | templates: Joi.object() 46 | .keys({ 47 | html: Joi.string().allow(null).allow('').optional(), 48 | css: Joi.string().allow(null).allow('').optional(), 49 | js: Joi.string().allow(null).allow('').optional(), 50 | }) 51 | .optional(), 52 | }); 53 | 54 | export const addGamePlayerSchema = Joi.object().keys({ 55 | player: Joi.object() 56 | .keys({ 57 | id: Joi.alternatives(Joi.string(), Joi.number()).required(), 58 | 59 | language: Joi.string() 60 | .valid(...Object.values(['html', 'css', 'js'])) 61 | .required(), 62 | 63 | team: Joi.alternatives(Joi.number()).required(), 64 | }) 65 | .required(), 66 | }); 67 | 68 | export const removeGamePlayerSchema = Joi.object().keys({ 69 | player: Joi.object() 70 | .keys({ 71 | id: Joi.alternatives(Joi.string(), Joi.number()).required(), 72 | }) 73 | .required(), 74 | }); 75 | -------------------------------------------------------------------------------- /app/routes/validators/index.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express'; 2 | import * as Joi from '@hapi/joi'; 3 | import { map } from 'lodash'; 4 | 5 | async function validator( 6 | content: any, 7 | schema: Joi.ObjectSchema | Joi.ArraySchema, 8 | code = 400, 9 | request: Request, 10 | response: Response, 11 | next: NextFunction 12 | ) { 13 | try { 14 | await schema.validateAsync(content); 15 | return next(); 16 | } catch (error) { 17 | return response.status(code).json({ 18 | error: `${map(error.details, ({ message }) => message.replace(/['"]/g, '')).join( 19 | ' and ' 20 | )}, please check your content and try again.`, 21 | }); 22 | } 23 | } 24 | 25 | /** 26 | * Tests a schema to a given content, if the schema passes, the result will be a empty string 27 | * otherwise the error message generated. 28 | * @param content The content being tested against the schema. 29 | * @param schema The schema being tested. 30 | */ 31 | export async function testSchemaValidation(content: any, schema: Joi.ObjectSchema): Promise { 32 | try { 33 | await schema.validateAsync(content); 34 | return null; 35 | } catch (error) { 36 | return `${map(error.details, ({ message }) => message.replace(/['"]/g, '')).join( 37 | ' and ' 38 | )}, please check your content and try again.`; 39 | } 40 | } 41 | 42 | /** 43 | * Applies and performs a joi validation on the request body based on the passed schema. If the 44 | * validation passes, the next function will be called, otherwise a formatted error message will 45 | * be returned with the provided status code (defaulting to 400 if not specified). 46 | * @param schema The joi schema to be validated against. 47 | * @param code The http code response on validated validation. 48 | */ 49 | export const bodyValidation = (schema: Joi.ObjectSchema | Joi.ArraySchema, code?: number) => async ( 50 | request: Request, 51 | response: Response, 52 | next: NextFunction 53 | ) => validator(request.body, schema, code, request, response, next); 54 | 55 | /** 56 | * Applies and performs a joi validation on the request query based on the passed schema. If the 57 | * validation passes, the next function will be called, otherwise a formatted error message will 58 | * be returned with the provided status code (defaulting to 400 if not specified). 59 | * @param schema The joi schema to be validated against. 60 | * @param code The http code response on validated validation. 61 | */ 62 | export const queryValidation = (schema: Joi.ObjectSchema, code?: number) => async ( 63 | request: Request, 64 | response: Response, 65 | next: NextFunction 66 | ) => validator(request.query, schema, code, request, response, next); 67 | 68 | /** 69 | * Applies and performs a joi validation on the request query based on the passed schema. If the 70 | * validation passes, the next function will be called, otherwise a formatted error message will 71 | * be returned with the provided status code (defaulting to 400 if not specified). 72 | * @param schema The joi schema to be validated against. 73 | * @param code The http code response on validated validation. 74 | */ 75 | export const paramsValidation = (schema: Joi.ObjectSchema, code?: number) => async ( 76 | request: Request, 77 | response: Response, 78 | next: NextFunction 79 | ) => validator(request.params, schema, code, request, response, next); 80 | -------------------------------------------------------------------------------- /app/routes/validators/linkedAccount.validator.ts: -------------------------------------------------------------------------------- 1 | import * as Joi from '@hapi/joi'; 2 | 3 | import * as constants from '../../constants'; 4 | 5 | export const updateTwitchCoinsSchema = Joi.object().keys({ 6 | amount: Joi.number().min(constants.TWITCH_COINS_MIN_UPDATE).max(constants.TWITCH_COINS_MAX_UPDATE).required(), 7 | username: Joi.string().required(), 8 | apiKey: Joi.string().optional(), 9 | }); 10 | -------------------------------------------------------------------------------- /app/routes/validators/user.validator.ts: -------------------------------------------------------------------------------- 1 | import * as Joi from '@hapi/joi'; 2 | 3 | import * as constants from '../../constants'; 4 | import { Sex } from '../../models/userProfile.model'; 5 | import { UserRole } from '../../models/user.model'; 6 | 7 | export const statsSchema = Joi.object().keys({ 8 | coins: Joi.number().min(constants.STATS_COINS_MIN_AMOUNT).max(constants.STATS_COINS_MAX_AMOUNT).required(), 9 | xp: Joi.number().min(constants.STATS_XP_MIN_AMOUNT).max(constants.STATS_XP_MAX_AMOUNT).required(), 10 | }); 11 | 12 | export const profileSchema = Joi.object().keys({ 13 | firstName: Joi.string().allow('', null).optional(), 14 | lastName: Joi.string().allow('', null).optional(), 15 | dob: Joi.date().allow(null).optional(), 16 | sex: Joi.string().valid(Sex.MALE, Sex.FEMALE, Sex.OTHER).optional(), 17 | about: Joi.string().allow('', null).optional(), 18 | forHire: Joi.boolean().optional(), 19 | company: Joi.string().allow('', null).optional(), 20 | websiteUrl: Joi.string().allow('', null).optional(), 21 | addressOne: Joi.string().allow('', null).optional(), 22 | addressTwo: Joi.string().allow('', null).optional(), 23 | city: Joi.string().allow('', null).optional(), 24 | state: Joi.string().allow('', null).optional(), 25 | zip: Joi.string().allow('', null).optional(), 26 | country: Joi.string().allow('', null).optional(), 27 | skills: Joi.object().optional(), 28 | }); 29 | 30 | export const updateUserSchema = Joi.object().keys({ 31 | username: Joi.string() 32 | .min(constants.USERNAME_MIN_LENGTH) 33 | .max(constants.USERNAME_MAX_LENGTH) 34 | .regex(constants.USERNAME_REGEX) 35 | .optional(), 36 | 37 | role: Joi.string() 38 | .valid(...Object.values(UserRole)) 39 | .optional(), 40 | }); 41 | -------------------------------------------------------------------------------- /app/seeding/activity.seeding.ts: -------------------------------------------------------------------------------- 1 | import * as faker from 'faker'; 2 | import Activity from '../models/activity.model'; 3 | import User from '../models/user.model'; 4 | 5 | export default class ActivitySeeding { 6 | public static default(): Activity { 7 | const activity = new Activity(); 8 | 9 | Object.assign(activity, { 10 | description: faker.random.words(5), 11 | coins: faker.datatype.number(20000), 12 | xp: faker.datatype.number(20000), 13 | }); 14 | 15 | return activity; 16 | } 17 | 18 | public static withUser(user: User): Activity { 19 | const activity = this.default(); 20 | 21 | activity.user = user; 22 | 23 | return activity; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/seeding/emailOptIn.seeding.ts: -------------------------------------------------------------------------------- 1 | import * as faker from 'faker'; 2 | import EmailOptIn from '../models/emailOptIn.model'; 3 | import User from '../models/user.model'; 4 | 5 | export default class EmailOptInSeeding { 6 | public static default(user?: User): EmailOptIn { 7 | return new EmailOptIn( 8 | user, 9 | faker.datatype.boolean(), 10 | faker.datatype.boolean(), 11 | faker.datatype.boolean(), 12 | faker.datatype.boolean(), 13 | ); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/seeding/emailVerification.seeding.ts: -------------------------------------------------------------------------------- 1 | import EmailVerification from '../models/emailVerification.model'; 2 | import User from '../models/user.model'; 3 | 4 | export default class EmailVerificationSeeding { 5 | public static default(): EmailVerification { 6 | const emailVerification = new EmailVerification(); 7 | emailVerification.token = 'secret'; 8 | 9 | return emailVerification; 10 | } 11 | 12 | public static withUser(user: User): EmailVerification { 13 | const emailVerification = this.default(); 14 | 15 | emailVerification.user = user; 16 | 17 | return emailVerification; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/seeding/gameApplication.seeding.ts: -------------------------------------------------------------------------------- 1 | import * as faker from 'faker'; 2 | 3 | import GameApplication from '../models/gameApplication.model'; 4 | import Game from '../models/game.model'; 5 | import User from '../models/user.model'; 6 | 7 | export default class GameApplicationSeeding { 8 | public static default(): GameApplication { 9 | const gameApplication = new GameApplication(); 10 | 11 | gameApplication.game = null; 12 | gameApplication.user = null; 13 | gameApplication.team = faker.random.arrayElement([0, 1, null]); 14 | gameApplication.assignedLanguages = 15 | gameApplication.team != null ? [faker.random.arrayElement(['js', 'css', 'html'])] : []; 16 | 17 | return gameApplication; 18 | } 19 | 20 | public static withGameAndUser(game: Game, user: User, blankAssignment = false): GameApplication { 21 | const application = this.default(); 22 | 23 | application.game = game; 24 | application.user = user; 25 | 26 | if (blankAssignment) { 27 | application.assignedLanguages = []; 28 | application.team = null; 29 | } 30 | 31 | return application; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/seeding/index.ts: -------------------------------------------------------------------------------- 1 | export { default as GameApplicationSeeding } from './gameApplication.seeding'; 2 | 3 | export { default as ActivitySeeding } from './activity.seeding'; 4 | 5 | export { default as GameSeeding } from './game.seeding'; 6 | 7 | export { default as UserSeeding } from './user.seeding'; 8 | 9 | export { default as UserGameStatsSeeding } from './userGameStats.seeding'; 10 | 11 | export { default as UserProfileSeeding } from './userProfile.seeding'; 12 | 13 | export { default as UserStatsSeeding } from './userStats.seeding'; 14 | 15 | export { default as EmailVerificationSeeding } from './emailVerification.seeding'; 16 | 17 | export { default as BadgeSeeding } from './badge.seeding'; 18 | -------------------------------------------------------------------------------- /app/seeding/rank.seeding.ts: -------------------------------------------------------------------------------- 1 | import Rank from '../models/rank.model'; 2 | 3 | export default class RankSeeding { 4 | public static default(): Rank[] { 5 | return [ 6 | new Rank(1, 'Intern I', 0), 7 | new Rank(2, 'Intern II', 5000), 8 | new Rank(3, 'Intern III', 10000), 9 | new Rank(4, 'Trainee I', 20000), 10 | new Rank(5, 'Trainee II', 25000), 11 | new Rank(6, 'Trainee III', 30000), 12 | new Rank(7, 'Developer I', 40000), 13 | new Rank(8, 'Developer II', 45000), 14 | new Rank(9, 'Developer III', 50000), 15 | new Rank(10, 'Engineer I', 60000), 16 | new Rank(11, 'Engineer II', 65000), 17 | new Rank(12, 'Engineer III', 70000), 18 | new Rank(13, 'Hacker I', 80000), 19 | new Rank(14, 'Hacker II', 85000), 20 | new Rank(15, 'Hacker III', 90000), 21 | new Rank(16, 'Webmaster', 100000), 22 | ]; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/seeding/user.seeding.ts: -------------------------------------------------------------------------------- 1 | import * as bcrypt from 'bcrypt'; 2 | import * as faker from 'faker'; 3 | import User, { UserRole } from '../models/user.model'; 4 | import { UserProfileSeeding, UserStatsSeeding, UserGameStatsSeeding } from '.'; 5 | import EmailOptInSeeding from './emailOptIn.seeding'; 6 | 7 | export default class UserSeeding { 8 | public static default(): User { 9 | const userCard = faker.helpers.userCard(); 10 | 11 | const role = faker.random.arrayElement([UserRole.PENDING, UserRole.ADMIN, UserRole.MODERATOR, UserRole.USER]); 12 | const user = new User(userCard.username, bcrypt.hashSync('secret', 1), userCard.email, role); 13 | 14 | // user.avatarUrl = random.image(); 15 | user.lastSignIn = new Date(); 16 | 17 | return user; 18 | } 19 | 20 | /** 21 | * Creates a default user with the provided username (this will be forced to lowercase) 22 | * @param username The username of the default user. 23 | */ 24 | public static withUsername(username: string): User { 25 | return Object.assign(this.default(), { 26 | username, 27 | }); 28 | } 29 | 30 | /** 31 | * Creates a default user with the provided email (this will be forced to lowercase) 32 | * @param email The email of the default user. 33 | */ 34 | public static withEmail(email: string): User { 35 | return Object.assign(this.default(), { 36 | email, 37 | }); 38 | } 39 | 40 | public static withRole(role: UserRole) { 41 | const user = this.default(); 42 | 43 | user.role = role; 44 | return user; 45 | } 46 | 47 | public static withComponents(username: string = null, email: string = null, role: UserRole = null) { 48 | return { 49 | save: async (): Promise => { 50 | const user = UserSeeding.default(); 51 | 52 | if (username != null) user.username = username; 53 | if (email != null) user.email = email; 54 | if (role != null) user.role = role; 55 | 56 | const profile = UserProfileSeeding.default(); 57 | const emailOptIn = EmailOptInSeeding.default(); 58 | const stats = UserStatsSeeding.default(); 59 | const gameStats = UserGameStatsSeeding.default(); 60 | 61 | await user.save(); 62 | 63 | profile.user = user; 64 | stats.user = user; 65 | gameStats.user = user; 66 | emailOptIn.user = user; 67 | 68 | await profile.save(); 69 | await stats.save(); 70 | await gameStats.save(); 71 | await emailOptIn.save(); 72 | 73 | return user; 74 | }, 75 | }; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /app/seeding/userGameStats.seeding.ts: -------------------------------------------------------------------------------- 1 | import * as faker from 'faker'; 2 | import UserGameStats from '../models/userGameStats.model'; 3 | import User from '../models/user.model'; 4 | 5 | export default class UserGameStatsSeeding { 6 | public static default(): UserGameStats { 7 | const stats = new UserGameStats(); 8 | 9 | stats.wins = faker.datatype.number({ min: 1, max: 20 }); 10 | stats.loses = faker.datatype.number({ min: 1, max: 20 }); 11 | 12 | return stats; 13 | } 14 | 15 | /** 16 | * User game stats with a user. 17 | * @param user The user who owns the game statistics. 18 | */ 19 | public static withUser(user: User): UserGameStats { 20 | const stats = this.default(); 21 | stats.user = user; 22 | 23 | return stats; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/seeding/userProfile.seeding.ts: -------------------------------------------------------------------------------- 1 | import * as faker from 'faker'; 2 | import UserProfile, { Sex } from '../models/userProfile.model'; 3 | 4 | export default class UserProfileSeeding { 5 | public static default(): UserProfile { 6 | const profile = new UserProfile(); 7 | 8 | profile.firstName = faker.name.firstName(); 9 | profile.lastName = faker.name.lastName(); 10 | profile.dob = faker.date.past(50); 11 | profile.sex = faker.helpers.randomize([Sex.MALE, Sex.FEMALE, Sex.OTHER]); 12 | profile.about = faker.lorem.paragraphs(5); 13 | profile.forHire = faker.datatype.boolean(); 14 | profile.company = faker.company.companyName(); 15 | profile.websiteUrl = faker.internet.url(); 16 | profile.addressOne = faker.address.streetAddress(); 17 | profile.addressTwo = faker.address.secondaryAddress(); 18 | profile.city = faker.address.city(); 19 | profile.state = faker.address.state(); 20 | profile.zip = faker.address.zipCode(); 21 | profile.country = faker.address.country(); 22 | profile.skills = { 23 | css: faker.datatype.number({ min: 1, max: 5 }), 24 | html: faker.datatype.number({ min: 1, max: 5 }), 25 | js: faker.datatype.number({ min: 1, max: 5 }), 26 | }; 27 | 28 | return profile; 29 | } 30 | 31 | public static withUser(user: any) { 32 | const profile = this.default(); 33 | 34 | profile.user = user; 35 | 36 | return profile; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/seeding/userStats.seeding.ts: -------------------------------------------------------------------------------- 1 | import * as faker from 'faker'; 2 | import UserStats from '../models/userStats.model'; 3 | import User from '../models/user.model'; 4 | 5 | export default class UserStatsSeeding { 6 | public static default(): UserStats { 7 | const stats = new UserStats(); 8 | 9 | stats.coins = faker.datatype.number(20000); 10 | stats.xp = faker.datatype.number(100000); 11 | 12 | return stats; 13 | } 14 | 15 | /** 16 | * User stats with a user. 17 | * @param user The user who owns the statistics. 18 | */ 19 | public static withUser(user: User): UserStats { 20 | const stats = this.default(); 21 | stats.user = user; 22 | 23 | return stats; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/services/auth.service.ts: -------------------------------------------------------------------------------- 1 | import * as jwt from 'jsonwebtoken'; 2 | import { getManager } from 'typeorm'; 3 | import { hash } from '../utils/hash'; 4 | import { addHours } from 'date-fns'; 5 | import { nanoid } from 'nanoid'; 6 | 7 | import PasswordReset from '../models/passwordReset.model'; 8 | import UserGameStats from '../models/userGameStats.model'; 9 | import User, { UserRole } from '../models/user.model'; 10 | import UserProfile from '../models/userProfile.model'; 11 | import EmailOptIn from '../models/emailOptIn.model'; 12 | import UserStats from '../models/userStats.model'; 13 | 14 | import RegistrationRequest from '../request/registrationRequest'; 15 | 16 | import { VerificationService } from './verification.service'; 17 | import { sendPasswordResetEmail } from './mail.service'; 18 | 19 | export class AuthService { 20 | public static async register(request: RegistrationRequest, shouldSendVerification = true) { 21 | const { username, email, password } = request; 22 | 23 | let user = new User(username, await hash(password), email, UserRole.PENDING); 24 | user.lastSignIn = new Date(); 25 | 26 | await getManager().transaction(async (transactionalEntityManager) => { 27 | user = await transactionalEntityManager.save(user); 28 | 29 | const profile = new UserProfile(user); 30 | profile.skills = { html: 1, css: 1, js: 1 }; 31 | const userStats = new UserStats(user); 32 | const gameStats = new UserGameStats(user); 33 | const emailOptIn = new EmailOptIn(user); 34 | 35 | await transactionalEntityManager.save(profile); 36 | await transactionalEntityManager.save(userStats); 37 | await transactionalEntityManager.save(gameStats); 38 | await transactionalEntityManager.save(emailOptIn); 39 | }); 40 | 41 | // Only email if specified (is by default) 42 | if (shouldSendVerification) await VerificationService.reset(user); 43 | 44 | return user; 45 | } 46 | 47 | /** 48 | * Generates a new JWT token that will be used for the authorization of the user. 49 | * @param user The user who is getting the new token. 50 | */ 51 | public static async newToken(user: User, expiresIn: string | number = '7d'): Promise { 52 | user.token = jwt.sign({ id: user.id }, process.env.AUTH_SECRET, { expiresIn }); 53 | await user.save(); 54 | return user.token; 55 | } 56 | 57 | /** 58 | * Verifies a given authentication token, if the token fails to verify, then it will be thrown, 59 | * otherwise will return a decoded object. That will contain the database id of the given user. 60 | * @param token The token that is being verified. 61 | */ 62 | public static VerifyAuthenticationToken(token: string): { id: string } | null { 63 | try { 64 | return jwt.verify(token, process.env.AUTH_SECRET) as { id: string }; 65 | } catch (error) { 66 | return null; 67 | } 68 | } 69 | 70 | /** 71 | * Generates a reset token for the user to reset there given password, sending a new reset 72 | * email. They have 6 hours from the current server time to change the password. 73 | * @param user The user of the password being reset. 74 | */ 75 | public static async resetPassword(user: User) { 76 | const reset = new PasswordReset(user, nanoid(64), addHours(new Date(), 6)); 77 | const resetUrl = `${process.env.FRONT_URL}/reset-password?token=${reset.token}`; 78 | 79 | await reset.save(); 80 | 81 | await sendPasswordResetEmail(user, resetUrl); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /app/services/avatar.service.ts: -------------------------------------------------------------------------------- 1 | import * as AWS from 'aws-sdk'; 2 | import * as fs from 'fs'; 3 | import { nanoid } from 'nanoid'; 4 | import { PutObjectRequest } from 'aws-sdk/clients/s3'; 5 | import { ManagedUpload } from 'aws-sdk/lib/s3/managed_upload'; 6 | import User from '../models/user.model'; 7 | 8 | export class AvatarService { 9 | public static async updateAvatarForUser(user: User, filePath: string) { 10 | const path = `profilepics-test/${user.id}/${nanoid()}.jpg`; 11 | 12 | const params: PutObjectRequest = { 13 | Body: fs.createReadStream(filePath), 14 | Bucket: process.env.AWS_BUCKET_NAME, 15 | Key: path, 16 | }; 17 | 18 | const s3 = new AWS.S3(); 19 | 20 | const sendData = await new Promise((resolve, reject) => { 21 | s3.upload(params, (err: Error, data: ManagedUpload.SendData) => { 22 | if (err) { 23 | reject(err); 24 | } 25 | 26 | resolve(data); 27 | }); 28 | }); 29 | 30 | user.avatarUrl = sendData.Location; 31 | 32 | await user.save(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/services/badge.service.ts: -------------------------------------------------------------------------------- 1 | import { getCustomRepository, In } from 'typeorm'; 2 | import * as _ from 'lodash'; 3 | 4 | import Badge from '../models/badge.model'; 5 | import User from '../models/user.model'; 6 | import UserBadges from '../models/userBadges.model'; 7 | import BadgeRepository from '../repository/badge.repository'; 8 | import UserBadgesRepository from '../repository/userBadges.repository'; 9 | import UserGameStatsRepository from '../repository/userGameStats.repository'; 10 | import UserStatisticsRepository from '../repository/userStatistics.repository'; 11 | import { BADGES } from '../constants'; 12 | 13 | export class BadgeService { 14 | /** 15 | * Returns true if and only if the badge has already been assigned. 16 | * 17 | * @param user The user who could own the badge. 18 | * @param badge The badge that might be owned. 19 | */ 20 | public static async checkUserOwnsBadge(user: User, badge: Badge): Promise { 21 | const userBadgesRepository = getCustomRepository(UserBadgesRepository); 22 | const exists = await userBadgesRepository.count({ where: { user, badge } }); 23 | return exists >= 1; 24 | } 25 | 26 | /** 27 | * Award a badge to a given user. 28 | * 29 | * @param user The user getting the badge 30 | * @param badge The badge being awarded. 31 | */ 32 | public static async awardBadgeToUser(user: User, badge: Badge) { 33 | if (await this.checkUserOwnsBadge(user, badge)) return; 34 | 35 | const userBadgesRepository = getCustomRepository(UserBadgesRepository); 36 | const userStatsRepository = getCustomRepository(UserStatisticsRepository); 37 | 38 | await Promise.all([ 39 | await userBadgesRepository.insert(new UserBadges(user, badge)), 40 | await userStatsRepository.updateCoinsForUser(user, badge.awardingCoins), 41 | await userStatsRepository.increaseExperienceForUsers(badge.awardingExperience, [user]), 42 | ]); 43 | } 44 | 45 | /** 46 | * Award a badge to a given user by the badge id.. 47 | * 48 | * @param user The user getting the badge 49 | * @param badgeId The id of the badge being awarded. 50 | */ 51 | public static async awardBadgeToUserById(user: User, badgeId: number) { 52 | const badge = await getCustomRepository(BadgeRepository).findOne(badgeId); 53 | 54 | if (_.isNil(badge)) return; 55 | 56 | return this.awardBadgeToUser(user, badge); 57 | } 58 | 59 | /** 60 | * Goes through the process of assigning badges for users who are at different stages of winning 61 | * games. Badges for first win, 5, 10, 25. 62 | * 63 | * @param users The users who have one the recent game. 64 | */ 65 | public static async assignGameWinningBadgesForUsers(users: User[]) { 66 | const userGameStatsRepository = getCustomRepository(UserGameStatsRepository); 67 | 68 | const userStats = await userGameStatsRepository.find({ 69 | relations: ['user'], 70 | where: { 71 | user: In(users.map((e) => e.id)), 72 | }, 73 | }); 74 | 75 | const winBadges: { [index: string]: (user: User) => Promise } = { 76 | HOT_STREAK: (user: User) => this.awardBadgeToUserById(user, BADGES.WIN_3_IN_ROW), 77 | 5: (user: User) => this.awardBadgeToUserById(user, BADGES.WIN_5_GAMES), 78 | 10: (user: User) => this.awardBadgeToUserById(user, BADGES.WIN_10_GAMES), 79 | 25: (user: User) => this.awardBadgeToUserById(user, BADGES.WIN_25_GAMES), 80 | }; 81 | 82 | const badgesBeingAwarded: Array> = []; 83 | 84 | for (const stats of userStats) { 85 | if (stats.wins >= 1 && stats.loses === 0) { 86 | badgesBeingAwarded.push(this.awardBadgeToUserById(stats.user, BADGES.WIN_FIRST_GAME)); 87 | } 88 | 89 | const badge = winBadges[stats.wins]; 90 | 91 | // If the user has met any of the win related badge requirements, go and 92 | // distribute that badge to the user. 93 | if (!_.isNil(badge)) badgesBeingAwarded.push(badge(stats.user)); 94 | 95 | // If the user is on a win streak, then go and award them the hot streak badge 96 | if (stats.winStreak === 3) badgesBeingAwarded.push(winBadges['HOT_STREAK'](stats.user)); 97 | } 98 | 99 | return Promise.all(badgesBeingAwarded); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /app/services/connection.service.ts: -------------------------------------------------------------------------------- 1 | import { config } from '../config'; 2 | import { createConnection } from 'typeorm'; 3 | 4 | export const Connection = createConnection(config.databaseOptions); 5 | -------------------------------------------------------------------------------- /app/services/discord.service.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { stringify } from 'qs'; 3 | 4 | import logger from '../utils/logger'; 5 | 6 | export interface DiscordUser { 7 | id: string; 8 | username: string; 9 | } 10 | 11 | export class DiscordService { 12 | public static async accessTokenForCode(code: string): Promise { 13 | const tokenEndpoint = 'https://discordapp.com/api/oauth2/token'; 14 | 15 | const params = { 16 | client_id: process.env.DISCORD_CLIENT, 17 | client_secret: process.env.DISCORD_SECRET, 18 | code, 19 | grant_type: 'authorization_code', 20 | redirect_uri: `${process.env.API_URL}/oauth/discord`, 21 | scope: 'identify', 22 | }; 23 | 24 | try { 25 | const response = await axios({ 26 | method: 'post', 27 | url: tokenEndpoint, 28 | data: stringify(params), 29 | headers: { 30 | 'content-type': 'application/x-www-form-urlencoded', 31 | }, 32 | }); 33 | 34 | return response.data.access_token; 35 | } catch (error) { 36 | logger.error(`error performing discord lookup, ${error}`); 37 | return null; 38 | } 39 | } 40 | 41 | public static async discordUserForToken(token: string): Promise { 42 | const userEndpoint = 'https://discordapp.com/api/users/@me'; 43 | 44 | try { 45 | const response = await axios.get(userEndpoint, { 46 | headers: { 47 | Authorization: `Bearer ${token}`, 48 | }, 49 | }); 50 | 51 | const { id, username } = response.data; 52 | 53 | return { id: id as string, username: username as string }; 54 | } catch (e) { 55 | return null; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/services/ranking.service.ts: -------------------------------------------------------------------------------- 1 | import { getCustomRepository } from 'typeorm'; 2 | import { EXPERIENCE } from '../constants'; 3 | 4 | import User from '../models/user.model'; 5 | 6 | import UserStatisticsRepository from '../repository/userStatistics.repository'; 7 | 8 | export default class RankingService { 9 | /** 10 | * Increase the total amount of experience for a given list of users for winning a game. 11 | * @param users The users who will be gaining the amount of experience. 12 | */ 13 | public static async assignWinningExperienceToUsers(users: User[]) { 14 | const userStatisticsRepository = getCustomRepository(UserStatisticsRepository); 15 | await userStatisticsRepository.increaseExperienceForUsers(EXPERIENCE.GAME_WIN, users); 16 | } 17 | 18 | /** 19 | * Decrease the total amount of experience for a given list of users for losing a game. 20 | * @param users The users who will be losing the amount of experience. 21 | */ 22 | public static async assignLosingExperienceToUsers(users: User[]) { 23 | const userStatisticsRepository = getCustomRepository(UserStatisticsRepository); 24 | await userStatisticsRepository.decreaseExperienceForUsers(EXPERIENCE.GAME_LOST, users); 25 | } 26 | 27 | /** 28 | * Add experience for users who completed all the objectives. 29 | * @param users The users who completed all objectives. 30 | */ 31 | public static async assignObjectiveCompletionExperienceToUsers(users: User[]) { 32 | const userStatisticsRepository = getCustomRepository(UserStatisticsRepository); 33 | await userStatisticsRepository.decreaseExperienceForUsers(EXPERIENCE.ALL_OBJECTIVES, users); 34 | } 35 | 36 | /** 37 | * Assign all users that participating within Devwars a given amount of experience. 38 | * @param users The users who will be gaining the participation amount. 39 | */ 40 | public static async assignParticipationExperienceToUsers(users: User[]) { 41 | const userStatisticsRepository = getCustomRepository(UserStatisticsRepository); 42 | await userStatisticsRepository.increaseExperienceForUsers(EXPERIENCE.PARTICIPATION, users); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/services/reset.service.ts: -------------------------------------------------------------------------------- 1 | import User from '../models/user.model'; 2 | import { VerificationService } from './verification.service'; 3 | 4 | export class ResetService { 5 | public static async resetEmail(user: User, email: string) { 6 | user.email = email; 7 | await user.save(); 8 | 9 | await VerificationService.reset(user); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/services/server.service.ts: -------------------------------------------------------------------------------- 1 | import * as bodyParser from 'body-parser'; 2 | import * as cookieParser from 'cookie-parser'; 3 | import * as cors from 'cors'; 4 | import * as express from 'express'; 5 | import * as http from 'http'; 6 | import * as morgan from 'morgan'; 7 | 8 | import { config } from '../config'; 9 | import * as errorController from '../controllers/error.controller'; 10 | import * as Connection from './connection.service'; 11 | import logger from '../utils/logger'; 12 | import { Routes } from '../routes'; 13 | 14 | export default class ServerService { 15 | public static async ConnectToDatabase() { 16 | try { 17 | const connection = await Connection.Connection; 18 | await connection.query('select 1+1 as answer'); 19 | await connection.synchronize(); 20 | } catch (e) { 21 | logger.error(`Could not synchronize database, error=${e}`); 22 | } 23 | } 24 | 25 | private readonly app: express.Application; 26 | private readonly server: http.Server; 27 | 28 | constructor() { 29 | this.app = express(); 30 | this.server = http.createServer(this.app); 31 | } 32 | 33 | public App = (): express.Application => this.app; 34 | 35 | public async Start(): Promise { 36 | await ServerService.ConnectToDatabase(); 37 | this.ExpressConfiguration(); 38 | this.ConfigurationRouter(); 39 | return this.server; 40 | } 41 | 42 | private ExpressConfiguration(): void { 43 | this.app.use(bodyParser.urlencoded({ extended: true })); 44 | this.app.use(bodyParser.json({ limit: '1mb' })); 45 | this.app.use(cookieParser()); 46 | 47 | this.app.use((req, res, next): void => { 48 | res.header('Access-Control-Allow-Headers', 'X-Requested-With, Content-Type, Authorization'); 49 | res.header('Access-Control-Allow-Methods', 'GET,PUT,PATCH,POST,DELETE,OPTIONS'); 50 | next(); 51 | }); 52 | 53 | const routeLogging = morgan('combined', { 54 | stream: { 55 | write: (text: string) => { 56 | logger.verbose(text.replace(/\n$/, '')); 57 | }, 58 | }, 59 | }); 60 | 61 | this.app.use(routeLogging); 62 | this.app.use(cors(config.cors)); 63 | } 64 | 65 | private ConfigurationRouter(): void { 66 | for (const route of Routes) { 67 | this.app.use(route.path, route.handler); 68 | } 69 | 70 | this.app.use(errorController.handleError); 71 | this.app.use(errorController.handleMissing); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /app/services/twitch.service.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | import logger from '../utils/logger'; 4 | 5 | export interface TwitchUser { 6 | id: string; 7 | username: string; 8 | } 9 | 10 | export class TwitchService { 11 | public static async accessTokenForCode(code: string): Promise { 12 | let tokenEndpoint = 'https://id.twitch.tv/oauth2/token?'; 13 | 14 | const params: { 15 | [index: string]: string; 16 | } = { 17 | client_id: process.env.TWITCH_CLIENT, 18 | client_secret: process.env.TWITCH_SECRET, 19 | code, 20 | grant_type: 'authorization_code', 21 | redirect_uri: `${process.env.API_URL}/oauth/twitch`, 22 | }; 23 | 24 | for (const paramKey of Object.keys(params)) { 25 | tokenEndpoint += `${paramKey}=${params[paramKey]}&`; 26 | } 27 | 28 | try { 29 | const response = await axios({ 30 | method: 'post', 31 | url: tokenEndpoint.substring(0, tokenEndpoint.length - 1), 32 | }); 33 | 34 | return response.data.access_token; 35 | } catch (error) { 36 | logger.error(`error performing twitch lookup, ${error}`); 37 | return null; 38 | } 39 | } 40 | 41 | public static async twitchUserForToken(token: string): Promise { 42 | const userEndpoint = 'https://api.twitch.tv/helix/users'; 43 | 44 | try { 45 | const response = await axios.get(userEndpoint, { 46 | headers: { 47 | Authorization: `Bearer ${token}`, 48 | 'Client-ID': process.env.TWITCH_CLIENT, 49 | }, 50 | }); 51 | 52 | const { id, login: username } = response.data.data[0]; 53 | 54 | return { id: id as string, username: username as string }; 55 | } catch (e) { 56 | return null; 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/services/verification.service.ts: -------------------------------------------------------------------------------- 1 | import { getManager, getCustomRepository } from 'typeorm'; 2 | import { nanoid } from 'nanoid'; 3 | import EmailVerification from '../models/emailVerification.model'; 4 | import User, { UserRole } from '../models/user.model'; 5 | 6 | import logger from '../utils/logger'; 7 | import { sendWelcomeEmail } from './mail.service'; 8 | 9 | import EmailVerificationRepository from '../repository/emailVerification.repository'; 10 | 11 | export class VerificationService { 12 | /** 13 | * Generates a new random verification token that is stored in the database with the user 14 | * getting a verification link sent. Verification token will be removed once the user clicks the 15 | * validation link, inturn calling into the verify endpoint. 16 | * @param user The user who is getting their verification progress reset. 17 | */ 18 | public static async reset(user: User) { 19 | // If the given user is moderator or a admin, they should not be subject to updating user 20 | // role state. e.g only standard users have to go through email verification again. 21 | if (user.isStaff()) return; 22 | 23 | const emailRepository = getCustomRepository(EmailVerificationRepository); 24 | await emailRepository.removeForUser(user); 25 | 26 | user.role = UserRole.PENDING; 27 | 28 | const verification = new EmailVerification(); 29 | verification.token = nanoid(64); 30 | 31 | const verificationUrl = `${process.env.API_URL}/auth/verify?token=${verification.token}`; 32 | 33 | await getManager().transaction(async (transactionalEntityManager) => { 34 | verification.user = await transactionalEntityManager.save(user); 35 | await transactionalEntityManager.save(verification); 36 | }); 37 | 38 | // Log verification urls in production in case email service fails. 39 | if (process.env.NODE_ENV === 'production') { 40 | logger.info('USER REGISTRATION:' 41 | + ` username: ${user.username}` 42 | + ` email: ${user.email}` 43 | + ` verificationUrl: ${verificationUrl}` 44 | ); 45 | } 46 | 47 | await sendWelcomeEmail(user, verificationUrl); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/types/common.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Templates that are used in the game, these will be loaded before the game starts ready for the user to use. 3 | */ 4 | export interface GameEditorTemplates { 5 | // The template for the html editor that will be inserted before the game starts. 6 | html?: string; 7 | 8 | // The template for the css editor that will be inserted before the game starts. 9 | css?: string; 10 | 11 | // The template for the javascript editor that will be inserted before the game starts. 12 | js?: string; 13 | } 14 | 15 | /** 16 | * The game storage objective that contains all the information about the 17 | * possible objectives of the given game. Including the id, bonus state and 18 | * description that will be given to the users. 19 | */ 20 | export interface GameObjective { 21 | // The id of the given objective. 22 | id: number; 23 | 24 | // If the objective is the bonus objective, this is awarded more score 25 | // compared to other objectives. Commonly unlocked after completing all 26 | // other. 27 | isBonus: boolean; 28 | 29 | // The given description of the objective. This is the objective that 30 | // will be shown to the user, e.g what should be done to be awarded the 31 | // objective. 32 | description: string; 33 | } 34 | -------------------------------------------------------------------------------- /app/types/game.ts: -------------------------------------------------------------------------------- 1 | import { GameEditorTemplates, GameObjective } from './common'; 2 | 3 | /** 4 | * The given game storage object is a json blob and is not another table. It 5 | * will contain additional core information about the running game and after the 6 | * game has been completed. This includes players, editor assignments, results. 7 | */ 8 | export interface GameStorage { 9 | // The template html code that will be used to help get the game up and 10 | // running faster. 11 | templates?: GameEditorTemplates; 12 | 13 | // The objectives of the given game, what the teams must do to be win. 14 | // index is the id of the objective. 15 | objectives?: { [index: string]: GameObjective }; 16 | 17 | // The object of the editors that is related to the given game. including 18 | // which users have been to assigned to which editor. 19 | editors?: { [index: string]: GameStorageEditor }; 20 | 21 | // any related meta information about hte game, typically containing all the 22 | // related results and scores of the finished game. 23 | meta?: GameStorageMeta; 24 | } 25 | 26 | /** 27 | * The meta object of the given game, this includes the winning team, and the 28 | * scores/results of each team that competed. 29 | */ 30 | export interface GameStorageMeta { 31 | // The object of scores of the teams that played. the index is the id of the 32 | // given team/ 33 | teamScores: { [index: string]: GameStorageMetaTeamScore }; 34 | 35 | // The id of the winning team. 36 | winningTeam: number; 37 | 38 | // If the result of the game was a tie or not. 39 | tie: boolean; 40 | } 41 | 42 | /** 43 | * The scoring result of the given game per team, this will be used for 44 | * rendering results on the site. 45 | */ 46 | export interface GameStorageMetaTeamScore { 47 | // The id of the given team. 48 | id: number; 49 | 50 | // The status of each objective for the given name in a string format. e.g 51 | // has the given team completed or not completed the given objectives. 52 | objectives?: { 53 | [index: string]: 'complete' | 'incomplete'; 54 | }; 55 | 56 | // The number of bets for the given team. 57 | bets: number; 58 | 59 | // The score the team got from the ui voting stage. 60 | ui: number; 61 | 62 | // The score the team got from the ux voting stage. 63 | ux: number; 64 | } 65 | 66 | /** 67 | * The editor object related to a given game, this will include who, what team 68 | * and what language the given editor will be using. 69 | */ 70 | export interface GameStorageEditor { 71 | // The id of the given editor. 72 | id: number; 73 | 74 | // The team the editor is associated with. 75 | team: number; 76 | 77 | // The id of the player that has been assigned to the game editor. 78 | player: number; 79 | 80 | // The language the given editor has been assigned. 81 | language: string; 82 | } 83 | -------------------------------------------------------------------------------- /app/types/mailgun.js/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'mailgun.js'; 2 | -------------------------------------------------------------------------------- /app/types/newGame.ts: -------------------------------------------------------------------------------- 1 | import { ArchiveGameRequest } from '../request/archiveGameRequest'; 2 | 3 | export interface GameStorage { 4 | raw?: ArchiveGameRequest; 5 | } 6 | -------------------------------------------------------------------------------- /app/types/process.d.ts: -------------------------------------------------------------------------------- 1 | export interface ProcessEnv { 2 | // ################################# 3 | // # Master Configuration Options # 4 | // ################################# 5 | NODE_ENV: 'development' | 'production' | 'test'; 6 | APP_PORT: string; 7 | 8 | // # `COOKIE_DOMAIN` Leave this blank unless you have a good understanding 9 | // # of a cookie domain and its functionality. Leaving blank will allow 10 | // # local development without any constraints. 11 | COOKIE_DOMAIN: string; 12 | API_KEY: string; 13 | 14 | DB_HOST: string; 15 | DB_PORT: string; 16 | DB_NAME: string; 17 | DB_USER: string; 18 | DB_PASS: string; 19 | 20 | FRONT_URL: string; 21 | API_URL: string; 22 | 23 | AUTH_SECRET: string; 24 | LOG_LEVEL: string; 25 | 26 | DISCORD_CLIENT: string; 27 | DISCORD_SECRET: string; 28 | 29 | TWITCH_CLIENT: string; 30 | TWITCH_SECRET: string; 31 | 32 | MAILGUN_KEY: string; 33 | 34 | AWS_ENDPOINT_URL: string; 35 | AWS_ACCESS_KEY: string; 36 | AWS_SECRET_KEY: string; 37 | AWS_BUCKET_NAME: string; 38 | 39 | // ################################# 40 | // # Testing Configuration Options # 41 | // ################################# 42 | TEST_DB_HOST: string; 43 | TEST_DB_PORT: string; 44 | TEST_DB_NAME: string; 45 | TEST_DB_USER: string; 46 | TEST_DB_PASS: string; 47 | // ... 48 | } 49 | -------------------------------------------------------------------------------- /app/utils/apiError.ts: -------------------------------------------------------------------------------- 1 | export default class ApiError extends Error { 2 | // The optional API code for sending back to the client (default: 500) 3 | public code: number; 4 | 5 | // If the error message should be logged and also logged down to disk. 6 | public log: boolean; 7 | 8 | // If the error stack should also be logged with the error message. 9 | public logStack: boolean; 10 | 11 | /** 12 | * Creates a new instance of the API error object. This is to be thrown when used with asyncErrorHandler. 13 | * 14 | * @param error.code The optional API code for sending back to the client (default: 500) 15 | * @param error.error The optional API error message, interchangeable with error.message (this takes lead). 16 | * @param error.message The optional API error message, interchangeable with error.error (ignored if error set.) 17 | * @param error.log If the error message should be logged and also logged down to disk. 18 | * @param error.logStack If the error stack should also be logged with the error message. 19 | */ 20 | public constructor(error: { code?: number; error?: string; message?: string; log?: boolean; logStack?: boolean }) { 21 | super(error.error || error.message); 22 | 23 | this.code = error.code || 500; 24 | this.message = error.error || error.message || undefined; 25 | this.logStack = error.logStack || false; 26 | this.log = error.log || false; 27 | } 28 | 29 | /** 30 | * Simple override of string to ensure stack is logged if specified overwise default. 31 | */ 32 | public toString(): string { 33 | return this.logStack || process.env.NODE_ENV === 'development' ? this.stack : super.toString(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/utils/hash.ts: -------------------------------------------------------------------------------- 1 | import * as bcrypt from 'bcrypt'; 2 | 3 | const NUM_ROUNDS = 12; 4 | 5 | export async function hash(input: string): Promise { 6 | return bcrypt.hash(input, NUM_ROUNDS); 7 | } 8 | -------------------------------------------------------------------------------- /app/utils/logger.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as dotenv from 'dotenv'; 3 | import * as winston from 'winston'; 4 | import { join } from 'path'; 5 | 6 | import { pathExists, canAccessPath } from './helpers'; 7 | dotenv.config(); 8 | 9 | const { colorize, combine, timestamp, printf, splat } = winston.format; 10 | const logDirectory = join(__dirname, 'logs'); 11 | 12 | let consoleLoggingOnly = false; 13 | 14 | if (!pathExists(logDirectory) && canAccessPath(logDirectory, fs.constants.R_OK | fs.constants.W_OK)) { 15 | fs.mkdirSync(logDirectory); 16 | } 17 | 18 | if ( 19 | !canAccessPath(logDirectory, fs.constants.W_OK | fs.constants.R_OK) || 20 | !canAccessPath(join(logDirectory, 'error.txt'), fs.constants.W_OK | fs.constants.R_OK) 21 | ) { 22 | consoleLoggingOnly = true; 23 | } 24 | 25 | const logLevels = { 26 | levels: { 27 | error: 0, 28 | warn: 1, 29 | info: 2, 30 | verbose: 3, 31 | debug: 4, 32 | silly: 5, 33 | }, 34 | }; 35 | 36 | /** 37 | * Defines a custom format with winston print-f, used for formatting 38 | * with a timestamp, level and message. Designed to also handle cases 39 | * in which a error stack/message is involved. 40 | */ 41 | const myFormat = printf((info: any) => { 42 | let message = `${info.timestamp} ${info.level}: `; 43 | 44 | if (info instanceof Error) { 45 | message += ` ${info.stack}`; 46 | } else if (info.message instanceof Object) { 47 | message += JSON.stringify(info.message); 48 | } else { 49 | message += info.message; 50 | } 51 | 52 | return message; 53 | }); 54 | 55 | /** 56 | * Creates a new logger that is exported, allows for logging directly 57 | * into the terminal and into two files, just errors and everything. 58 | */ 59 | const logger = winston.createLogger({ 60 | levels: logLevels.levels, 61 | format: combine(timestamp(), splat(), myFormat), 62 | transports: [ 63 | new winston.transports.Console({ 64 | format: combine(timestamp(), colorize(), splat(), myFormat), 65 | level: process.env.LOG_LEVEL || 'info', 66 | }), 67 | ], 68 | }); 69 | 70 | if (!consoleLoggingOnly) { 71 | logger.add(new winston.transports.File({ filename: './logs/error.log', level: 'warn', maxsize: 2e6, maxFiles: 3 })); 72 | 73 | logger.add( 74 | new winston.transports.File({ 75 | filename: './logs/all.log', 76 | level: process.env.LOG_LEVEL || 'info', 77 | maxsize: 2e6, 78 | maxFiles: 3, 79 | }) 80 | ); 81 | } 82 | 83 | if (consoleLoggingOnly) { 84 | logger.error(`Logger cannot read or write to directory ${join(__dirname, 'logs')}.`); 85 | logger.error('Logs will only be written to the console until the issue is resolved.'); 86 | } 87 | 88 | export default logger; 89 | -------------------------------------------------------------------------------- /cli/seed.emails.ts: -------------------------------------------------------------------------------- 1 | import { Connection as typeConnection } from 'typeorm'; 2 | import * as faker from 'faker'; 3 | 4 | import { Connection } from '../app/services/connection.service'; 5 | import logger from '../app/utils/logger'; 6 | import User from '../app/models/user.model'; 7 | 8 | let connection: typeConnection; 9 | 10 | const updateEmailAddress = async (): Promise => { 11 | const users = await User.find(); 12 | 13 | for (const user of users) { 14 | const profile = faker.helpers.createCard(); 15 | user.email = `${profile.username}.${profile.email}`; 16 | await user.save(); 17 | } 18 | }; 19 | 20 | (async (): Promise => { 21 | connection = await Connection; 22 | 23 | logger.info('Updating user emails'); 24 | await updateEmailAddress(); 25 | 26 | logger.info('Seeding complete'); 27 | await connection.close(); 28 | })(); 29 | -------------------------------------------------------------------------------- /cli/seed.passwords.ts: -------------------------------------------------------------------------------- 1 | import { Connection as typeConnection } from 'typeorm'; 2 | 3 | import { Connection } from '../app/services/connection.service'; 4 | import { hash } from '../app/utils/hash'; 5 | import logger from '../app/utils/logger'; 6 | import User from '../app/models/user.model'; 7 | 8 | let connection: typeConnection; 9 | 10 | const updateUserPasswords = async (): Promise => { 11 | const users = await User.find(); 12 | 13 | const password = await hash('secret'); 14 | 15 | let count = 0; 16 | 17 | for (const user of users) { 18 | count += 1; 19 | 20 | process.stdout.cursorTo(0); 21 | process.stdout.write(`updating ${count}/${users.length} users`); 22 | 23 | user.password = password; 24 | await user.save(); 25 | } 26 | 27 | process.stdout.write('\n'); 28 | }; 29 | 30 | (async (): Promise => { 31 | connection = await Connection; 32 | 33 | logger.info('Updating user passwords'); 34 | await updateUserPasswords(); 35 | 36 | logger.info('Seeding complete'); 37 | await connection.close(); 38 | })(); 39 | -------------------------------------------------------------------------------- /cli/seeder.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | import * as faker from 'faker'; 3 | import * as typeorm from 'typeorm'; 4 | 5 | import GameApplicationSeeding from '../app/seeding/gameApplication.seeding'; 6 | import ActivitySeeding from '../app/seeding/activity.seeding'; 7 | import GameSeeding from '../app/seeding/game.seeding'; 8 | import UserSeeding from '../app/seeding/user.seeding'; 9 | import RankSeeding from '../app/seeding/rank.seeding'; 10 | 11 | import { Connection } from '../app/services/connection.service'; 12 | import User, { UserRole } from '../app/models/user.model'; 13 | import logger from '../app/utils/logger'; 14 | 15 | import UserRepository from '../app/repository/user.repository'; 16 | 17 | import GameRepository from '../app/repository/game.repository'; 18 | import { GameStatus } from '../app/models/game.model'; 19 | import { BadgeSeeding } from '../app/seeding'; 20 | import Badge from '../app/models/badge.model'; 21 | import UserBadges from '../app/models/userBadges.model'; 22 | 23 | let connection: typeorm.Connection; 24 | 25 | const players: User[] = []; 26 | 27 | const generateConstantUsers = async (badges: Badge[]): Promise => { 28 | for (const role of ['admin', 'moderator', 'user']) { 29 | const userRole: UserRole = (UserRole as any)[role.toUpperCase()]; 30 | const user = await UserSeeding.withComponents(`test${role}`, null, userRole).save(); 31 | 32 | const userBadges = _.map(_.sampleSize(badges, 3), (b) => new UserBadges(user, b).save()); 33 | await Promise.all(userBadges); 34 | 35 | } 36 | }; 37 | 38 | const generateBadges = async (): Promise => { 39 | const badges = BadgeSeeding.default(); 40 | 41 | for (const badge of badges) { 42 | await badge.save(); 43 | } 44 | 45 | return badges; 46 | }; 47 | 48 | const generateBasicUsers = async (badges: Badge[]): Promise => { 49 | await generateConstantUsers(badges); 50 | 51 | for (let i = 4; i <= 100; i++) { 52 | const user = await UserSeeding.withComponents().save(); 53 | 54 | for (let j = 1; j <= 25; j++) { 55 | const activity = ActivitySeeding.withUser(user); 56 | await activity.save(); 57 | } 58 | 59 | const userBadges = _.map(_.sampleSize(badges, 3), (b) => new UserBadges(user, b).save()); 60 | await Promise.all(userBadges); 61 | 62 | players.push(user); 63 | } 64 | }; 65 | 66 | const generateGames = async (): Promise => { 67 | for (let i = 1; i <= 150; i++) { 68 | const gamePlayers = players.slice(i % players.length, (i + 6) % players.length); 69 | const game = (await GameSeeding.default().common(gamePlayers)) 70 | .withStatus(GameStatus.ENDED) 71 | .withSeason(faker.helpers.randomize([1, 2, 3])); 72 | await game.save(); 73 | } 74 | }; 75 | 76 | const generateApplications = async (): Promise => { 77 | const gameRepository = typeorm.getCustomRepository(GameRepository); 78 | const userRepository = typeorm.getCustomRepository(UserRepository); 79 | 80 | for (let i = 1; i <= 25; i++) { 81 | const game = await gameRepository.findOne(i); 82 | const user = await userRepository.findOne(i); 83 | 84 | const application = GameApplicationSeeding.withGameAndUser(game, user); 85 | await connection.manager.save(application); 86 | } 87 | }; 88 | 89 | /** 90 | * Generate all the core ranks for the application. 91 | */ 92 | const generateRanks = async (): Promise => { 93 | const ranks = RankSeeding.default(); 94 | 95 | for (const rank of ranks) { 96 | await rank.save(); 97 | } 98 | }; 99 | 100 | (async (): Promise => { 101 | connection = await Connection; 102 | 103 | logger.info('Seeding database'); 104 | logger.info('Synchronizing database, dropTablesBeforeSync = true'); 105 | await connection.synchronize(true); 106 | 107 | logger.info('Generating badges'); 108 | const badges = await generateBadges(); 109 | 110 | logger.info('Generating basic users'); 111 | await generateBasicUsers(badges); 112 | 113 | logger.info('Generating games'); 114 | await generateGames(); 115 | 116 | logger.info('Generating applications'); 117 | await generateApplications(); 118 | 119 | logger.info('Generate Ranks'); 120 | await generateRanks(); 121 | 122 | // logger.info('Seeding complete'); 123 | await connection.close(); 124 | })(); 125 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "devwars-api", 3 | "version": "0.2.2", 4 | "author": "DevWars, LLC", 5 | "license": "MIT", 6 | "description": "Official Node API for DevWars", 7 | "scripts": { 8 | "start": "cross-env NODE_ENV=production node ./dist/index.js", 9 | "dev": "ts-node-dev --transpile-only --respawn --no-notify ./app/index.ts", 10 | "test:nyc": "cross-env NODE_ENV=test nyc --reporter=html --reporter=text-summary mocha", 11 | "test": "cross-env NODE_ENV=test nyc --reporter=text-summary mocha", 12 | "test:break": "cross-env NODE_ENV=test mocha --inspect-brk", 13 | "build": "npm run clean && tsc", 14 | "clean": "rimraf dist", 15 | "docs:build": "apidoc -i ./app -o ./docs", 16 | "docs:production": "apidoc -i ./app -o ./dist/docs", 17 | "docs": "apidoc -i ./app -o ./docs & http-server -p 8081 docs", 18 | "seed": "node -r ts-node/register ./cli/Seeder.ts", 19 | "seed:password": "node -r ts-node/register ./cli/seed.passwords.ts", 20 | "lint": "eslint . --ext .js,.jsx,.ts,.tsx", 21 | "lint:fix": "eslint . --fix --ext .js,.jsx,.ts,.tsx", 22 | "release": "standard-version" 23 | }, 24 | "husky": { 25 | "hooks": { 26 | "pre-commit": "npm run lint && npm test" 27 | } 28 | }, 29 | "nyc": { 30 | "include": [ 31 | "app/**/*.ts" 32 | ], 33 | "extension": [ 34 | ".ts", 35 | ".tsx" 36 | ], 37 | "exclude": [ 38 | "**/*.d.ts" 39 | ], 40 | "reporter": [ 41 | "html" 42 | ], 43 | "all": true 44 | }, 45 | "dependencies": { 46 | "@hapi/joi": "^17.1.1", 47 | "aws-sdk": "^2.929.0", 48 | "axios": "^0.21.1", 49 | "bcrypt": "^5.0.1", 50 | "body-parser": "^1.18.2", 51 | "cookie-parser": "^1.4.5", 52 | "cors": "^2.8.4", 53 | "date-fns": "^2.22.1", 54 | "dotenv": "^10.0.0", 55 | "express": "^4.17.1", 56 | "faker": "^5.5.3", 57 | "jsonwebtoken": "^8.2.0", 58 | "lodash": "^4.17.21", 59 | "mailgun-js": "^0.22.0", 60 | "mime": "^2.5.2", 61 | "mjml": "^4.9.3", 62 | "morgan": "^1.10.0", 63 | "multer": "^1.4.2", 64 | "nanoid": "^3.1.23", 65 | "pg": "^8.6.0", 66 | "qs": "^6.10.1", 67 | "superagent": "^6.1.0", 68 | "token-extractor": "^0.1.6", 69 | "typeorm": "^0.2.34", 70 | "winston": "^3.3.3" 71 | }, 72 | "devDependencies": { 73 | "@babel/code-frame": "^7.14.5", 74 | "@types/bcrypt": "^5.0.0", 75 | "@types/chai": "^4.2.18", 76 | "@types/cookie-parser": "^1.4.2", 77 | "@types/cors": "^2.8.10", 78 | "@types/express": "^4.17.12", 79 | "@types/faker": "^5.5.6", 80 | "@types/hapi__joi": "^17.1.6", 81 | "@types/jsonwebtoken": "^8.5.1", 82 | "@types/lodash": "^4.14.170", 83 | "@types/mailgun-js": "^0.22.11", 84 | "@types/mime": "^2.0.3", 85 | "@types/mjml": "^4.7.0", 86 | "@types/mocha": "^8.2.2", 87 | "@types/morgan": "^1.9.2", 88 | "@types/multer": "^1.4.5", 89 | "@types/node": "^15.12.2", 90 | "@types/qs": "^6.9.6", 91 | "@types/superagent": "^4.1.11", 92 | "@types/supertest": "^2.0.11", 93 | "@typescript-eslint/eslint-plugin": "^4.27.0", 94 | "@typescript-eslint/parser": "^4.27.0", 95 | "apidoc": "^0.28.1", 96 | "chai": "^4.3.4", 97 | "copyfiles": "^2.4.1", 98 | "cross-env": "^7.0.3", 99 | "eslint": "^7.28.0", 100 | "eslint-config-prettier": "^8.3.0", 101 | "http-server": "^0.12.3", 102 | "husky": "^6.0.0", 103 | "mocha": "^9.0.0", 104 | "nyc": "^15.1.0", 105 | "standard-version": "^9.3.0", 106 | "supertest": "^6.1.3", 107 | "ts-node": "^10.0.0", 108 | "ts-node-dev": "^1.1.6", 109 | "typescript": "^4.3.2" 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /templates/connect-discord.mjml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Connect your Discord account 8 | 9 | Hello __USERNAME__, we recently added a new requirement to link your Discord account to your DevWars account in order to compete in future games. 10 | 11 | 12 | Since Discord is an important part of DevWars, connecting your Discord will help us automate more of the sign up process to be faster and easier to join games. 13 | 14 | 15 | 16 | 17 | 18 | 19 | LINK MY DISCORD 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /templates/contact-us.mjml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | DevWars Contact Us! 8 | 9 | Thank you for contacting us __NAME__! We received your email, you will 10 | hear back from us soon. Below is a copy of the contact-us response that was sent. 11 | 12 | 13 | 14 | 15 | 16 | 17 | Name: __NAME__ 18 | Email: __EMAIL__ 19 | 20 | __MESSAGE__ 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /templates/game-application-resign.mjml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | DevWars Game Application Update! 8 | 9 | Hi __USERNAME__! We received your request for resigning from the 10 | application for the __GAME_MODE__ game mode. You will no longer be taken 11 | into consideration during the selection process. Be sure to keep an eye out for DevWars 12 | events and games in the future! 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /templates/game-application.mjml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | DevWars Game Application! 8 | 9 | Thank you for applying __USERNAME__! We received your application for the __GAME_MODE__ game mode. 10 | The game will be taking place on __GAME_TIME__. Be sure to check the rules and information about this mode and keep an eye out for DevWars going live! 11 | 12 | 13 | 14 | 15 | 16 | 17 | GAME MODES 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /templates/layouts/footer.mjml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /templates/layouts/header.mjml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /templates/layouts/simple-footer.mjml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | © DEVWARS.TV - 2019 12 | Join our Discord! 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /templates/layouts/unsubscribe.mjml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

8 | © 2019 All Rights Reserved. 9 |

10 | Unsubscribe 11 |
12 |
13 |
14 |
15 |
-------------------------------------------------------------------------------- /templates/linked-account-disconnect.mjml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | __PROVIDER__ Disconnected! 8 | 9 | Hello __USERNAME__, your account with __PROVIDER__ has now been disconnected from DevWars. You 10 | can view your third-party linked accounts by viewing the connection tab under your 11 | settings page. 12 | 13 | 14 | 15 | 16 | 17 | 18 | View Connections 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /templates/linked-account.mjml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | __PROVIDER__ Linked! 8 | 9 | Hello __USERNAME__, your account with __PROVIDER__ has now been linked with DevWars. You 10 | can view your third-party linked accounts by viewing the connection tab under your 11 | settings page. 12 | 13 | 14 | 15 | 16 | 17 | 18 | View Connections 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /templates/new-contact.mjml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | New Contact 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | Field 16 | Response 17 | 18 | 19 | Name 20 | __NAME__ 21 | 22 | 23 | Email 24 | __EMAIL__ 25 | 26 | 27 | Contact Message: 28 | __MESSAGE__ 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /templates/reset-password.mjml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Password Reset 8 | 9 | We received a password change request for your DevWars account: __USERNAME__. To change your password, click the link below: 10 | 11 | 12 | 13 | 14 | 15 | 16 | RESET PASSWORD 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /templates/welcome.mjml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Welcome __USERNAME__! 8 | Thanks for signing up on DevWars. To start hacking, please verify your email address by clicking the link below. 9 | 10 | 11 | 12 | 13 | 14 | CONFIRM EMAIL 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /test/contactUs.test.ts: -------------------------------------------------------------------------------- 1 | import * as supertest from 'supertest'; 2 | 3 | import { Connection } from '../app/services/connection.service'; 4 | import ServerService from '../app/services/server.service'; 5 | import { 6 | CONTACT_US_MESSAGE_MAX, 7 | CONTACT_US_MESSAGE_MIN, 8 | CONTACT_US_NAME_MAX, 9 | CONTACT_US_NAME_MIN, 10 | } from '../app/constants'; 11 | 12 | const server: ServerService = new ServerService(); 13 | let agent: any; 14 | 15 | describe('Contact Us ', () => { 16 | const validPost = { 17 | name: 'John Doe', 18 | email: 'example@example.com', 19 | message: 'This is a test message that is valid.', 20 | }; 21 | 22 | before(async () => { 23 | await server.Start(); 24 | await (await Connection).synchronize(true); 25 | }); 26 | 27 | beforeEach(() => { 28 | agent = supertest.agent(server.App()); 29 | }); 30 | 31 | describe('POST - /contact - Creating a new contact us request.', () => { 32 | it('Should pass if name, email and message are valid', async () => { 33 | await agent.post('/contact').send(validPost).expect(200); 34 | }); 35 | 36 | it('Should fail if no name, email or message is provided', async () => { 37 | const { name, email, message } = { ...validPost }; 38 | 39 | for (const test of [{ email, message }, { name, message }, { name, email }, {}]) { 40 | await agent.post('/contact').send(test).expect(400); 41 | } 42 | }); 43 | 44 | it('Should fail the name is above or below the constraints', async () => { 45 | let name = ''; 46 | while (name.length < CONTACT_US_NAME_MIN - 1) name += 'A'; 47 | 48 | await agent 49 | .post('/contact') 50 | .send({ name, email: validPost.email, message: validPost.message }) 51 | .expect(400, { 52 | error: 'name length must be at least 3 characters long, please check your content and try again.', 53 | }); 54 | 55 | while (name.length < CONTACT_US_NAME_MAX + 1) name += 'A'; 56 | 57 | await agent 58 | .post('/contact') 59 | .send({ name, email: validPost.email, message: validPost.message }) 60 | .expect(400, { 61 | error: 62 | 'name length must be less than or equal to 64 characters long, ' + 63 | 'please check your content and try again.', 64 | }); 65 | }); 66 | 67 | it('Should fail the email is not a valid email.', async () => { 68 | const { name, message } = { ...validPost }; 69 | 70 | const invalidEmail = 'email must be a valid email, please check your content and try again.'; 71 | const emptyEmail = 'email is not allowed to be empty, please check your content and try again.'; 72 | 73 | for (const email of ['test.com', 'testing', '@test.com']) { 74 | await agent.post('/contact').send({ name, email, message }).expect(400, { error: invalidEmail }); 75 | } 76 | 77 | await agent.post('/contact').send({ name, email: '', message }).expect(400, { error: emptyEmail }); 78 | }); 79 | 80 | it('Should fail the message is above or below the constraints', async () => { 81 | let message = ''; 82 | while (message.length < CONTACT_US_MESSAGE_MIN - 1) message += 'A'; 83 | 84 | await agent.post('/contact').send({ name: validPost.name, email: validPost.email, message }).expect(400, { 85 | error: 'message length must be at least 24 characters long, please check your content and try again.', 86 | }); 87 | 88 | while (message.length < CONTACT_US_MESSAGE_MAX + 1) message += 'A'; 89 | 90 | await agent 91 | .post('/contact') 92 | .send({ message, email: validPost.email, name: validPost.name }) 93 | .expect(400, { 94 | error: 95 | 'message length must be less than or equal to 500 characters long, ' + 96 | 'please check your content and try again.', 97 | }); 98 | }); 99 | }); 100 | }); 101 | -------------------------------------------------------------------------------- /test/health.test.ts: -------------------------------------------------------------------------------- 1 | import * as supertest from 'supertest'; 2 | import * as chai from 'chai'; 3 | import * as _ from 'lodash'; 4 | 5 | import ServerService from '../app/services/server.service'; 6 | import { UserRole } from '../app/models/user.model'; 7 | import { UserSeeding } from '../app/seeding'; 8 | import { cookieForUser } from '../app/utils/helpers'; 9 | 10 | const server: ServerService = new ServerService(); 11 | let agent: supertest.SuperTest = null; 12 | 13 | describe('Health', () => { 14 | before(async () => { 15 | await server.Start(); 16 | }); 17 | 18 | beforeEach(() => { 19 | agent = supertest.agent(server.App()); 20 | }); 21 | 22 | describe('GET - /health - Get the related health information of the server', async () => { 23 | it('should return healthy and the current version number', async () => { 24 | // eslint-disable-next-line @typescript-eslint/no-var-requires 25 | const packageJson = require('../package'); 26 | await agent.get('/health').expect(200, { 27 | status: 'Healthy', 28 | version: packageJson.version, 29 | }); 30 | }); 31 | }); 32 | 33 | describe('GET - /health/logs - Get the related server logs', async () => { 34 | it('should fail if you are not a moderator or higher', async () => { 35 | for (const role of [UserRole.PENDING, UserRole.USER]) { 36 | const user = await UserSeeding.withRole(role).save(); 37 | 38 | await agent 39 | .get('/health/logs') 40 | .set('Cookie', await cookieForUser(user)) 41 | .expect(403, { error: "Unauthorized, you currently don't meet the minimal requirement." }); 42 | } 43 | }); 44 | 45 | it('should not fail if you are a moderator or higher', async () => { 46 | for (const role of [UserRole.MODERATOR, UserRole.ADMIN]) { 47 | const user = await UserSeeding.withRole(role).save(); 48 | 49 | const response = await agent 50 | .get('/health/logs') 51 | .set('Cookie', await cookieForUser(user)) 52 | .expect(200); 53 | 54 | chai.expect(_.isArray(response.body.logs)).to.be.eq(true); 55 | } 56 | }); 57 | }); 58 | 59 | describe('GET - /health/logs/error - Get the related server error logs', async () => { 60 | it('should fail if you are not a moderator or higher', async () => { 61 | for (const role of [UserRole.PENDING, UserRole.USER]) { 62 | const user = await UserSeeding.withRole(role).save(); 63 | 64 | await agent 65 | .get('/health/logs/error') 66 | .set('Cookie', await cookieForUser(user)) 67 | .expect(403, { error: "Unauthorized, you currently don't meet the minimal requirement." }); 68 | } 69 | }); 70 | 71 | it('should not fail if you are a moderator or higher', async () => { 72 | for (const role of [UserRole.MODERATOR, UserRole.ADMIN]) { 73 | const user = await UserSeeding.withRole(role).save(); 74 | 75 | const response = await agent 76 | .get('/health/logs/error') 77 | .set('Cookie', await cookieForUser(user)) 78 | .expect(200); 79 | 80 | chai.expect(_.isArray(response.body.logs)).to.be.eq(true); 81 | } 82 | }); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /test/user.activity.test.ts: -------------------------------------------------------------------------------- 1 | import * as supertest from 'supertest'; 2 | import * as chai from 'chai'; 3 | 4 | import { Connection } from '../app/services/connection.service'; 5 | import ServerService from '../app/services/server.service'; 6 | import { UserSeeding, ActivitySeeding } from '../app/seeding'; 7 | import { cookieForUser } from '../app/utils/helpers'; 8 | import User, { UserRole } from '../app/models/user.model'; 9 | 10 | const server: ServerService = new ServerService(); 11 | let agent: any; 12 | 13 | describe('User Activity', () => { 14 | let user: User; 15 | 16 | before(async () => { 17 | await server.Start(); 18 | await (await Connection).synchronize(true); 19 | }); 20 | 21 | beforeEach(async () => { 22 | user = await UserSeeding.default().save(); 23 | }); 24 | 25 | beforeEach(() => { 26 | agent = supertest.agent(server.App()); 27 | }); 28 | 29 | describe('GET - /users/:user/activities - Get the related users activities.', () => { 30 | it('Should return no activities when the given user has none', async () => { 31 | await agent 32 | .get(`/users/${user.id}/activities`) 33 | .set('cookie', await cookieForUser(user)) 34 | .expect(200, []); 35 | }); 36 | 37 | it('Should return the users activities if the user has them', async () => { 38 | await ActivitySeeding.withUser(user).save(); 39 | await ActivitySeeding.withUser(user).save(); 40 | await ActivitySeeding.withUser(user).save(); 41 | 42 | const response = await agent 43 | .get(`/users/${user.id}/activities`) 44 | .set('cookie', await cookieForUser(user)) 45 | .send(); 46 | 47 | chai.expect(response.status).to.be.equal(200); 48 | chai.expect(response.body.length).to.be.equal(3); 49 | }); 50 | 51 | it('Should pass if you are not the owning user and a admin or moderator', async () => { 52 | for (const role of [UserRole.ADMIN, UserRole.MODERATOR]) { 53 | const notOwning = await UserSeeding.withRole(role).save(); 54 | 55 | const response = await agent 56 | .get(`/users/${user.id}/activities`) 57 | .set('cookie', await cookieForUser(notOwning)) 58 | .send(); 59 | 60 | chai.expect(response.status).to.be.equal(200); 61 | } 62 | }); 63 | }); 64 | 65 | describe('GET - /users/:user/activities/:activity - Get the related users activity.', () => { 66 | it('Should fail if the user has no activity for that id', async () => { 67 | const response = await agent 68 | .get(`/users/${user.id}/activities/99`) 69 | .set('cookie', await cookieForUser(user)) 70 | .send(); 71 | 72 | chai.expect(response.status).to.be.equal(404); 73 | chai.expect(response.body.error).to.be.equal('The activity does not exist by the provided id.'); 74 | }); 75 | 76 | it('Should fail if the activity is not a number', async () => { 77 | const response = await agent 78 | .get(`/users/${user.id}/activities/null`) 79 | .set('cookie', await cookieForUser(user)) 80 | .send(); 81 | 82 | chai.expect(response.status).to.be.equal(400); 83 | chai.expect(response.body.error).to.be.equal('Invalid activity id was provided.'); 84 | }); 85 | 86 | it('Should return the users activity if the user has one', async () => { 87 | const activity = await ActivitySeeding.withUser(user).save(); 88 | 89 | const response = await agent 90 | .get(`/users/${user.id}/activities/${activity.id}`) 91 | .set('cookie', await cookieForUser(user)) 92 | .send(); 93 | 94 | chai.expect(response.status).to.be.equal(200); 95 | chai.expect(response.body.id).to.be.equal(activity.id); 96 | chai.expect(response.body.coins).to.be.equal(activity.coins); 97 | chai.expect(response.body.xp).to.be.equal(activity.xp); 98 | chai.expect(response.body.description).to.be.equal(activity.description); 99 | }); 100 | 101 | it('Should pass if you are not the owning user and a admin or moderator', async () => { 102 | for (const role of [UserRole.ADMIN, UserRole.MODERATOR]) { 103 | const activity = await ActivitySeeding.withUser(user).save(); 104 | const notOwning = await UserSeeding.withRole(role).save(); 105 | 106 | const response = await agent 107 | .get(`/users/${user.id}/activities/${activity.id}`) 108 | .set('cookie', await cookieForUser(notOwning)) 109 | .send(); 110 | 111 | chai.expect(response.status).to.be.equal(200); 112 | } 113 | }); 114 | }); 115 | }); 116 | -------------------------------------------------------------------------------- /test/user.statistics.test.ts: -------------------------------------------------------------------------------- 1 | import * as supertest from 'supertest'; 2 | import * as chai from 'chai'; 3 | 4 | import { UserSeeding, UserStatsSeeding, UserGameStatsSeeding } from '../app/seeding'; 5 | import { Connection } from '../app/services/connection.service'; 6 | import ServerService from '../app/services/server.service'; 7 | import User, { UserRole } from '../app/models/user.model'; 8 | import { cookieForUser } from '../app/utils/helpers'; 9 | import UserStats from '../app/models/userStats.model'; 10 | import UserGameStats from '../app/models/userGameStats.model'; 11 | 12 | const server: ServerService = new ServerService(); 13 | let agent: any; 14 | 15 | describe('User Statistics', () => { 16 | let user: User; 17 | let stats: UserStats; 18 | let gameStats: UserGameStats; 19 | 20 | before(async () => { 21 | await server.Start(); 22 | await (await Connection).synchronize(true); 23 | }); 24 | 25 | beforeEach(async () => { 26 | user = await UserSeeding.withRole(UserRole.USER).save(); 27 | stats = await UserStatsSeeding.withUser(user).save(); 28 | gameStats = await UserGameStatsSeeding.withUser(user).save(); 29 | }); 30 | 31 | beforeEach(() => { 32 | agent = supertest.agent(server.App()); 33 | }); 34 | 35 | describe('GET - /users/:user/statistics - Get the related users statistics.', () => { 36 | it('Should return the users statistics if the user has them', async () => { 37 | const response = await agent 38 | .get(`/users/${user.id}/statistics`) 39 | .set('cookie', await cookieForUser(user)) 40 | .send(); 41 | 42 | chai.expect(response.status).to.be.equal(200); 43 | chai.expect(response.body.coins).to.be.equal(stats.coins); 44 | chai.expect(response.body.xp).to.be.equal(stats.xp); 45 | chai.expect(response.body.game.loses).to.be.equal(gameStats.loses); 46 | chai.expect(response.body.game.wins).to.be.equal(gameStats.wins); 47 | }); 48 | 49 | it('Should allow if you are not the owning user and not a admin or moderator', async () => { 50 | const notOwning = await UserSeeding.withRole(UserRole.USER).save(); 51 | 52 | const response = await agent 53 | .get(`/users/${user.id}/statistics`) 54 | .set('cookie', await cookieForUser(notOwning)) 55 | .send(); 56 | 57 | chai.expect(response.status).to.be.equal(200); 58 | }); 59 | }); 60 | 61 | describe('GET - /users/:user/statistics/game - Get the users game statistics.', () => { 62 | it('Should return the users game statistics if the user has them', async () => { 63 | const response = await agent 64 | .get(`/users/${user.id}/statistics/game`) 65 | .set('cookie', await cookieForUser(user)) 66 | .send(); 67 | 68 | chai.expect(response.status).to.be.equal(200); 69 | chai.expect(response.body.loses).to.be.equal(gameStats.loses); 70 | chai.expect(response.body.wins).to.be.equal(gameStats.wins); 71 | }); 72 | 73 | it('Should pass if you are not the owning user and a admin or moderator', async () => { 74 | for (const role of [UserRole.ADMIN, UserRole.MODERATOR]) { 75 | const notOwning = await UserSeeding.withRole(role).save(); 76 | 77 | const response = await agent 78 | .get(`/users/${user.id}/statistics/game`) 79 | .set('cookie', await cookieForUser(notOwning)) 80 | .send(); 81 | 82 | chai.expect(response.status).to.be.equal(200); 83 | } 84 | }); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "CommonJS", 4 | "target": "es2019", 5 | "noImplicitAny": true, 6 | "sourceMap": true, 7 | "emitDecoratorMetadata": true, 8 | "experimentalDecorators": true, 9 | "outDir": "./dist" 10 | }, 11 | "include": [ 12 | "app/**/*" 13 | ], 14 | "exclude": [ 15 | "node_modules" 16 | ] 17 | } 18 | --------------------------------------------------------------------------------