├── .docker ├── app │ └── Dockerfile └── database │ └── Dockerfile ├── .env.example ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── Caddyfile_template.txt ├── README.md ├── docker-compose.yml ├── icon.png ├── nest-cli.json ├── package-lock.json ├── package.json ├── renovate.json ├── src ├── app.module.ts ├── auth │ ├── account.decorator.ts │ ├── account.entity.ts │ ├── account.repository.ts │ ├── account_access.entity.ts │ ├── account_banned.entity.ts │ ├── account_information.entity.ts │ ├── account_password.entity.ts │ ├── account_password.repository.ts │ ├── auth.controller.ts │ ├── auth.module.ts │ ├── auth.service.ts │ └── dto │ │ ├── account.dto.ts │ │ ├── account_password.dto.ts │ │ └── email.dto.ts ├── characters │ ├── arena_team.entity.ts │ ├── arena_team_member.entity.ts │ ├── battleground_deserters.entity.ts │ ├── character_arena_stats.entity.ts │ ├── character_banned.entity.ts │ ├── characters.controller.ts │ ├── characters.entity.ts │ ├── characters.module.ts │ ├── characters.service.ts │ ├── dto │ │ ├── characters.dto.ts │ │ └── recovery_item.dto.ts │ ├── guild.entity.ts │ ├── guild_member.entity.ts │ ├── recovery_item.entity.ts │ └── worldstates.entity.ts ├── config │ └── database.config.ts ├── main.ts ├── shared │ ├── auth.guard.ts │ ├── email.ts │ ├── misc.ts │ └── soap.ts ├── website │ ├── post.entity.ts │ ├── post.repository.ts │ ├── website.controller.ts │ ├── website.module.ts │ └── website.service.ts └── world │ ├── world.controller.ts │ ├── world.module.ts │ └── world.service.ts ├── test ├── app.e2e-spec.ts └── jest-e2e.json ├── test_api_endpoints.sh ├── tsconfig.build.json └── tsconfig.json /.docker/app/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12.13-alpine As development 2 | 3 | WORKDIR /usr/src/app 4 | 5 | COPY package*.json ./ 6 | 7 | RUN npm i -g @nestjs/cli 8 | RUN npm install 9 | 10 | COPY . . 11 | 12 | RUN npm run build -------------------------------------------------------------------------------- /.docker/database/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mysql:5.7 2 | 3 | # List of timezones: http://en.wikipedia.org/wiki/List_of_tz_database_time_zones 4 | 5 | # set timezone environment variable 6 | ENV TZ=Etc/UTC 7 | 8 | ENV LANG C.UTF-8 9 | 10 | HEALTHCHECK --interval=5s --timeout=15s --start-period=30s --retries=3 CMD mysqladmin -uroot -p$MYSQL_ROOT_PASSWORD ping -h localhost 11 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | WEBSITE_PORT=3000 3 | WEB_SITE_EXTERNAL_PORT=3306 4 | 5 | AUTH_DATABASE_HOST=127.0.0.1 6 | AUTH_DATABASE_PORT=3306 7 | AUTH_DATABASE_NAME=acore_auth 8 | AUTH_DATABASE_USERNAME=root 9 | AUTH_DATABASE_PASSWORD=root 10 | 11 | WORLD_DATABASE_HOST=127.0.0.1 12 | WORLD_DATABASE_PORT=3306 13 | WORLD_DATABASE_NAME=acore_world 14 | WORLD_DATABASE_USERNAME=root 15 | WORLD_DATABASE_PASSWORD=root 16 | 17 | CHARACTERS_DATABASE_HOST=127.0.0.1 18 | CHARACTERS_DATABASE_PORT=3306 19 | CHARACTERS_DATABASE_NAME=acore_characters 20 | CHARACTERS_DATABASE_USERNAME=root 21 | CHARACTERS_DATABASE_PASSWORD=root 22 | 23 | WEB_SITE_DATABASE_HOST=127.0.0.1 24 | WEB_SITE_DATABASE_PORT=3306 25 | WEB_SITE_DATABASE_NAME=website 26 | WEB_SITE_DATABASE_USERNAME=root 27 | WEB_SITE_DATABASE_PASSWORD=root 28 | 29 | SOAP_HOST_NAME=127.0.0.1 30 | SOAP_PORT=7878 31 | SOAP_URI=urn:AC 32 | SOAP_USERNAME=AZEROTHJS 33 | SOAP_PASSWORD=AZER0THJS!@ 34 | 35 | JWT_SECRET_KEY=secret 36 | JWT_EXPIRES_IN=30d 37 | JWT_COOKIE_EXPIRES_IN=30 38 | 39 | MAIL_HOST=smtp.mailtrap.io 40 | MAIL_PORT=25 41 | MAIL_USERNAME=null 42 | MAIL_PASSWORD=null 43 | MAIL_FROM=no-reply@azerothjs.com 44 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: [ 9 | 'plugin:@typescript-eslint/recommended', 10 | 'plugin:prettier/recommended', 11 | ], 12 | root: true, 13 | env: { 14 | node: true, 15 | jest: true, 16 | }, 17 | ignorePatterns: ['.eslintrc.js'], 18 | rules: { 19 | '@typescript-eslint/interface-name-prefix': 'off', 20 | '@typescript-eslint/explicit-function-return-type': 'off', 21 | '@typescript-eslint/explicit-module-boundary-types': 'off', 22 | '@typescript-eslint/no-explicit-any': 'off', 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # OS 14 | .DS_Store 15 | 16 | # Tests 17 | /coverage 18 | /.nyc_output 19 | 20 | # IDEs and editors 21 | /.idea 22 | .project 23 | .classpath 24 | .c9/ 25 | *.launch 26 | .settings/ 27 | *.sublime-workspace 28 | 29 | # IDE - VSCode 30 | .vscode/* 31 | !.vscode/settings.json 32 | !.vscode/tasks.json 33 | !.vscode/launch.json 34 | !.vscode/extensions.json 35 | 36 | # Config 37 | .env 38 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "tabWidth": 2 5 | } 6 | -------------------------------------------------------------------------------- /Caddyfile_template.txt: -------------------------------------------------------------------------------- 1 | ## Example of Caddyfile, with /api prefix rerouted. Caddy works 10x faster than Apache2 for this API/Server-status. 2 | 3 | { 4 | debug 5 | log { 6 | output file /var/log/caddy/access.log 7 | level DEBUG 8 | } 9 | } 10 | 11 | 12 | :80 { 13 | root * /var/www/azerothcore/server-status/dist/server-status/browser 14 | file_server 15 | 16 | handle /api/* { 17 | uri strip_prefix /api 18 | reverse_proxy 127.0.0.1:3000 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Azeroth API 2 | 3 | This project is a RESTful APIs for [AzerothCore](https://azerothcore.org). 4 | 5 | This API support these tools: 6 | - [ServerStatus](https://github.com/azerothcore/server-status/) 7 | - [ArenaStats](https://github.com/azerothcore/arena-stats) 8 | - [BG Queue Abuser Viewer](https://github.com/Helias/BG-Queue-Abuser-Viewer) 9 | - [WoW Statistics](https://github.com/azerothcore/wow-statistics) 10 | 11 | ### Requirements 12 | 13 | - NodeJS & npm 14 | - MySQL 15 | - AzerothCore database 16 | 17 | ### Installation 18 | 19 | ``` 20 | $ npm install 21 | ``` 22 | 23 | Copy the configuration file `.env.example` file into `.env` and fill it. 24 | 25 | Run the project: 26 | ``` 27 | $ npm start 28 | ``` 29 | 30 | Now you can locally access to the API routes through [http://localhost:3000](http://localhost:3000). 31 | 32 | ### Credits 33 | 34 | - [IntelligentQuantum](https://github.com/IntelligentQuantum) 35 | 36 | AzerothCore: [repository](https://github.com/azerothcore) - [website](http://azerothcore.org/) - [discord chat community](https://discord.gg/PaqQRkd) 37 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | 3 | services: 4 | #nestJS api 5 | ac-api: 6 | image: azerothcore/api 7 | restart: unless-stopped 8 | build: 9 | context: . 10 | dockerfile: ./.docker/app/Dockerfile 11 | target: development 12 | user: "node" 13 | working_dir: /usr/src/app 14 | environment: 15 | - NODE_ENV=${NODE_ENV:-dev} 16 | - VERSION=1.0 17 | volumes: 18 | - .:/usr/src/app 19 | - /usr/app/node_modules 20 | ports: 21 | - ${WEBSITE_PORT:-3000}:3000 22 | tty: true 23 | command: npm run start:dev 24 | networks: 25 | - azeroth-network 26 | 27 | # database 28 | ac-website-database: 29 | image: azerothcore/website-database 30 | build: 31 | context: . 32 | dockerfile: ./.docker/database/Dockerfile 33 | ports: 34 | - '${WEB_SITE_EXTERNAL_PORT:-3307}:3306' 35 | environment: 36 | - MYSQL_ROOT_PASSWORD=${WEB_SITE_DATABASE_PASSWORD:-password} 37 | - MYSQL_DATABASE=${WEB_SITE_DATABASE_NAME} 38 | - MYSQL_USER=${WEB_SITE_DATABASE_USERNAME} 39 | - MYSQL_PASSWORD=${WEB_SITE_DATABASE_PASSWORD} 40 | stdin_open: true # docker run -i 41 | tty: true # docker run -t 42 | networks: 43 | - azeroth-network 44 | 45 | networks: 46 | azeroth-network: 47 | external: 48 | name: azeroth-network -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azerothcore/acore-api/65f6e61a5ca35caeaaba5a0d36dbcebfeb738b4d/icon.png -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src" 4 | } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "azeroth-api", 3 | "version": "0.0.1", 4 | "description": "AzerothAPI", 5 | "author": "IntelligentQuantum", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/azerothcore/acore-api.git" 9 | }, 10 | "bugs": { 11 | "url": "https://github.com/azerothcore/acore-api/issues" 12 | }, 13 | "private": true, 14 | "license": "MIT", 15 | "scripts": { 16 | "prebuild": "rimraf dist", 17 | "build": "nest build", 18 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 19 | "start": "nest start", 20 | "start:dev": "nest start --watch", 21 | "start:debug": "nest start --debug --watch", 22 | "start:prod": "node dist/main", 23 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 24 | "test": "jest", 25 | "test:watch": "jest --watch", 26 | "test:cov": "jest --coverage", 27 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 28 | "test:e2e": "jest --config ./test/jest-e2e.json" 29 | }, 30 | "dependencies": { 31 | "@nestjs/common": "^7.6.13", 32 | "@nestjs/core": "^7.6.13", 33 | "@nestjs/platform-express": "^7.6.13", 34 | "@nestjs/typeorm": "^7.1.5", 35 | "class-transformer": "^0.4.0", 36 | "class-validator": "^0.13.1", 37 | "compression": "^1.7.4", 38 | "dotenv": "^8.2.0", 39 | "express-rate-limit": "^5.2.6", 40 | "helmet": "^4.4.1", 41 | "html-to-text": "^7.0.0", 42 | "js-sha1": "^0.6.0", 43 | "jsbn": "^1.1.0", 44 | "jsonwebtoken": "^8.5.1", 45 | "mysql2": "^2.2.5", 46 | "nodemailer": "^6.6.1", 47 | "reflect-metadata": "^0.1.13", 48 | "rimraf": "^3.0.2", 49 | "rxjs": "^6.6.6", 50 | "typeorm": "^0.2.31" 51 | }, 52 | "devDependencies": { 53 | "@nestjs/cli": "^7.5.6", 54 | "@nestjs/schematics": "^7.2.7", 55 | "@nestjs/testing": "^7.6.13", 56 | "@types/compression": "^1.7.0", 57 | "@types/express": "^4.17.11", 58 | "@types/express-rate-limit": "^5.1.1", 59 | "@types/html-to-text": "^6.0.0", 60 | "@types/jest": "^26.0.20", 61 | "@types/jsonwebtoken": "^8.5.0", 62 | "@types/node": "^14.14.31", 63 | "@types/nodemailer": "^6.4.0", 64 | "@types/supertest": "^2.0.10", 65 | "@typescript-eslint/eslint-plugin": "^4.15.2", 66 | "@typescript-eslint/parser": "^4.15.2", 67 | "eslint": "^7.20.0", 68 | "eslint-config-prettier": "^8.1.0", 69 | "eslint-plugin-prettier": "^3.3.1", 70 | "jest": "^26.6.3", 71 | "prettier": "^2.2.1", 72 | "supertest": "^6.1.3", 73 | "ts-jest": "^26.5.2", 74 | "ts-loader": "^8.0.17", 75 | "ts-node": "^9.1.1", 76 | "tsconfig-paths": "^3.9.0", 77 | "typescript": "^4.1.5" 78 | }, 79 | "jest": { 80 | "moduleFileExtensions": [ 81 | "js", 82 | "json", 83 | "ts" 84 | ], 85 | "rootDir": "src", 86 | "testRegex": ".*\\.spec\\.ts$", 87 | "transform": { 88 | "^.+\\.(t|j)s$": "ts-jest" 89 | }, 90 | "collectCoverageFrom": [ 91 | "**/*.(t|j)s" 92 | ], 93 | "coverageDirectory": "../coverage", 94 | "testEnvironment": "node" 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | 4 | import { 5 | AuthDatabaseConfig, 6 | WorldDatabaseConfig, 7 | CharactersDatabaseConfig, 8 | WebsiteDatabaseConfig, 9 | } from './config/database.config'; 10 | import { AuthModule } from './auth/auth.module'; 11 | import { WorldModule } from './world/world.module'; 12 | import { CharactersModule } from './characters/characters.module'; 13 | import { WebsiteModule } from './website/website.module'; 14 | 15 | @Module({ 16 | imports: [ 17 | TypeOrmModule.forRoot(AuthDatabaseConfig), 18 | TypeOrmModule.forRoot(WorldDatabaseConfig), 19 | TypeOrmModule.forRoot(CharactersDatabaseConfig), 20 | TypeOrmModule.forRoot(WebsiteDatabaseConfig), 21 | AuthModule, 22 | WorldModule, 23 | CharactersModule, 24 | WebsiteModule, 25 | ], 26 | }) 27 | export class AppModule {} 28 | -------------------------------------------------------------------------------- /src/auth/account.decorator.ts: -------------------------------------------------------------------------------- 1 | import { createParamDecorator, ExecutionContext } from '@nestjs/common'; 2 | 3 | export const Account = createParamDecorator((data, ctx: ExecutionContext) => { 4 | const req = ctx.switchToHttp().getRequest(); 5 | return data ? req.account[data] : req.account; 6 | }); 7 | -------------------------------------------------------------------------------- /src/auth/account.entity.ts: -------------------------------------------------------------------------------- 1 | import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; 2 | 3 | @Entity({ synchronize: false }) 4 | export class Account extends BaseEntity { 5 | @PrimaryGeneratedColumn() 6 | id: number; 7 | 8 | @Column() 9 | username: string; 10 | 11 | @Column() 12 | salt: Buffer; 13 | 14 | @Column() 15 | verifier: Buffer; 16 | 17 | @Column() 18 | reg_mail: string; 19 | } 20 | -------------------------------------------------------------------------------- /src/auth/account.repository.ts: -------------------------------------------------------------------------------- 1 | import { sign } from 'jsonwebtoken'; 2 | import { EntityRepository, getRepository, Repository } from 'typeorm'; 3 | 4 | import { Account } from './account.entity'; 5 | import { AccountDto } from './dto/account.dto'; 6 | import { 7 | BadRequestException, 8 | ConflictException, 9 | HttpStatus, 10 | InternalServerErrorException, 11 | UnauthorizedException, 12 | } from '@nestjs/common'; 13 | import { AccountPasswordDto } from './dto/account_password.dto'; 14 | import { AccountPassword } from './account_password.entity'; 15 | import { EmailDto } from './dto/email.dto'; 16 | import { Response } from 'express'; 17 | import { AccountBanned } from './account_banned.entity'; 18 | import { AccountInformation } from './account_information.entity'; 19 | import { Misc } from '../shared/misc'; 20 | 21 | @EntityRepository(Account) 22 | export class AccountRepository extends Repository { 23 | private accountInformationRepo = getRepository( 24 | AccountInformation, 25 | 'authConnection', 26 | ); 27 | 28 | private accountPasswordRepo = getRepository( 29 | AccountPassword, 30 | 'authConnection', 31 | ); 32 | 33 | private accountBannedRepo = getRepository(AccountBanned, 'authConnection'); 34 | 35 | async signUp(accountDto: AccountDto, response: Response): Promise { 36 | const { 37 | username, 38 | firstName, 39 | lastName, 40 | phone, 41 | password, 42 | email, 43 | passwordConfirm, 44 | } = accountDto; 45 | const account = this.create(); 46 | 47 | const emailExists = await this.findOne({ reg_mail: email }); 48 | const phoneExists = await this.accountInformationRepo.findOne({ phone }); 49 | 50 | if (emailExists) { 51 | throw new ConflictException(['Email address already exists']); 52 | } 53 | 54 | if (phoneExists) { 55 | throw new ConflictException(['Phone already exists']); 56 | } 57 | 58 | if (passwordConfirm !== password) { 59 | throw new BadRequestException(['Password does not match']); 60 | } 61 | 62 | const [salt, verifier] = Misc.GetSRP6RegistrationData(username, password); 63 | 64 | account.username = username.toUpperCase(); 65 | account.salt = salt; 66 | account.verifier = verifier; 67 | account.reg_mail = email.toUpperCase(); 68 | 69 | try { 70 | await this.save(account); 71 | 72 | const accountInformation = new AccountInformation(); 73 | accountInformation.id = account.id; 74 | accountInformation.first_name = firstName; 75 | accountInformation.last_name = lastName; 76 | accountInformation.phone = phone; 77 | await this.accountInformationRepo.save(accountInformation); 78 | 79 | AccountRepository.createToken(account, HttpStatus.CREATED, response); 80 | } catch (error) { 81 | if (error.code === 'ER_DUP_ENTRY') { 82 | throw new ConflictException(['Username already exists']); 83 | } else { 84 | throw new InternalServerErrorException([ 85 | 'Something went wrong! Please try again later.', 86 | ]); 87 | } 88 | } 89 | } 90 | 91 | async signIn(accountDto: AccountDto, response: Response): Promise { 92 | const { username, password } = accountDto; 93 | const account = await this.findOne({ where: { username } }); 94 | 95 | if ( 96 | !account || 97 | !Misc.verifySRP6(username, password, account.salt, account.verifier) 98 | ) { 99 | throw new UnauthorizedException(['Incorrect username or password']); 100 | } 101 | 102 | AccountRepository.createToken(account, HttpStatus.OK, response); 103 | } 104 | 105 | async updatePassword( 106 | accountPasswordDto: AccountPasswordDto, 107 | response: Response, 108 | accountId: number, 109 | ): Promise { 110 | const { passwordCurrent, password, passwordConfirm } = accountPasswordDto; 111 | const account = await this.findOne({ where: { id: accountId } }); 112 | 113 | if ( 114 | !Misc.verifySRP6( 115 | account.username, 116 | passwordCurrent, 117 | account.salt, 118 | account.verifier, 119 | ) 120 | ) { 121 | throw new UnauthorizedException(['Your current password is wrong!']); 122 | } 123 | 124 | if (passwordConfirm !== password) { 125 | throw new BadRequestException(['Password does not match']); 126 | } 127 | 128 | account.verifier = Misc.calculateSRP6Verifier( 129 | account.username, 130 | password, 131 | account.salt, 132 | ); 133 | console.log(account.salt); 134 | await this.save(account); 135 | 136 | const accountPassword = new AccountPassword(); 137 | accountPassword.id = account.id; 138 | accountPassword.password_changed_at = new Date(Date.now() - 1000); 139 | await this.accountPasswordRepo.save(accountPassword); 140 | 141 | AccountRepository.createToken(account, HttpStatus.OK, response); 142 | } 143 | 144 | async updateEmail(emailDto: EmailDto, accountId: number) { 145 | const { password, emailCurrent, email, emailConfirm } = emailDto; 146 | const account = await this.findOne({ where: { id: accountId } }); 147 | 148 | if (emailCurrent.toUpperCase() !== account.reg_mail) { 149 | throw new BadRequestException(['Your current email is wrong!']); 150 | } 151 | 152 | if (emailConfirm.toUpperCase() !== email.toUpperCase()) { 153 | throw new BadRequestException(['Email does not match']); 154 | } 155 | 156 | if (email.toUpperCase() === account.reg_mail) { 157 | throw new ConflictException(['That email address already exists']); 158 | } 159 | 160 | if ( 161 | !Misc.verifySRP6( 162 | account.username, 163 | password, 164 | account.salt, 165 | account.verifier, 166 | ) 167 | ) { 168 | throw new UnauthorizedException(['Your current password is wrong!']); 169 | } 170 | 171 | account.reg_mail = email.toUpperCase(); 172 | await this.save(account); 173 | 174 | return { 175 | status: 'success', 176 | message: ['Your email has been changed successfully!'], 177 | }; 178 | } 179 | 180 | async unban(accountId: number) { 181 | const accountBanned = await this.accountBannedRepo.findOne({ 182 | where: { id: accountId, active: 1 }, 183 | }); 184 | 185 | if (!accountBanned) { 186 | throw new BadRequestException(['Your account is not banned!']); 187 | } 188 | 189 | await Misc.setCoin(10, accountId); 190 | 191 | accountBanned.active = 0; 192 | await this.accountBannedRepo.save(accountBanned); 193 | 194 | return { status: 'success' }; 195 | } 196 | 197 | private static createToken( 198 | account: any, 199 | statusCode: number, 200 | response: Response, 201 | ): void { 202 | const token = sign({ id: account.id }, process.env.JWT_SECRET_KEY, { 203 | expiresIn: process.env.JWT_EXPIRES_IN, 204 | }); 205 | 206 | delete account.salt; 207 | delete account.verifier; 208 | 209 | response.status(statusCode).json({ status: 'success', token, account }); 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/auth/account_access.entity.ts: -------------------------------------------------------------------------------- 1 | import { BaseEntity, Column, Entity, PrimaryColumn } from 'typeorm'; 2 | 3 | @Entity({ synchronize: false }) 4 | export class AccountAccess extends BaseEntity { 5 | @PrimaryColumn() 6 | id: number; 7 | 8 | @Column() 9 | gmlevel: number; 10 | 11 | @Column() 12 | RealmID: number; 13 | 14 | @Column() 15 | comment: string; 16 | } 17 | -------------------------------------------------------------------------------- /src/auth/account_banned.entity.ts: -------------------------------------------------------------------------------- 1 | import { BaseEntity, Column, Entity, PrimaryColumn } from 'typeorm'; 2 | 3 | @Entity({ synchronize: false }) 4 | export class AccountBanned extends BaseEntity { 5 | @PrimaryColumn() 6 | id: number; 7 | 8 | @PrimaryColumn() 9 | bandate: number; 10 | 11 | @Column() 12 | unbandate: number; 13 | 14 | @Column() 15 | bannedby: string; 16 | 17 | @Column() 18 | banreason: string; 19 | 20 | @Column() 21 | active: number; 22 | } 23 | -------------------------------------------------------------------------------- /src/auth/account_information.entity.ts: -------------------------------------------------------------------------------- 1 | import { BaseEntity, Column, Entity, PrimaryColumn } from 'typeorm'; 2 | 3 | @Entity() 4 | export class AccountInformation extends BaseEntity { 5 | @PrimaryColumn({ unsigned: true, default: 0 }) 6 | id: number; 7 | 8 | @Column({ type: 'varchar', default: '', length: 50 }) 9 | first_name: string; 10 | 11 | @Column({ type: 'varchar', default: '', length: 50 }) 12 | last_name: string; 13 | 14 | @Column({ type: 'varchar', default: '', length: 25, unique: true }) 15 | phone: string; 16 | 17 | @Column({ type: 'int', default: 0, unsigned: true }) 18 | coins: number; 19 | 20 | @Column({ type: 'int', default: 0, unsigned: true }) 21 | points: number; 22 | } 23 | -------------------------------------------------------------------------------- /src/auth/account_password.entity.ts: -------------------------------------------------------------------------------- 1 | import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; 2 | 3 | @Entity() 4 | export class AccountPassword extends BaseEntity { 5 | @PrimaryGeneratedColumn({ unsigned: true }) 6 | id: number; 7 | 8 | @Column({ type: 'timestamp', nullable: true, default: null }) 9 | password_changed_at: Date; 10 | 11 | @Column({ type: 'timestamp', nullable: true, default: null }) 12 | password_reset_expires: Date; 13 | 14 | @Column({ nullable: true, default: null, collation: 'utf8_general_ci' }) 15 | password_reset_token: string; 16 | } 17 | -------------------------------------------------------------------------------- /src/auth/account_password.repository.ts: -------------------------------------------------------------------------------- 1 | import { createHash, randomBytes } from 'crypto'; 2 | import { EntityRepository, getRepository, MoreThan, Repository } from 'typeorm'; 3 | 4 | import { AccountPassword } from './account_password.entity'; 5 | import { AccountDto } from './dto/account.dto'; 6 | import { 7 | BadRequestException, 8 | InternalServerErrorException, 9 | NotFoundException, 10 | } from '@nestjs/common'; 11 | import { Account } from './account.entity'; 12 | import { Email } from '../shared/email'; 13 | import { AccountPasswordDto } from './dto/account_password.dto'; 14 | import { Request } from 'express'; 15 | import { Misc } from '../shared/misc'; 16 | 17 | @EntityRepository(AccountPassword) 18 | export class AccountPasswordRepository extends Repository { 19 | private accountRepo = getRepository(Account, 'authConnection'); 20 | 21 | async forgotPassword(accountDto: AccountDto, request: Request) { 22 | const account = await this.accountRepo.findOne({ 23 | reg_mail: accountDto.email, 24 | }); 25 | 26 | if (!account) { 27 | throw new NotFoundException(['There is no account with email address']); 28 | } 29 | 30 | const resetToken: string = randomBytes(32).toString('hex'); 31 | const passwordResetExpires: any = new Date( 32 | Date.now() + 10 * 60 * 1000, 33 | ).toISOString(); 34 | const passwordResetToken: string = createHash('sha256') 35 | .update(resetToken) 36 | .digest('hex'); 37 | 38 | const accountPassword = this.create(); 39 | accountPassword.id = account.id; 40 | accountPassword.password_reset_expires = passwordResetExpires; 41 | accountPassword.password_reset_token = passwordResetToken; 42 | await this.save(accountPassword); 43 | 44 | try { 45 | const resetURL = `${request.protocol}://${request.get( 46 | 'host', 47 | )}/auth/resetPassword/${resetToken}`; 48 | await new Email(account, resetURL).sendPasswordReset(); 49 | return { status: 'success', message: ['Token sent to email'] }; 50 | } catch (error) { 51 | await this.delete(account.id); 52 | 53 | if (error) 54 | throw new InternalServerErrorException([ 55 | 'There was an error sending the email. Try again later!', 56 | ]); 57 | } 58 | } 59 | 60 | async resetPassword(accountPasswordDto: AccountPasswordDto, token: string) { 61 | const { password, passwordConfirm } = accountPasswordDto; 62 | const hashedToken: string = createHash('sha256') 63 | .update(token) 64 | .digest('hex'); 65 | 66 | const accountPassword = await this.findOne({ 67 | where: { 68 | password_reset_token: hashedToken, 69 | password_reset_expires: MoreThan(new Date()), 70 | }, 71 | }); 72 | 73 | if (!accountPassword) { 74 | throw new BadRequestException(['Token is invalid or has expired']); 75 | } 76 | 77 | if (passwordConfirm !== password) { 78 | throw new BadRequestException(['Password does not match']); 79 | } 80 | 81 | const account = await this.accountRepo.findOne({ 82 | where: { id: accountPassword.id }, 83 | }); 84 | 85 | account.verifier = Misc.calculateSRP6Verifier( 86 | account.username, 87 | password, 88 | account.salt, 89 | ); 90 | await this.accountRepo.save(account); 91 | 92 | accountPassword.password_changed_at = new Date(Date.now() - 1000); 93 | accountPassword.password_reset_expires = null; 94 | accountPassword.password_reset_token = null; 95 | await this.save(accountPassword); 96 | 97 | return { 98 | status: 'success', 99 | message: ['Your password has been reset successfully!'], 100 | }; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/auth/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | Param, 5 | Patch, 6 | Post, 7 | Req, 8 | Res, 9 | UseGuards, 10 | ValidationPipe, 11 | Get, 12 | HttpStatus, 13 | } from '@nestjs/common'; 14 | import { Request, Response } from 'express'; 15 | 16 | import { AuthService } from './auth.service'; 17 | import { AccountDto } from './dto/account.dto'; 18 | import { AuthGuard } from '../shared/auth.guard'; 19 | import { Account } from './account.decorator'; 20 | import { getConnection } from 'typeorm'; 21 | import { Account as AccountEntity } from './account.entity'; 22 | import { AccountPasswordDto } from './dto/account_password.dto'; 23 | import { EmailDto } from './dto/email.dto'; 24 | 25 | @Controller('auth') 26 | export class AuthController { 27 | constructor(private readonly authService: AuthService) {} 28 | 29 | @Post('/signup') 30 | async signUp( 31 | @Body(ValidationPipe) accountDto: AccountDto, 32 | @Res() response: Response, 33 | ): Promise { 34 | return this.authService.signUp(accountDto, response); 35 | } 36 | 37 | @Post('/signin') 38 | async signIn( 39 | @Body() accountDto: AccountDto, 40 | @Res() response: Response, 41 | ): Promise { 42 | return this.authService.signIn(accountDto, response); 43 | } 44 | 45 | @Get('/logout') 46 | logout(@Res() response: Response): void { 47 | response.cookie('jwt', 'logout', { 48 | expires: new Date(Date.now() + 10), 49 | httpOnly: true, 50 | }); 51 | response.status(HttpStatus.OK).json({ status: 'success' }); 52 | } 53 | 54 | @Patch('/updateMyPassword') 55 | @UseGuards(new AuthGuard()) 56 | async updatePassword( 57 | @Body(ValidationPipe) accountPasswordDto: AccountPasswordDto, 58 | @Res() response: Response, 59 | @Account('id') accountId: number, 60 | ): Promise { 61 | return this.authService.updatePassword( 62 | accountPasswordDto, 63 | response, 64 | accountId, 65 | ); 66 | } 67 | 68 | @Patch('/updateMyEmail') 69 | @UseGuards(new AuthGuard()) 70 | async updateEmail( 71 | @Body(ValidationPipe) emailDto: EmailDto, 72 | @Account('id') accountId: number, 73 | ) { 74 | return this.authService.updateEmail(emailDto, accountId); 75 | } 76 | 77 | @Patch('/unban') 78 | @UseGuards(new AuthGuard()) 79 | async unban(@Account('id') accountId: number): Promise<{ status: string }> { 80 | return this.authService.unban(accountId); 81 | } 82 | 83 | @Post('/forgotPassword') 84 | async forgotPassword( 85 | @Body() accountDto: AccountDto, 86 | @Req() request: Request, 87 | ) { 88 | return this.authService.forgotPassword(accountDto, request); 89 | } 90 | 91 | @Patch('/resetPassword/:token') 92 | async resetPassword( 93 | @Body(ValidationPipe) accountPasswordDto: AccountPasswordDto, 94 | @Param('token') token: string, 95 | ) { 96 | return this.authService.resetPassword(accountPasswordDto, token); 97 | } 98 | 99 | @Get('/pulse/:days') 100 | async pulse(@Param('days') days: number): Promise { 101 | return await getConnection('authConnection') 102 | .getRepository(AccountEntity) 103 | .createQueryBuilder('auth') 104 | .select(['COUNT(*) AS accounts', 'COUNT(DISTINCT(last_ip)) AS IPs']) 105 | .where('DATEDIFF(NOW(), last_login) < ' + days) 106 | .getRawMany(); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AuthController } from './auth.controller'; 3 | import { AuthService } from './auth.service'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import { AccountRepository } from './account.repository'; 6 | import { AccountPasswordRepository } from './account_password.repository'; 7 | 8 | @Module({ 9 | imports: [ 10 | TypeOrmModule.forFeature( 11 | [AccountRepository, AccountPasswordRepository], 12 | 'authConnection', 13 | ), 14 | ], 15 | controllers: [AuthController], 16 | providers: [AuthService], 17 | }) 18 | export class AuthModule {} 19 | -------------------------------------------------------------------------------- /src/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | import { AccountRepository } from './account.repository'; 4 | import { InjectRepository } from '@nestjs/typeorm'; 5 | import { AccountDto } from './dto/account.dto'; 6 | import { AccountPasswordRepository } from './account_password.repository'; 7 | import { AccountPasswordDto } from './dto/account_password.dto'; 8 | import { EmailDto } from './dto/email.dto'; 9 | import { Request, Response } from 'express'; 10 | 11 | @Injectable() 12 | export class AuthService { 13 | constructor( 14 | @InjectRepository(AccountRepository, 'authConnection') 15 | private readonly accountRepository: AccountRepository, 16 | @InjectRepository(AccountPasswordRepository, 'authConnection') 17 | private readonly accountPasswordRepository: AccountPasswordRepository, 18 | ) {} 19 | 20 | async signUp(accountDto: AccountDto, response: Response): Promise { 21 | return this.accountRepository.signUp(accountDto, response); 22 | } 23 | 24 | async signIn(accountDto: AccountDto, response: Response): Promise { 25 | return this.accountRepository.signIn(accountDto, response); 26 | } 27 | 28 | async updatePassword( 29 | accountPasswordDto: AccountPasswordDto, 30 | response: Response, 31 | accountId: number, 32 | ): Promise { 33 | return this.accountRepository.updatePassword( 34 | accountPasswordDto, 35 | response, 36 | accountId, 37 | ); 38 | } 39 | 40 | async updateEmail(emailDto: EmailDto, accountId: number) { 41 | return this.accountRepository.updateEmail(emailDto, accountId); 42 | } 43 | 44 | async unban(accountId: number) { 45 | return this.accountRepository.unban(accountId); 46 | } 47 | 48 | async forgotPassword(accountDto: AccountDto, request: Request) { 49 | return this.accountPasswordRepository.forgotPassword(accountDto, request); 50 | } 51 | 52 | async resetPassword(accountPasswordDto: AccountPasswordDto, token: string) { 53 | return this.accountPasswordRepository.resetPassword( 54 | accountPasswordDto, 55 | token, 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/auth/dto/account.dto.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IsEmail, 3 | IsPhoneNumber, 4 | IsString, 5 | Matches, 6 | MaxLength, 7 | MinLength, 8 | } from 'class-validator'; 9 | 10 | export class AccountDto { 11 | @MinLength(4) 12 | @MaxLength(20) 13 | @Matches(/^[A-Za-z0-9_]+$/, { message: 'Please enter a valid username' }) 14 | readonly username: string; 15 | 16 | @MinLength(2) 17 | @MaxLength(50) 18 | @IsString() 19 | readonly firstName: string; 20 | 21 | @MinLength(2) 22 | @MaxLength(50) 23 | @IsString() 24 | readonly lastName: string; 25 | 26 | @IsPhoneNumber() 27 | readonly phone: string; 28 | 29 | @IsString() 30 | @MinLength(8) 31 | @MaxLength(20) 32 | @Matches(/((?=.*\d)|(?=.&\W+))(?![.\n])(?=.*[A-Z])(?=.*[a-z]).*$/, { 33 | message: 'Password too weak', 34 | }) 35 | readonly password: string; 36 | 37 | @IsString() 38 | readonly passwordConfirm: string; 39 | 40 | @IsEmail({}, { message: 'Please enter a valid email address' }) 41 | readonly email: string; 42 | } 43 | -------------------------------------------------------------------------------- /src/auth/dto/account_password.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsString, Matches, MaxLength, MinLength } from 'class-validator'; 2 | 3 | export class AccountPasswordDto { 4 | readonly passwordCurrent: string; 5 | 6 | @IsString() 7 | @MinLength(8) 8 | @MaxLength(20) 9 | @Matches(/((?=.*\d)|(?=.&\W+))(?![.\n])(?=.*[A-Z])(?=.*[a-z]).*$/, { 10 | message: 'Password too weak', 11 | }) 12 | readonly password: string; 13 | 14 | @IsString() 15 | readonly passwordConfirm: string; 16 | } 17 | -------------------------------------------------------------------------------- /src/auth/dto/email.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail } from 'class-validator'; 2 | 3 | export class EmailDto { 4 | readonly password: string; 5 | 6 | readonly emailCurrent: string; 7 | 8 | @IsEmail({}, { message: 'Please enter a valid email address' }) 9 | readonly email: string; 10 | 11 | readonly emailConfirm: string; 12 | } 13 | -------------------------------------------------------------------------------- /src/characters/arena_team.entity.ts: -------------------------------------------------------------------------------- 1 | import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; 2 | 3 | @Entity({ synchronize: false }) 4 | export class ArenaTeam extends BaseEntity { 5 | @PrimaryGeneratedColumn() 6 | arenaTeamId: number; 7 | 8 | @Column() 9 | name: string; 10 | 11 | @Column() 12 | captainGuid: number; 13 | 14 | @Column() 15 | type: number; 16 | 17 | @Column() 18 | rating: number; 19 | 20 | @Column() 21 | seasonGames: number; 22 | 23 | @Column() 24 | seasonWins: number; 25 | 26 | @Column() 27 | weekGames: number; 28 | 29 | @Column() 30 | weekWins: number; 31 | 32 | @Column() 33 | rank: number; 34 | 35 | @Column() 36 | backgroundColor: number; 37 | 38 | @Column() 39 | emblemStyle: number; 40 | 41 | @Column() 42 | emblemColor: number; 43 | 44 | @Column() 45 | borderStyle: number; 46 | 47 | @Column() 48 | borderColor: number; 49 | } 50 | -------------------------------------------------------------------------------- /src/characters/arena_team_member.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BaseEntity, 3 | Column, 4 | Entity, 5 | PrimaryGeneratedColumn, 6 | PrimaryColumn, 7 | } from 'typeorm'; 8 | 9 | @Entity({ synchronize: false }) 10 | export class ArenaTeamMember extends BaseEntity { 11 | @PrimaryGeneratedColumn() 12 | arenaTeamId: number; 13 | 14 | @PrimaryColumn() 15 | guid: number; 16 | 17 | @Column() 18 | weekGames: number; 19 | 20 | @Column() 21 | weekWins: number; 22 | 23 | @Column() 24 | seasonGames: number; 25 | 26 | @Column() 27 | seasonWins: number; 28 | 29 | @Column() 30 | personalRating: number; 31 | } 32 | -------------------------------------------------------------------------------- /src/characters/battleground_deserters.entity.ts: -------------------------------------------------------------------------------- 1 | import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; 2 | 3 | @Entity({ synchronize: false }) 4 | export class BattlegroundDeserters extends BaseEntity { 5 | @PrimaryGeneratedColumn() 6 | guid: number; 7 | 8 | @Column() 9 | type: number; 10 | 11 | @Column() 12 | datetime: string; 13 | } 14 | -------------------------------------------------------------------------------- /src/characters/character_arena_stats.entity.ts: -------------------------------------------------------------------------------- 1 | import { BaseEntity, Column, Entity, PrimaryColumn } from 'typeorm'; 2 | 3 | @Entity({ synchronize: false }) 4 | export class CharacterArenaStats extends BaseEntity { 5 | @PrimaryColumn() 6 | guid: number; 7 | 8 | @PrimaryColumn() 9 | slot: number; 10 | 11 | @Column() 12 | matchMakerRating: number; 13 | 14 | @Column() 15 | maxMMR: number; 16 | } 17 | -------------------------------------------------------------------------------- /src/characters/character_banned.entity.ts: -------------------------------------------------------------------------------- 1 | import { BaseEntity, Column, Entity, PrimaryColumn } from 'typeorm'; 2 | 3 | @Entity({ synchronize: false }) 4 | export class CharacterBanned extends BaseEntity { 5 | @PrimaryColumn() 6 | guid: number; 7 | 8 | @PrimaryColumn() 9 | bandate: number; 10 | 11 | @Column() 12 | unbandate: number; 13 | 14 | @Column() 15 | bannedby: string; 16 | 17 | @Column() 18 | banreason: string; 19 | 20 | @Column() 21 | active: number; 22 | } 23 | -------------------------------------------------------------------------------- /src/characters/characters.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | Get, 5 | Param, 6 | Patch, 7 | Post, 8 | Query, 9 | UseGuards, 10 | } from '@nestjs/common'; 11 | 12 | import { getConnection } from 'typeorm'; 13 | import { Account } from '../auth/account.decorator'; 14 | import { AccountAccess } from '../auth/account_access.entity'; 15 | import { AuthGuard } from '../shared/auth.guard'; 16 | import { ArenaTeam } from './arena_team.entity'; 17 | import { ArenaTeamMember } from './arena_team_member.entity'; 18 | import { BattlegroundDeserters } from './battleground_deserters.entity'; 19 | import { CharacterArenaStats } from './character_arena_stats.entity'; 20 | import { Characters } from './characters.entity'; 21 | import { CharactersService } from './characters.service'; 22 | import { CharactersDto } from './dto/characters.dto'; 23 | import { RecoveryItemDTO } from './dto/recovery_item.dto'; 24 | import { Guild } from './guild.entity'; 25 | import { GuildMember } from './guild_member.entity'; 26 | import { RecoveryItem } from './recovery_item.entity'; 27 | import { Worldstates } from './worldstates.entity'; 28 | 29 | @Controller('characters') 30 | export class CharactersController { 31 | constructor(private readonly charactersService: CharactersService) {} 32 | 33 | @Get('/online') 34 | async online() { 35 | const accountGMs = await getConnection('authConnection') 36 | .getRepository(AccountAccess) 37 | .createQueryBuilder('account_access') 38 | .select(['id']) 39 | .where('gmlevel > 0') 40 | .getRawMany(); 41 | 42 | const GmIds = accountGMs.map((aa) => aa.id); 43 | 44 | const connection = getConnection('charactersConnection'); 45 | const characters = await connection 46 | .getRepository(Characters) 47 | .createQueryBuilder('characters') 48 | .leftJoinAndSelect(GuildMember, 'gm', 'gm.guid = characters.guid') 49 | .leftJoinAndSelect(Guild, 'g', 'g.guildid = gm.guildid') 50 | .where('online = 1 AND account NOT IN (' + GmIds.join(',') + ')') 51 | .select([ 52 | 'characters.guid as guid', 53 | 'characters.name as name', 54 | 'characters.race as race', 55 | 'characters.class as class', 56 | 'characters.gender as gender', 57 | 'characters.level as level', 58 | 'characters.map as map', 59 | 'characters.instance_id as instance_id', 60 | 'characters.zone as zone', 61 | 'gm.guildid as guildId', 62 | 'g.name as guildName', 63 | ]) 64 | .getRawMany(); 65 | 66 | return characters; 67 | } 68 | 69 | /* characters statistics */ 70 | @Get('/stats') 71 | async characters_stats() { 72 | const connection = getConnection('charactersConnection'); 73 | return await connection 74 | .getRepository(Characters) 75 | .createQueryBuilder('characters') 76 | .select([ 77 | 'characters.race as race', 78 | 'characters.class as class', 79 | 'characters.level as level', 80 | ]) 81 | .getRawMany(); 82 | } 83 | 84 | /* characters data */ 85 | @Get('/search_characters') 86 | async character_data(@Query('name') name: string) { 87 | const connection = getConnection('charactersConnection'); 88 | 89 | // If 'name' parameter is null or void, return all characters 90 | if (!name || name.trim() === '') { 91 | return await connection 92 | .getRepository(Characters) 93 | .createQueryBuilder('characters') 94 | .select([ 95 | 'characters.guid as guid', 96 | 'characters.name as name', 97 | 'characters.race as race', 98 | 'characters.class as class', 99 | 'characters.level as level', 100 | 'characters.gender as gender', 101 | ]) 102 | .getRawMany(); 103 | } 104 | 105 | 106 | // if 'name" is there, search with filter 107 | return await connection 108 | .getRepository(Characters) 109 | .createQueryBuilder('characters') 110 | .select([ 111 | 'characters.guid as guid', 112 | 'characters.name as name', 113 | 'characters.race as race', 114 | 'characters.class as class', 115 | 'characters.level as level', 116 | 'characters.gender as gender', 117 | ]) 118 | .where('LOWER(characters.name) LIKE :name', { 119 | name: `${name.toLowerCase()}%`, 120 | }) 121 | .getRawMany(); 122 | } 123 | 124 | /* battleground_deserters */ 125 | @Get('/battleground_deserters/:count') 126 | async battleground_deserters(@Param('count') count: number, @Query() query) { 127 | let from = 0; 128 | let where = ''; 129 | 130 | if (!!query['from']) { 131 | from = query['from']; 132 | } 133 | 134 | if (!!query['name']) { 135 | where = `UPPER(name) LIKE UPPER('%${query['name']}%')`; 136 | } 137 | 138 | const connection = getConnection('charactersConnection'); 139 | return await connection 140 | .getRepository(BattlegroundDeserters) 141 | .createQueryBuilder('battleground_deserters') 142 | .innerJoinAndSelect( 143 | Characters, 144 | 'c', 145 | 'c.guid = battleground_deserters.guid', 146 | ) 147 | .select([ 148 | 'battleground_deserters.*', 149 | 'c.account as account', 150 | 'c.guid AS guid', 151 | 'c.name AS name', 152 | 'c.level AS level', 153 | 'c.race AS race', 154 | 'c.class AS class', 155 | 'c.gender AS gender', 156 | ]) 157 | .where(where) 158 | .orderBy({ 'battleground_deserters.datetime': 'DESC' }) 159 | .offset(from) 160 | .limit(count) 161 | .getRawMany(); 162 | } 163 | 164 | /* Arena routes */ 165 | 166 | @Get('/arena_team/id/:arenaTeamId') 167 | async arena_team_id(@Param('arenaTeamId') arenaTeamId: number) { 168 | const connection = getConnection('charactersConnection'); 169 | return await connection 170 | .getRepository(ArenaTeam) 171 | .createQueryBuilder('arena_team') 172 | .innerJoinAndSelect(Characters, 'c', 'c.guid = arena_team.captainGuid') 173 | .select([ 174 | 'c.name AS captainName', 175 | 'c.race AS captainRace', 176 | 'c.gender AS captainGender', 177 | 'c.class AS captainClass', 178 | 'arena_team.*', 179 | ]) 180 | .where('arena_team.arenaTeamId = ' + arenaTeamId) 181 | .getRawMany(); 182 | } 183 | 184 | @Get('/arena_team/type/:type/') 185 | async arena_team(@Param('type') type: number) { 186 | const connection = getConnection('charactersConnection'); 187 | return await connection 188 | .getRepository(ArenaTeam) 189 | .createQueryBuilder('arena_team') 190 | .innerJoinAndSelect(Characters, 'c', 'c.guid = arena_team.captainGuid') 191 | .select([ 192 | 'c.name AS captainName', 193 | 'c.race AS captainRace', 194 | 'c.gender AS captainGender', 195 | 'c.class AS captainClass', 196 | 'arena_team.*', 197 | ]) 198 | .where('arena_team.type = ' + type) 199 | .orderBy({ 'arena_team.rating': 'DESC' }) 200 | .getRawMany(); 201 | } 202 | 203 | @Get('/arena_team_member/:arenaTeamId') 204 | async arena_team_member(@Param('arenaTeamId') arenaTeamId: number) { 205 | const connection = getConnection('charactersConnection'); 206 | return await connection 207 | .getRepository(ArenaTeamMember) 208 | .createQueryBuilder('arena_team_member') 209 | .innerJoinAndSelect(Characters, 'c', 'c.guid = arena_team_member.guid') 210 | .innerJoinAndSelect( 211 | ArenaTeam, 212 | 'at', 213 | 'at.arenaTeamId = arena_team_member.arenaTeamId', 214 | ) 215 | .leftJoinAndSelect( 216 | CharacterArenaStats, 217 | 'cas', 218 | ` 219 | (arena_team_member.guid = cas.guid AND at.type = 220 | (CASE cas.slot 221 | WHEN 0 THEN 2 222 | WHEN 1 THEN 3 223 | WHEN 2 THEN 5 224 | END) 225 | )`, 226 | ) 227 | .select([ 228 | 'arena_team_member.*', 229 | 'c.name AS name', 230 | 'c.class AS class', 231 | 'c.race AS race', 232 | 'c.gender AS gender', 233 | 'cas.matchmakerRating as matchmakerRating', 234 | ]) 235 | .where('at.arenaTeamId = ' + arenaTeamId) 236 | .getRawMany(); 237 | } 238 | 239 | /* player arena team */ 240 | @Get('/player_arena_team/:guid') 241 | async player_arena_team(@Param('guid') guid: number) { 242 | const connection = getConnection('charactersConnection'); 243 | 244 | const charData = await connection 245 | .getRepository(Characters) 246 | .createQueryBuilder('characters') 247 | .select([ 248 | 'characters.guid as guid', 249 | 'characters.name as name', 250 | 'characters.race as race', 251 | 'characters.class as class', 252 | 'characters.level as level', 253 | 'characters.gender as gender', 254 | ]) 255 | .where('guid=:guid', { guid }) 256 | .getRawOne(); 257 | 258 | const arenaTeamsData = await connection 259 | .getRepository(ArenaTeamMember) 260 | .createQueryBuilder('arena_team_member') 261 | .leftJoinAndSelect( 262 | ArenaTeam, 263 | 'at', 264 | 'at.arenaTeamId = arena_team_member.arenaTeamId', 265 | ) 266 | .select([ 267 | 'at.arenaTeamId as arenaTeamId', 268 | 'arena_team_member.weekGames as weekGames', 269 | 'arena_team_member.weekWins as weekWins', 270 | 'arena_team_member.seasonGames as seasonGames', 271 | 'arena_team_member.seasonWins as seasonWins', 272 | 'arena_team_member.personalRating as personalRating', 273 | 'at.weekGames as arenaTeamWeekGames', 274 | 'at.name as arenaTeamName', 275 | 'at.type as arenaType', 276 | ]) 277 | .where('arena_team_member.guid=:guid', { guid }) 278 | .orderBy({ 'at.type': 'ASC' }) 279 | .getRawMany(); 280 | 281 | const characterArenaStats = await connection 282 | .getRepository(CharacterArenaStats) 283 | .createQueryBuilder('character_arena_stats') 284 | .select([ 285 | 'character_arena_stats.guid as guid', 286 | 'character_arena_stats.slot as slot', 287 | 'character_arena_stats.matchMakerRating as matchMakerRating', 288 | 'character_arena_stats.maxMMR as maxMMR', 289 | ]) 290 | .where('character_arena_stats.guid=:guid', { guid }) 291 | .getRawMany(); 292 | 293 | return { 294 | playerData: charData, 295 | arenaTeamsData, 296 | characterArenaStats, 297 | }; 298 | } 299 | 300 | @Get('search/worldstates') 301 | async search_worldstates( 302 | @Query() param: Worldstates, 303 | ): Promise { 304 | return this.charactersService.search_worldstates(param); 305 | } 306 | 307 | @Get('/recoveryItemList/:guid') 308 | @UseGuards(new AuthGuard()) 309 | async recoveryItemList( 310 | @Param('guid') guid: number, 311 | @Account('id') accountId: number, 312 | ): Promise { 313 | return this.charactersService.recoveryItemList(guid, accountId); 314 | } 315 | 316 | @Post('/recoveryItem') 317 | @UseGuards(new AuthGuard()) 318 | async recoveryItem( 319 | @Body() recoveryItemDto: RecoveryItemDTO, 320 | @Account('id') accountId: number, 321 | ) { 322 | return this.charactersService.recoveryItem(recoveryItemDto, accountId); 323 | } 324 | 325 | @Get('/recoveryHeroList') 326 | @UseGuards(new AuthGuard()) 327 | async recoveryHeroList( 328 | @Account('id') accountId: number, 329 | ): Promise { 330 | return this.charactersService.recoveryHeroList(accountId); 331 | } 332 | 333 | @Post('/recoveryHero') 334 | @UseGuards(new AuthGuard()) 335 | async recoveryHero( 336 | @Body() charactersDto: CharactersDto, 337 | @Account('id') accountId: number, 338 | ) { 339 | return this.charactersService.recoveryHero(charactersDto, accountId); 340 | } 341 | 342 | @Patch('/unban') 343 | @UseGuards(new AuthGuard()) 344 | async unban( 345 | @Body() charactersDto: CharactersDto, 346 | @Account('id') accountId: number, 347 | ) { 348 | return this.charactersService.unban(charactersDto, accountId); 349 | } 350 | 351 | @Post('/rename') 352 | @UseGuards(new AuthGuard()) 353 | async rename( 354 | @Body() charactersDto: CharactersDto, 355 | @Account('id') accountId: number, 356 | ) { 357 | return this.charactersService.rename(charactersDto, accountId); 358 | } 359 | 360 | @Post('/customize') 361 | @UseGuards(new AuthGuard()) 362 | async customize( 363 | @Body() charactersDto: CharactersDto, 364 | @Account('id') accountId: number, 365 | ) { 366 | return this.charactersService.customize(charactersDto, accountId); 367 | } 368 | 369 | @Post('/changeFaction') 370 | @UseGuards(new AuthGuard()) 371 | async changeFaction( 372 | @Body() charactersDto: CharactersDto, 373 | @Account('id') accountId: number, 374 | ) { 375 | return this.charactersService.changeFaction(charactersDto, accountId); 376 | } 377 | 378 | @Post('/changeRace') 379 | @UseGuards(new AuthGuard()) 380 | async changeRace( 381 | @Body() charactersDto: CharactersDto, 382 | @Account('id') accountId: number, 383 | ) { 384 | return this.charactersService.changeRace(charactersDto, accountId); 385 | } 386 | 387 | @Post('/boost') 388 | @UseGuards(new AuthGuard()) 389 | async boost( 390 | @Body() charactersDto: CharactersDto, 391 | @Account('id') accountId: number, 392 | ) { 393 | return this.charactersService.boost(charactersDto, accountId); 394 | } 395 | 396 | @Post('/unstuck') 397 | @UseGuards(new AuthGuard()) 398 | async unstuck( 399 | @Body() charactersDto: CharactersDto, 400 | @Account('id') accountId: number, 401 | ) { 402 | return this.charactersService.unstuck(charactersDto, accountId); 403 | } 404 | } 405 | -------------------------------------------------------------------------------- /src/characters/characters.entity.ts: -------------------------------------------------------------------------------- 1 | import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; 2 | 3 | @Entity({ synchronize: false }) 4 | export class Characters extends BaseEntity { 5 | @PrimaryGeneratedColumn() 6 | guid: number; 7 | 8 | @Column() 9 | account: number; 10 | 11 | @Column() 12 | name: string; 13 | 14 | @Column() 15 | race: number; 16 | 17 | @Column() 18 | class: number; 19 | 20 | @Column() 21 | gender: number; 22 | 23 | @Column() 24 | level: number; 25 | 26 | @Column() 27 | xp: number; 28 | 29 | @Column() 30 | money: number; 31 | 32 | @Column() 33 | skin: number; 34 | 35 | @Column() 36 | face: number; 37 | 38 | @Column() 39 | hairStyle: number; 40 | 41 | @Column() 42 | hairColor: number; 43 | 44 | @Column() 45 | facialStyle: number; 46 | 47 | @Column() 48 | bankSlots: number; 49 | 50 | @Column() 51 | restState: number; 52 | 53 | @Column() 54 | playerFlags: number; 55 | 56 | @Column() 57 | position_x: number; 58 | 59 | @Column() 60 | position_y: number; 61 | 62 | @Column() 63 | position_z: number; 64 | 65 | @Column() 66 | map: number; 67 | 68 | @Column() 69 | instance_id: number; 70 | 71 | @Column() 72 | instance_mode_mask: number; 73 | 74 | @Column() 75 | orientation: number; 76 | 77 | @Column() 78 | taximask: string; 79 | 80 | @Column() 81 | online: number; 82 | 83 | @Column() 84 | cinematic: number; 85 | 86 | @Column() 87 | totaltime: number; 88 | 89 | @Column() 90 | leveltime: number; 91 | 92 | @Column() 93 | logout_time: number; 94 | 95 | @Column() 96 | is_logout_resting: number; 97 | 98 | @Column() 99 | rest_bonus: number; 100 | 101 | @Column() 102 | resettalents_cost: number; 103 | 104 | @Column() 105 | resettalents_time: number; 106 | 107 | @Column() 108 | trans_x: number; 109 | 110 | @Column() 111 | trans_y: number; 112 | 113 | @Column() 114 | trans_z: number; 115 | 116 | @Column() 117 | trans_o: number; 118 | 119 | @Column() 120 | transguid: number; 121 | 122 | @Column() 123 | extra_flags: number; 124 | 125 | @Column() 126 | stable_slots: number; 127 | 128 | @Column() 129 | at_login: number; 130 | 131 | @Column() 132 | zone: number; 133 | 134 | @Column() 135 | death_expire_time: number; 136 | 137 | @Column() 138 | taxi_path: string; 139 | 140 | @Column() 141 | arenaPoints: number; 142 | 143 | @Column() 144 | totalHonorPoints: number; 145 | 146 | @Column() 147 | todayHonorPoints: number; 148 | 149 | @Column() 150 | yesterdayHonorPoints: number; 151 | 152 | @Column() 153 | totalKills: number; 154 | 155 | @Column() 156 | todayKills: number; 157 | 158 | @Column() 159 | yesterdayKills: number; 160 | 161 | @Column() 162 | chosenTitle: number; 163 | 164 | @Column() 165 | knownCurrencies: number; 166 | 167 | @Column() 168 | watchedFaction: number; 169 | 170 | @Column() 171 | drunk: number; 172 | 173 | @Column() 174 | health: number; 175 | 176 | @Column() 177 | power1: number; 178 | 179 | @Column() 180 | power2: number; 181 | 182 | @Column() 183 | power3: number; 184 | 185 | @Column() 186 | power4: number; 187 | 188 | @Column() 189 | power5: number; 190 | 191 | @Column() 192 | power6: number; 193 | 194 | @Column() 195 | power7: number; 196 | 197 | @Column() 198 | latency: number; 199 | 200 | @Column() 201 | talentGroupsCount: number; 202 | 203 | @Column() 204 | activeTalentGroup: number; 205 | 206 | @Column() 207 | exploredZones: string; 208 | 209 | @Column() 210 | equipmentCache: string; 211 | 212 | @Column() 213 | ammoId: number; 214 | 215 | @Column() 216 | knownTitles: string; 217 | 218 | @Column() 219 | actionBars: number; 220 | 221 | @Column() 222 | grantableLevels: number; 223 | 224 | @Column() 225 | creation_date: number; 226 | 227 | @Column() 228 | deleteInfos_Account: number; 229 | 230 | @Column() 231 | deleteInfos_Name: string; 232 | 233 | @Column() 234 | deleteDate: number; 235 | } 236 | -------------------------------------------------------------------------------- /src/characters/characters.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { CharactersController } from './characters.controller'; 3 | import { CharactersService } from './characters.service'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import { Characters } from './characters.entity'; 6 | import { RecoveryItem } from './recovery_item.entity'; 7 | import { CharacterBanned } from './character_banned.entity'; 8 | import { Worldstates } from './worldstates.entity'; 9 | 10 | @Module({ 11 | imports: [ 12 | TypeOrmModule.forFeature( 13 | [Characters, RecoveryItem, CharacterBanned, Worldstates], 14 | 'charactersConnection', 15 | ), 16 | ], 17 | controllers: [CharactersController], 18 | providers: [CharactersService], 19 | }) 20 | export class CharactersModule {} 21 | -------------------------------------------------------------------------------- /src/characters/characters.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BadRequestException, 3 | Injectable, 4 | NotFoundException, 5 | } from '@nestjs/common'; 6 | 7 | import { InjectRepository } from '@nestjs/typeorm'; 8 | import { Characters } from './characters.entity'; 9 | import { Like, Repository } from 'typeorm'; 10 | import { RecoveryItemDTO } from './dto/recovery_item.dto'; 11 | import { RecoveryItem } from './recovery_item.entity'; 12 | import { Soap } from '../shared/soap'; 13 | import { CharactersDto } from './dto/characters.dto'; 14 | import { Misc } from '../shared/misc'; 15 | import { CharacterBanned } from './character_banned.entity'; 16 | import { Worldstates } from './worldstates.entity'; 17 | 18 | @Injectable() 19 | export class CharactersService { 20 | constructor( 21 | @InjectRepository(Characters, 'charactersConnection') 22 | private readonly charactersRepository: Repository, 23 | @InjectRepository(RecoveryItem, 'charactersConnection') 24 | private readonly recoveryItemRepository: Repository, 25 | @InjectRepository(CharacterBanned, 'charactersConnection') 26 | private readonly characterBannedRepository: Repository, 27 | @InjectRepository(Worldstates, 'charactersConnection') 28 | private readonly worldstatesRepository: Repository, 29 | ) {} 30 | 31 | async search_worldstates(param: Worldstates): Promise { 32 | return await this.worldstatesRepository.find({ 33 | comment: Like(`%${param.comment}%`), 34 | }); 35 | } 36 | 37 | async recoveryItemList( 38 | guid: number, 39 | accountId: number, 40 | ): Promise { 41 | const characters = await this.charactersRepository.findOne({ 42 | select: ['guid'], 43 | where: { account: accountId }, 44 | }); 45 | 46 | if (characters?.guid !== +guid) { 47 | throw new NotFoundException(['Account with that character not found']); 48 | } 49 | 50 | return await this.recoveryItemRepository.find({ where: { Guid: guid } }); 51 | } 52 | 53 | async recoveryItem(recoveryItemDto: RecoveryItemDTO, accountId: number) { 54 | const characters = await this.charactersRepository.findOne({ 55 | select: ['guid', 'name'], 56 | where: { account: accountId }, 57 | }); 58 | const recoveryItem = await this.recoveryItemRepository.findOne({ 59 | select: ['Count'], 60 | where: { Id: recoveryItemDto.id }, 61 | }); 62 | 63 | if (characters?.guid !== recoveryItemDto.guid) { 64 | throw new NotFoundException(['Account with that character not found']); 65 | } 66 | 67 | if (!recoveryItem) { 68 | throw new NotFoundException(['Item Not Found']); 69 | } 70 | 71 | await this.recoveryItemRepository.delete({ Id: recoveryItemDto.id }); 72 | 73 | Soap.command( 74 | `send items ${characters.name} "Recovery Item" "AzerothJS Recovery Item" ${recoveryItemDto.itemEntry}:${recoveryItem.Count}`, 75 | ); 76 | 77 | return { status: 'success' }; 78 | } 79 | 80 | async recoveryHeroList(accountId: number): Promise { 81 | return await this.charactersRepository.find({ 82 | select: ['guid', 'class', 'totaltime', 'totalKills', 'deleteInfos_Name'], 83 | where: { deleteInfos_Account: accountId }, 84 | }); 85 | } 86 | 87 | async recoveryHero(charactersDto: CharactersDto, accountId: number) { 88 | const characters = await this.charactersRepository.findOne({ 89 | select: ['guid', 'deleteInfos_Name'], 90 | where: { deleteInfos_Account: accountId }, 91 | }); 92 | 93 | if (characters?.guid !== +charactersDto.guid) { 94 | throw new NotFoundException(['Account with that character not found']); 95 | } 96 | 97 | await Misc.setCoin(10, accountId); 98 | 99 | Soap.command( 100 | `character deleted restore ${charactersDto.guid} ${characters.deleteInfos_Name} ${accountId}`, 101 | ); 102 | 103 | return { status: 'success' }; 104 | } 105 | 106 | async unban(charactersDto: CharactersDto, accountId: number) { 107 | const characters = await this.charactersRepository.findOne({ 108 | select: ['guid', 'name'], 109 | where: { account: accountId }, 110 | }); 111 | 112 | if (characters?.guid !== +charactersDto.guid) { 113 | throw new NotFoundException('Account with that character not found'); 114 | } 115 | 116 | const characterBanned = await this.characterBannedRepository.findOne({ 117 | where: { guid: charactersDto.guid, active: 1 }, 118 | }); 119 | 120 | if (!characterBanned) { 121 | throw new BadRequestException('Your character is not ban!'); 122 | } 123 | 124 | await Misc.setCoin(5, accountId); 125 | 126 | Soap.command(`unban character ${characters.name}`); 127 | 128 | return { status: 'success' }; 129 | } 130 | 131 | async rename(charactersDto: CharactersDto, accountId: number) { 132 | return this.characterCommand(charactersDto, accountId, 'rename', 5); 133 | } 134 | 135 | async customize(charactersDto: CharactersDto, accountId: number) { 136 | return this.characterCommand(charactersDto, accountId, 'customize', 5); 137 | } 138 | 139 | async changeFaction(charactersDto: CharactersDto, accountId: number) { 140 | return this.characterCommand(charactersDto, accountId, 'changeFaction', 10); 141 | } 142 | 143 | async changeRace(charactersDto: CharactersDto, accountId: number) { 144 | return this.characterCommand(charactersDto, accountId, 'changeRace', 10); 145 | } 146 | 147 | async boost(charactersDto: CharactersDto, accountId: number) { 148 | return this.characterCommand(charactersDto, accountId, 'level', 5, 80); 149 | } 150 | 151 | async unstuck(charactersDto: CharactersDto, accountId: number) { 152 | const characters = await this.charactersRepository.findOne({ 153 | select: ['guid', 'name'], 154 | where: { account: accountId }, 155 | }); 156 | 157 | if (characters?.guid !== charactersDto.guid) { 158 | throw new NotFoundException(['Account with that character not found']); 159 | } 160 | 161 | Soap.command(`unstuck ${characters.name} graveyard`); 162 | Soap.command(`revive ${characters.name}`); 163 | 164 | return { status: 'success' }; 165 | } 166 | 167 | private async characterCommand( 168 | charactersDto: CharactersDto, 169 | accountId: number, 170 | command: string, 171 | coin: number, 172 | option?: number, 173 | ) { 174 | const characters = await this.charactersRepository.findOne({ 175 | select: ['guid', 'name'], 176 | where: { account: accountId }, 177 | }); 178 | 179 | if (characters?.guid !== +charactersDto.guid) { 180 | throw new NotFoundException(['Account with that character not found']); 181 | } 182 | 183 | await Misc.setCoin(coin, accountId); 184 | 185 | Soap.command(`character ${command} ${characters.name} ${option}`); 186 | 187 | return { status: 'success' }; 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/characters/dto/characters.dto.ts: -------------------------------------------------------------------------------- 1 | export class CharactersDto { 2 | readonly guid: number; 3 | } 4 | -------------------------------------------------------------------------------- /src/characters/dto/recovery_item.dto.ts: -------------------------------------------------------------------------------- 1 | export class RecoveryItemDTO { 2 | readonly id: number; 3 | readonly guid: number; 4 | readonly itemEntry: number; 5 | } 6 | -------------------------------------------------------------------------------- /src/characters/guild.entity.ts: -------------------------------------------------------------------------------- 1 | import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; 2 | 3 | @Entity({ synchronize: false }) 4 | export class Guild extends BaseEntity { 5 | @PrimaryGeneratedColumn() 6 | guildid: number; 7 | 8 | @Column() 9 | name: string; 10 | 11 | @Column() 12 | leaderguid: number; 13 | 14 | @Column() 15 | EmblemStyle: number; 16 | 17 | @Column() 18 | EmblemColor: number; 19 | 20 | @Column() 21 | BorderStyle: number; 22 | 23 | @Column() 24 | BorderColor: number; 25 | 26 | @Column() 27 | BackgroundColor: number; 28 | 29 | @Column() 30 | info: string; 31 | 32 | @Column() 33 | motd: string; 34 | 35 | @Column() 36 | createdate: number; 37 | 38 | @Column() 39 | BankMoney: number; 40 | } 41 | -------------------------------------------------------------------------------- /src/characters/guild_member.entity.ts: -------------------------------------------------------------------------------- 1 | import { BaseEntity, Column, Entity, PrimaryColumn } from 'typeorm'; 2 | 3 | @Entity({ synchronize: false }) 4 | export class GuildMember extends BaseEntity { 5 | @Column() 6 | guildid: number; 7 | 8 | @PrimaryColumn() 9 | guid: number; 10 | 11 | @Column() 12 | rank: number; 13 | 14 | @Column() 15 | pnote: string; 16 | 17 | @Column() 18 | offnote: string; 19 | } 20 | -------------------------------------------------------------------------------- /src/characters/recovery_item.entity.ts: -------------------------------------------------------------------------------- 1 | import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; 2 | 3 | @Entity({ synchronize: false }) 4 | export class RecoveryItem extends BaseEntity { 5 | @PrimaryGeneratedColumn() 6 | Id: number; 7 | 8 | @Column() 9 | Guid: number; 10 | 11 | @Column() 12 | ItemEntry: number; 13 | 14 | @Column() 15 | Count: number; 16 | } 17 | -------------------------------------------------------------------------------- /src/characters/worldstates.entity.ts: -------------------------------------------------------------------------------- 1 | import { BaseEntity, Column, Entity, PrimaryColumn } from 'typeorm'; 2 | 3 | @Entity({ synchronize: false }) 4 | export class Worldstates extends BaseEntity { 5 | @PrimaryColumn() 6 | entry: number; 7 | 8 | @Column() 9 | value: number; 10 | 11 | @Column() 12 | comment: string; 13 | } 14 | -------------------------------------------------------------------------------- /src/config/database.config.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import { TypeOrmModuleOptions } from '@nestjs/typeorm'; 3 | 4 | export const AuthDatabaseConfig: TypeOrmModuleOptions = { 5 | name: 'authConnection', 6 | type: 'mysql', 7 | host: process.env.AUTH_DATABASE_HOST, 8 | port: +process.env.AUTH_DATABASE_PORT, 9 | username: process.env.AUTH_DATABASE_USERNAME, 10 | password: process.env.AUTH_DATABASE_PASSWORD, 11 | database: process.env.AUTH_DATABASE_NAME, 12 | entities: [join(__dirname, '..', 'auth', '*.entity.{js, ts}')], 13 | synchronize: true, 14 | }; 15 | 16 | export const WorldDatabaseConfig: TypeOrmModuleOptions = { 17 | name: 'worldConnection', 18 | type: 'mysql', 19 | host: process.env.WORLD_DATABASE_HOST, 20 | port: +process.env.WORLD_DATABASE_PORT, 21 | username: process.env.WORLD_DATABASE_USERNAME, 22 | password: process.env.WORLD_DATABASE_PASSWORD, 23 | database: process.env.WORLD_DATABASE_NAME, 24 | entities: [join(__dirname, '..', 'world', '*.entity.{js, ts}')], 25 | synchronize: true, 26 | }; 27 | 28 | export const CharactersDatabaseConfig: TypeOrmModuleOptions = { 29 | name: 'charactersConnection', 30 | type: 'mysql', 31 | host: process.env.CHARACTERS_DATABASE_HOST, 32 | port: +process.env.CHARACTERS_DATABASE_PORT, 33 | username: process.env.CHARACTERS_DATABASE_USERNAME, 34 | password: process.env.CHARACTERS_DATABASE_PASSWORD, 35 | database: process.env.CHARACTERS_DATABASE_NAME, 36 | entities: [join(__dirname, '..', 'characters', '*.entity.{js, ts}')], 37 | synchronize: true, 38 | }; 39 | 40 | export const WebsiteDatabaseConfig: TypeOrmModuleOptions = { 41 | name: 'websiteConnection', 42 | type: 'mysql', 43 | host: process.env.WEB_SITE_DATABASE_HOST, 44 | port: +process.env.WEB_SITE_DATABASE_PORT, 45 | username: process.env.WEB_SITE_DATABASE_USERNAME, 46 | password: process.env.WEB_SITE_DATABASE_PASSWORD, 47 | database: process.env.WEB_SITE_DATABASE_NAME, 48 | entities: [join(__dirname, '..', 'website', '*.entity.{js, ts}')], 49 | synchronize: true, 50 | }; 51 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | import { NestFactory } from '@nestjs/core'; 3 | import { AppModule } from './app.module'; 4 | import { Logger } from '@nestjs/common'; 5 | import * as helmet from 'helmet'; 6 | import * as rateLimit from 'express-rate-limit'; 7 | import * as compression from 'compression'; 8 | 9 | async function bootstrap() { 10 | const logger = new Logger('bootstrap'); 11 | const app = await NestFactory.create(AppModule); 12 | const port = process.env.WEBSITE_PORT || 3000; 13 | 14 | app.enableCors(); 15 | app.use(helmet()); 16 | 17 | if (process.env.NODE_ENV === 'production') { 18 | app.use( 19 | rateLimit({ 20 | max: 100, 21 | windowMs: 60 * 60 * 1000, 22 | message: 'Too many requests from this IP, Please try again in an hour!', 23 | }), 24 | ); 25 | } 26 | 27 | app.use(compression()); 28 | await app.listen(port); 29 | 30 | logger.log(`Application listening on port ${port}`); 31 | } 32 | bootstrap(); 33 | -------------------------------------------------------------------------------- /src/shared/auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CanActivate, 3 | ExecutionContext, 4 | Injectable, 5 | InternalServerErrorException, 6 | UnauthorizedException, 7 | } from '@nestjs/common'; 8 | import { verify } from 'jsonwebtoken'; 9 | import { Account } from '../auth/account.entity'; 10 | import { AccountPassword } from '../auth/account_password.entity'; 11 | import { AccountInformation } from '../auth/account_information.entity'; 12 | import { getRepository } from 'typeorm'; 13 | 14 | @Injectable() 15 | export class AuthGuard implements CanActivate { 16 | private decoded: any; 17 | 18 | async canActivate(context: ExecutionContext): Promise { 19 | const request = context.switchToHttp().getRequest(); 20 | return await this.validateToken(request); 21 | } 22 | 23 | private async validateToken(request: any): Promise { 24 | let token: string; 25 | 26 | if ( 27 | request.headers.authorization && 28 | request.headers.authorization.startsWith('Bearer') 29 | ) 30 | token = request.headers.authorization.split(' ')[1]; 31 | else if (request.cookies?.jwt) { 32 | token = request.cookies.jwt; 33 | } 34 | 35 | if (!token) { 36 | throw new UnauthorizedException([ 37 | 'You are not logged in! Please log in to get access.', 38 | ]); 39 | } 40 | 41 | try { 42 | this.decoded = verify(token, process.env.JWT_SECRET_KEY); 43 | } catch (error) { 44 | if (error.name === 'JsonWebTokenError' || this.decoded === undefined) { 45 | throw new UnauthorizedException([ 46 | 'Invalid Token. Please log in again!', 47 | ]); 48 | } 49 | 50 | if (error.name === 'TokenExpiredError') { 51 | throw new UnauthorizedException([ 52 | 'Your token has expired! Please log in again', 53 | ]); 54 | } 55 | 56 | if (error) { 57 | throw new InternalServerErrorException([ 58 | 'Something went wrong! Please try again later', 59 | ]); 60 | } 61 | } 62 | 63 | const accountExists = await getRepository( 64 | Account, 65 | 'authConnection', 66 | ).findOne({ 67 | where: { id: this.decoded.id }, 68 | }); 69 | 70 | if (!accountExists) { 71 | throw new UnauthorizedException([ 72 | 'The account belonging to this token does no longer exist.', 73 | ]); 74 | } 75 | 76 | delete accountExists.salt; 77 | delete accountExists.verifier; 78 | 79 | const accountPassword = await getRepository( 80 | AccountPassword, 81 | 'authConnection', 82 | ).findOne({ 83 | where: { id: this.decoded.id }, 84 | }); 85 | 86 | if ( 87 | request.url === '/auth/updateMyPassword' && 88 | accountPassword && 89 | accountPassword.password_changed_at 90 | ) { 91 | const changedTimestamp = 92 | accountPassword.password_changed_at.getTime() / 1000; 93 | 94 | if (this.decoded.iat < changedTimestamp) { 95 | throw new UnauthorizedException([ 96 | 'User recently changed password! Please log in again', 97 | ]); 98 | } 99 | } 100 | 101 | const accountInformation = await getRepository( 102 | AccountInformation, 103 | 'authConnection', 104 | ).findOne({ 105 | where: { id: this.decoded.id }, 106 | }); 107 | 108 | request.account = { ...accountExists, ...accountInformation }; 109 | 110 | return true; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/shared/email.ts: -------------------------------------------------------------------------------- 1 | import { createTransport } from 'nodemailer'; 2 | import { fromString } from 'html-to-text'; 3 | import { Account } from '../auth/account.entity'; 4 | 5 | interface IEmail { 6 | from: string; 7 | to: string; 8 | subject: string; 9 | html: string; 10 | text: string; 11 | } 12 | 13 | export class Email { 14 | private readonly to: string; 15 | private readonly username: string; 16 | private readonly url: string; 17 | private readonly from: string; 18 | 19 | constructor(account: Account, url: string) { 20 | this.to = account.reg_mail; 21 | this.username = account.username; 22 | this.url = url; 23 | this.from = process.env.MAIL_FROM; 24 | } 25 | 26 | private static newTransport(mailOptions: IEmail) { 27 | return createTransport({ 28 | host: process.env.MAIL_HOST, 29 | port: +process.env.MAIL_PORT, 30 | auth: { 31 | user: process.env.MAIL_USERNAME, 32 | pass: process.env.MAIL_PASSWORD, 33 | }, 34 | }).sendMail(mailOptions); 35 | } 36 | 37 | private async send(template: string, subject: string): Promise { 38 | const mailOptions: IEmail = { 39 | from: this.from, 40 | to: this.to, 41 | subject, 42 | html: template, 43 | text: fromString(template), 44 | }; 45 | await Email.newTransport(mailOptions); 46 | } 47 | 48 | async sendPasswordReset(): Promise { 49 | const template = ` 50 |

