├── .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 |

4 |
DevWars API
5 |
The Human Layer of the Stack
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
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 |
--------------------------------------------------------------------------------