Forgot your password?

51 |

Submit a patch request with your new password and password confirm to: Reset my password

52 |

Your password reset token (valid for only 10 minutes)

53 |

If you didn't forget your password, please ignore this email

54 | `; 55 | 56 | await this.send(template, 'AzerothJS Reset Password'); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/shared/misc.ts: -------------------------------------------------------------------------------- 1 | import { AccountInformation } from '../auth/account_information.entity'; 2 | import { BadRequestException } from '@nestjs/common'; 3 | import { randomBytes } from 'crypto'; 4 | import { BigInteger } from 'jsbn'; 5 | import * as sha1 from 'js-sha1'; 6 | import { getRepository } from 'typeorm'; 7 | 8 | export class Misc { 9 | static async setCoin(coin: number, accountId: number): Promise { 10 | const accountInformationRepo = getRepository( 11 | AccountInformation, 12 | 'authConnection', 13 | ); 14 | 15 | const accountInformation = await accountInformationRepo.findOne({ 16 | where: { id: accountId }, 17 | }); 18 | 19 | if (!accountInformation || accountInformation.coins < coin) { 20 | throw new BadRequestException([`You dont have enough coin (${coin})`]); 21 | } 22 | 23 | accountInformation.coins -= coin; 24 | await accountInformationRepo.save(accountInformation); 25 | } 26 | 27 | static calculateSRP6Verifier( 28 | username: string, 29 | password: string, 30 | salt?: Buffer, 31 | ): Buffer { 32 | if (!salt) { 33 | salt = randomBytes(32); 34 | } 35 | 36 | const N = new BigInteger( 37 | '894B645E89E1535BBDAD5B8B290650530801B18EBFBF5E8FAB3C82872A3E9BB7', 38 | 16, 39 | ); 40 | const g = new BigInteger('7', 16); 41 | 42 | const h1 = Buffer.from( 43 | sha1.arrayBuffer(`${username}:${password}`.toUpperCase()), 44 | ); 45 | 46 | const h2 = Buffer.from( 47 | sha1.arrayBuffer(Buffer.concat([salt, h1])), 48 | ).reverse(); 49 | 50 | const h2bigint = new BigInteger(h2.toString('hex'), 16); 51 | 52 | const verifierBigint = g.modPow(h2bigint, N); 53 | 54 | let verifier: Buffer = Buffer.from(verifierBigint.toByteArray()).reverse(); 55 | 56 | verifier = verifier.slice(0, 32); 57 | if (verifier.length != 32) { 58 | verifier = Buffer.concat([verifier], 32); 59 | } 60 | 61 | return verifier; 62 | } 63 | 64 | static GetSRP6RegistrationData( 65 | username: string, 66 | password: string, 67 | ): Array { 68 | const salt = randomBytes(32); 69 | 70 | const verifier = this.calculateSRP6Verifier(username, password, salt); 71 | 72 | return [salt, verifier]; 73 | } 74 | 75 | static verifySRP6( 76 | username: string, 77 | password: string, 78 | salt: Buffer, 79 | verifier: Buffer, 80 | ): boolean { 81 | const generated: Buffer = this.calculateSRP6Verifier( 82 | username, 83 | password, 84 | salt, 85 | ); 86 | 87 | return Buffer.compare(generated, verifier) === 0 ? true : false; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/shared/soap.ts: -------------------------------------------------------------------------------- 1 | import { request } from 'http'; 2 | import { Logger } from '@nestjs/common'; 3 | 4 | export class Soap { 5 | static command(command: string): void { 6 | const req = request( 7 | { 8 | hostname: process.env.SOAP_HOST_NAME, 9 | port: +process.env.SOAP_PORT, 10 | method: 'POST', 11 | auth: `${process.env.SOAP_USERNAME}:${process.env.SOAP_PASSWORD}`, 12 | }, 13 | (res) => { 14 | if (res.statusCode !== 200) { 15 | Logger.error(`${res.statusMessage} ${res.statusCode}`, null, 'Soap'); 16 | } 17 | }, 18 | ); 19 | 20 | req.write(Soap.execute(command)); 21 | req.end(); 22 | } 23 | 24 | private static execute(command: string): string { 25 | return ` 26 | 27 | 32 | 33 | 34 | ${command} 35 | 36 | 37 | 38 | `; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/website/post.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BaseEntity, 3 | Column, 4 | CreateDateColumn, 5 | Entity, 6 | PrimaryGeneratedColumn, 7 | UpdateDateColumn, 8 | } from 'typeorm'; 9 | 10 | @Entity() 11 | export class Post extends BaseEntity { 12 | @PrimaryGeneratedColumn({ unsigned: true }) 13 | id: number; 14 | 15 | @Column() 16 | title: string; 17 | 18 | @Column({ type: 'text' }) 19 | body: string; 20 | 21 | @CreateDateColumn({ type: 'timestamp' }) 22 | created_at: Date; 23 | 24 | @UpdateDateColumn({ type: 'timestamp' }) 25 | updated_at: Date; 26 | } 27 | -------------------------------------------------------------------------------- /src/website/post.repository.ts: -------------------------------------------------------------------------------- 1 | import { EntityRepository, Repository } from 'typeorm'; 2 | import { Post } from './post.entity'; 3 | 4 | @EntityRepository(Post) 5 | export class PostRepository extends Repository {} 6 | -------------------------------------------------------------------------------- /src/website/website.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller } from '@nestjs/common'; 2 | import { WebsiteService } from './website.service'; 3 | 4 | @Controller('website') 5 | export class WebsiteController { 6 | constructor(private websiteService: WebsiteService) {} 7 | } 8 | -------------------------------------------------------------------------------- /src/website/website.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { WebsiteController } from './website.controller'; 3 | import { WebsiteService } from './website.service'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import { PostRepository } from './post.repository'; 6 | 7 | @Module({ 8 | imports: [TypeOrmModule.forFeature([PostRepository], 'websiteConnection')], 9 | controllers: [WebsiteController], 10 | providers: [WebsiteService], 11 | }) 12 | export class WebsiteModule {} 13 | -------------------------------------------------------------------------------- /src/website/website.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { PostRepository } from './post.repository'; 4 | 5 | @Injectable() 6 | export class WebsiteService { 7 | constructor( 8 | @InjectRepository(PostRepository, 'websiteConnection') 9 | private postRepository: PostRepository, 10 | ) {} 11 | } 12 | -------------------------------------------------------------------------------- /src/world/world.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller } from '@nestjs/common'; 2 | import { WorldService } from './world.service'; 3 | 4 | @Controller('world') 5 | export class WorldController { 6 | constructor(private worldService: WorldService) {} 7 | // TODO 8 | } 9 | -------------------------------------------------------------------------------- /src/world/world.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { WorldController } from './world.controller'; 3 | import { WorldService } from './world.service'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | 6 | @Module({ 7 | imports: [TypeOrmModule.forFeature([])], 8 | controllers: [WorldController], 9 | providers: [WorldService], 10 | }) 11 | export class WorldModule {} 12 | -------------------------------------------------------------------------------- /src/world/world.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class WorldService { 5 | // TODO 6 | } 7 | -------------------------------------------------------------------------------- /test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test_api_endpoints.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | BASE_URL="http://127.0.0.1/api" 4 | 5 | ENDPOINTS=( 6 | "/characters/online" 7 | "/characters/stats" 8 | "/characters/search_characters" 9 | "/characters/battleground_deserters/1" 10 | "/characters/arena_team/id/1" 11 | "/characters/arena_team/type/2" 12 | "/characters/arena_team_member/1" 13 | "/characters/player_arena_team/1" 14 | "/characters/search/worldstates" 15 | "/characters/recoveryItemList/1" 16 | "/characters/recoveryHeroList" 17 | "/auth/logout" 18 | "/auth/pulse/7" 19 | ) 20 | 21 | echo "🔍 Testing API endpoints on $BASE_URL" 22 | echo "-----------------------------------------" 23 | 24 | for endpoint in "${ENDPOINTS[@]}" 25 | do 26 | full_url="${BASE_URL}${endpoint}" 27 | http_code=$(curl -s -o /dev/null -w "%{http_code}" "$full_url") 28 | echo "$full_url => HTTP $http_code" 29 | done 30 | 31 | echo "-----------------------------------------" 32 | echo "✅ Test completed." 33 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "allowJs": true 15 | } 16 | } 17 | --------------------------------------------------------------------------------