diff --git a/docker-compose.yml b/docker-compose.yml index f64dd18c7358bda9ee5b8b5bf21c7a63138b07dd..31504d4638e5f88aebb69d29e1f33a92e699c023 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,7 +4,7 @@ services: build: . container_name: back ports: - - 8080:5000 + - 5000:5000 postgres: image: mdillon/postgis container_name: postgis diff --git a/ormconfig.json.example b/ormconfig.json.example index e5a401dd8792446e8202058b594684ad44a86d6b..78ed436feede989d81d044795a41227e73e3f94a 100644 --- a/ormconfig.json.example +++ b/ormconfig.json.example @@ -1,12 +1,12 @@ - { - "type": "", - "host": "", - "port": , - "username": "", - "password": "", - "database": "", - "entities": ["src/**/*.entity{.ts,.js}"], - "synchronize": true, - "logging": true - } \ No newline at end of file + "type": "postgres", + "host": "localhost", + "port": 5432, + "username": "ehasa", + "password": "salasana", + "database": "ehasa", + "entities": ["src/**/*.entity{.ts,.js}"], + "synchronize": true, + "logging": true, + "dropSchema": true +} diff --git a/package-lock.json b/package-lock.json index 53a606c27128a913a5a331e85d089988f9f164f0..be2f7c8964168262bdeaafcfad35ef0656a2656e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1450,11 +1450,6 @@ "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz", "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==" }, - "bluebird": { - "version": "3.5.5", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.5.tgz", - "integrity": "sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w==" - }, "body-parser": { "version": "1.19.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", @@ -1906,15 +1901,6 @@ "wrap-ansi": "^2.0.0" } }, - "cls-bluebird": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cls-bluebird/-/cls-bluebird-2.1.0.tgz", - "integrity": "sha1-N+8eCAqP+1XC9BZPU28ZGeeWiu4=", - "requires": { - "is-bluebird": "^1.0.2", - "shimmer": "^1.1.0" - } - }, "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -2397,11 +2383,6 @@ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-6.2.0.tgz", "integrity": "sha512-HygQCKUBSFl8wKQZBSemMywRWcEDNidvNbjGVyZu3nbZ8qq9ubiPoGLMdRDpfSrpkkm9BXYFkpKxxFX38o/76w==" }, - "dottie": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.1.tgz", - "integrity": "sha512-ch5OQgvGDK2u8pSZeSYAQaV/lczImd7pMJ7BcEPXmnFVjy4yJIzP6CsODJUTH8mg1tyH1Z2abOiuJO3DjZ/GBw==" - }, "double-ended-queue": { "version": "2.1.0-0", "resolved": "https://registry.npmjs.org/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz", @@ -3125,8 +3106,7 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -3147,14 +3127,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3169,20 +3147,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -3299,8 +3274,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -3312,7 +3286,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -3327,7 +3300,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -3335,14 +3307,12 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -3361,7 +3331,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -3442,8 +3411,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -3455,7 +3423,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -3541,8 +3508,7 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -3578,7 +3544,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -3598,7 +3563,6 @@ "version": "3.0.1", "bundled": true, "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -3642,14 +3606,12 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true } } }, @@ -4020,11 +3982,6 @@ "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=" }, - "inflection": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.12.0.tgz", - "integrity": "sha1-ogCTVlbW9fa8TcdQLhrstwMihBY=" - }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -4088,11 +4045,6 @@ "binary-extensions": "^1.0.0" } }, - "is-bluebird": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-bluebird/-/is-bluebird-1.0.2.tgz", - "integrity": "sha1-CWQ5Bg9KpBGr7hkUOoTWpVNG1uI=" - }, "is-buffer": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", @@ -5145,7 +5097,8 @@ "lodash": { "version": "4.17.11", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" + "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", + "dev": true }, "lodash.includes": { "version": "4.3.0", @@ -5443,19 +5396,6 @@ "minimist": "0.0.8" } }, - "moment": { - "version": "2.24.0", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", - "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" - }, - "moment-timezone": { - "version": "0.5.25", - "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.25.tgz", - "integrity": "sha512-DgEaTyN/z0HFaVcVbSyVCUU6HeFdnNC3vE4c9cgu2dgMTvjBUBdBzWfasTBmAW45u5OIMeCJtU8yNjM22DHucw==", - "requires": { - "moment": ">= 2.9.0" - } - }, "ms": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", @@ -6553,14 +6493,6 @@ "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", "dev": true }, - "retry-as-promised": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-3.2.0.tgz", - "integrity": "sha512-CybGs60B7oYU/qSQ6kuaFmRd9sTZ6oXSc0toqePvV74Ac6/IFZSI1ReFQmtCN+uvW1Mtqdwpvt/LGOiCBAY2Mg==", - "requires": { - "any-promise": "^1.3.0" - } - }, "rimraf": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", @@ -6689,51 +6621,6 @@ } } }, - "sequelize": { - "version": "5.8.7", - "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-5.8.7.tgz", - "integrity": "sha512-1rubZM8fAyCt5ipyS+3HJ3Jbmb8WesLdPJ3jIbTD+78EbuPZILFEA5fK0mliVRBx7oM7oPULeVX0lxSRXBV1jw==", - "requires": { - "bluebird": "^3.5.0", - "cls-bluebird": "^2.1.0", - "debug": "^4.1.1", - "dottie": "^2.0.0", - "inflection": "1.12.0", - "lodash": "^4.17.11", - "moment": "^2.24.0", - "moment-timezone": "^0.5.21", - "retry-as-promised": "^3.1.0", - "semver": "^5.6.0", - "sequelize-pool": "^1.0.2", - "toposort-class": "^1.0.1", - "uuid": "^3.2.1", - "validator": "^10.11.0", - "wkx": "^0.4.6" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "requires": { - "ms": "^2.1.1" - } - }, - "validator": { - "version": "10.11.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-10.11.0.tgz", - "integrity": "sha512-X/p3UZerAIsbBfN/IwahhYaBbY68EN/UQBWHtsbXGT5bfrH/p4NQzUCG1kF/rtKaNpnJ7jAu6NGTdSNtyNIXMw==" - } - } - }, - "sequelize-pool": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-1.0.2.tgz", - "integrity": "sha512-VMKl/gCCdIvB1gFZ7p+oqLFEyZEz3oMMYjkKvfEC7GoO9bBcxmfOOU9RdkoltfXGgBZFigSChihRly2gKtsh2w==", - "requires": { - "bluebird": "^3.5.3" - } - }, "serve-static": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", @@ -6797,11 +6684,6 @@ "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", "dev": true }, - "shimmer": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz", - "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==" - }, "signal-exit": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", @@ -7555,11 +7437,6 @@ } } }, - "toposort-class": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz", - "integrity": "sha1-f/0feMi+KMO6Rc1OGj9e4ZO9mYg=" - }, "touch": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", @@ -7885,9 +7762,9 @@ } }, "typescript": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.4.3.tgz", - "integrity": "sha512-FFgHdPt4T/duxx6Ndf7hwgMZZjZpB+U0nMNGVCYPq0rEzWKjEDobm4J6yb3CS7naZ0yURFqdw9Gwc7UOh/P9oQ==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.2.tgz", + "integrity": "sha512-7KxJovlYhTX5RaRbUdkAXN1KUZ8PwWlTzQdHV6xNqvuFOs7+WBo10TQUqT19Q/Jz2hk5v9TQDIhyLhhJY4p5AA==", "dev": true }, "uglify-js": { @@ -8256,14 +8133,6 @@ "string-width": "^2.1.1" } }, - "wkx": { - "version": "0.4.6", - "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.4.6.tgz", - "integrity": "sha512-LHxXlzRCYQXA9ZHgs8r7Gafh0gVOE8o3QmudM1PIkOdkXXjW7Thcl+gb2P2dRuKgW8cqkitCRZkkjtmWzpHi7A==", - "requires": { - "@types/node": "*" - } - }, "wordwrap": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", diff --git a/package.json b/package.json index 9e44f929c2267e5ed2e143931c9b6a6bf908ff0e..794db7d353397154ef795d59135b5771df8300ff 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,6 @@ "reflect-metadata": "^0.1.12", "rimraf": "^2.6.2", "rxjs": "^6.3.3", - "sequelize": "^5.8.7", "socket.io-redis": "^5.2.0", "typeorm": "^0.2.17" }, @@ -60,7 +59,7 @@ "ts-node": "^8.3.0", "tsconfig-paths": "^3.8.0", "tslint": "5.16.0", - "typescript": "^3.4.3", + "typescript": "^3.5.2", "wait-on": "^3.2.0" }, "jest": { diff --git a/src/app.module.ts b/src/app.module.ts index 91f8bcb4a12aad77ab1d506b941bf71f7278a2a6..f2c7b7765304436488bbadb3fdfce4fbdc1fbd9a 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,16 +1,24 @@ import { Module } from '@nestjs/common'; import { APP_FILTER, APP_INTERCEPTOR, APP_GUARD } from '@nestjs/core'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { Connection } from 'typeorm'; + import { AppController } from './app.controller'; import { AppService } from './app.service'; -import { Connection } from 'typeorm'; -import { UserModule } from './user/user.module'; -import { HttpErrorFilter } from './shared/http-error.filter'; +import { RolesGuard } from './shared/roles.guard'; import { LoggingInterceptor } from './shared/logging.interceptor'; +import { StatesGuard } from './shared/states.guard'; +import { HttpErrorFilter } from './shared/http-error.filter'; + import { NotificationModule } from './notifications/notifications.module'; +import { TaskModule } from './task/task.module'; +import { TrackingModule } from './tracking/tracking.module'; +import { UserModule } from './user/user.module'; +import { DrawModule } from './draw/draw.module'; +import { FactionModule } from './faction/faction.module'; import { GameModule } from './game/game.module'; -import { RolesGuard } from './shared/roles.guard'; +import { ScoreModule } from './score/score.module'; @Module({ imports: [ @@ -18,6 +26,11 @@ import { RolesGuard } from './shared/roles.guard'; UserModule, GameModule, NotificationModule, + TaskModule, + DrawModule, + FactionModule, + TrackingModule, + ScoreModule, ], controllers: [AppController], providers: [ @@ -34,6 +47,10 @@ import { RolesGuard } from './shared/roles.guard'; provide: APP_GUARD, useClass: RolesGuard, }, + { + provide: APP_GUARD, + useClass: StatesGuard, + }, ], }) export class AppModule { diff --git a/src/draw/coordinate.entity.ts b/src/draw/coordinate.entity.ts new file mode 100644 index 0000000000000000000000000000000000000000..ef5447f61cfd1dd07d1f81a8ee1fce8e8256da2a --- /dev/null +++ b/src/draw/coordinate.entity.ts @@ -0,0 +1,51 @@ +import { + Entity, + Column, + PrimaryGeneratedColumn, + ManyToOne, + Timestamp, +} from 'typeorm'; + +import { Game_PersonEntity, GameEntity } from '../game/game.entity'; +import { FactionEntity } from '../faction/faction.entity'; + +@Entity('MapDrawing') +export class MapDrawingEntity { + @PrimaryGeneratedColumn('uuid') mapDrawingId: string; + @Column({ type: 'bool', nullable: true }) drawingIsActive: boolean; + @Column({ type: 'time', nullable: true }) drawingValidTill: string; + + @Column({ type: 'json', nullable: true }) data: JSON; + + @ManyToOne(type => FactionEntity, faction => faction.mapDrawings, { + onDelete: 'CASCADE', + }) + faction: FactionEntity; + @ManyToOne(type => GameEntity, gameEntity => gameEntity.id, { + onDelete: 'CASCADE', + }) + gameId: GameEntity; +} + +@Entity('Game_Person_MapDrawing') +export class Game_Person_MapDrawingEntity { + @PrimaryGeneratedColumn('uuid') GPmapDrawingId: string; + @Column({ type: 'timestamp' }) GPCTimeStamp: Timestamp; + + @ManyToOne( + type => Game_PersonEntity, + game_person => game_person.gamepersonId, + { + onDelete: 'CASCADE', + }, + ) + game_person: Game_PersonEntity; + @ManyToOne( + type => MapDrawingEntity, + map_drawing => map_drawing.mapDrawingId, + { + onDelete: 'CASCADE', + }, + ) + map_drawing: MapDrawingEntity; +} diff --git a/src/draw/draw.controller.ts b/src/draw/draw.controller.ts new file mode 100644 index 0000000000000000000000000000000000000000..26f6cf8c56142ef190ad3bcbfd7ce9d32caa371e --- /dev/null +++ b/src/draw/draw.controller.ts @@ -0,0 +1,42 @@ +import { + Controller, + Put, + UseGuards, + Get, + Param, + UsePipes, + ValidationPipe, + Body, +} from '@nestjs/common'; + +import { AuthGuard } from '../shared/auth.guard'; +import { DrawService } from './draw.service'; +import { Roles, GameStates } from '../shared/guard.decorator'; +import { MapDrawingDTO, ReturnDrawingsDTO } from './mapdrawing.dto'; + +/* + DrawController + + Functions either insert or return MapDrawing data, + Insert functions require user to have proper role (either gm or commander) in the game to be able store the ddata: MapDrawingDTOata to database. + Data return functions require atleast spectator role. + */ +@Controller('draw') +export class DrawController { + constructor(private drawService: DrawService) {} + + @Put('mapdrawing/:id') + @Roles('admin', 'factionleader') + @GameStates('CREATED', 'STARTED') + @UsePipes(new ValidationPipe()) + async draw(@Param('id') gameId, @Body() data: MapDrawingDTO) { + return this.drawService.draw(gameId, data); + } + + @Get('map/:id') + @UseGuards(new AuthGuard()) + @UsePipes(new ValidationPipe()) + async drawMap(@Param('id') id, @Body() data: ReturnDrawingsDTO) { + return this.drawService.drawMap(id, data); + } +} diff --git a/src/draw/draw.module.ts b/src/draw/draw.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..a56cbbbb7fb49b476842177a4a0a9e7e0b592b3c --- /dev/null +++ b/src/draw/draw.module.ts @@ -0,0 +1,24 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { DrawController } from './draw.controller'; +import { DrawService } from './draw.service'; +import { MapDrawingEntity } from '../draw/coordinate.entity'; +import { FactionEntity } from '../faction/faction.entity'; +import { Game_PersonEntity } from '../game/game.entity'; +/* +Draw +- contains everything to do with mapdrawing data. +*/ +@Module({ + imports: [ + TypeOrmModule.forFeature([ + MapDrawingEntity, + FactionEntity, + Game_PersonEntity, + ]), + ], + controllers: [DrawController], + providers: [DrawService], +}) +export class DrawModule {} diff --git a/src/draw/draw.service.ts b/src/draw/draw.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..d5ddf23e9574771bc769e2b6d58b59a4e95ad10d --- /dev/null +++ b/src/draw/draw.service.ts @@ -0,0 +1,46 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; + +import { MapDrawingEntity } from '../draw/coordinate.entity'; +import { MapDrawingDTO, ReturnDrawingsDTO } from './mapdrawing.dto'; + +@Injectable() +export class DrawService { + constructor( + @InjectRepository(MapDrawingEntity) + private mapDrawingRepository: Repository<MapDrawingEntity>, + ) {} + + async draw(gameId, data: MapDrawingDTO) { + data['gameId'] = gameId; + const drawing = await this.mapDrawingRepository.create(data); + if (data.mapDrawingId == null || data.mapDrawingId == '') { + // luo uuden instanssin. + const mapDrawing = await this.mapDrawingRepository.insert(drawing); + return mapDrawing.identifiers; + } else { + //päivittää mapDrawingin + return await this.mapDrawingRepository.save(drawing); + } + } + + // draw map based on game and + async drawMap(id, data: ReturnDrawingsDTO) { + // return mapdrawings with given faction and gameid + return await this.mapDrawingRepository.find({ + where: [ + { + gameId: id, + faction: data.factionId, + drawingIsActive: true, + }, + { + gameId: id, + faction: null, + drawingIsActive: true, + }, + ], + }); + } +} diff --git a/src/draw/mapdrawing.dto.ts b/src/draw/mapdrawing.dto.ts new file mode 100644 index 0000000000000000000000000000000000000000..f7e7c781a0cbd308edf5f8c14f068b449f43a05e --- /dev/null +++ b/src/draw/mapdrawing.dto.ts @@ -0,0 +1,27 @@ +import { IsUUID, IsOptional, IsBoolean, Allow } from 'class-validator'; + +import { FactionEntity } from '../faction/faction.entity'; +import { GameEntity } from '../game/game.entity'; + +export class MapDrawingDTO { + @IsOptional() + @IsUUID('4') + mapDrawingId: string; + @Allow() + data: JSON; + @IsOptional() + @IsUUID('4') + gameId: GameEntity; + @IsOptional() + @IsUUID('4') + faction?: FactionEntity; + @IsBoolean() + drawingIsActive?: boolean; + drawingValidTill?: string; +} + +export class ReturnDrawingsDTO { + @IsOptional() + @IsUUID('4') + factionId: FactionEntity; +} diff --git a/src/faction/faction.controller.ts b/src/faction/faction.controller.ts new file mode 100644 index 0000000000000000000000000000000000000000..da7e5950626bbcd5385d80755d208e59be589b48 --- /dev/null +++ b/src/faction/faction.controller.ts @@ -0,0 +1,98 @@ +import { + Controller, + Post, + UseGuards, + UsePipes, + Param, + Body, + Get, + Put, + UseInterceptors, + ClassSerializerInterceptor, +} from '@nestjs/common'; + +import { AuthGuard } from '../shared/auth.guard'; +import { ValidationPipe } from '../shared/validation.pipe'; +import { User } from '../user/user.decorator'; +import { + GameGroupDTO, + PromotePlayerDTO, + JoinFactionDTO, + JoinGameGroupDTO, +} from './faction.dto'; +import { FactionService } from './faction.service'; +import { Roles, GameStates } from '../shared/guard.decorator'; + +@Controller('faction') +export class FactionController { + constructor(private factionservice: FactionService) {} + + // takes gameId from the url to verify user role + @Post('create-group/:id') + @Roles('soldier') + @GameStates('CREATED') + @UsePipes(new ValidationPipe()) + async createGroup( + @User('id') person, + @Param('id') id: string, + @Body() data: GameGroupDTO, + ) { + try { + return this.factionservice.createGroup(person, id, data); + } catch (error) {} + } + + // id is faction ID + @Get('get-groups/:id') + async getGroups(@Param('id') id) { + return this.factionservice.showGroups(id); + } + + // takes gameId from the url to verify user role + @Put('join-group/:id') + @Roles('soldier') + @GameStates('CREATED') + async joinGroup( + @User('id') person, + @Param('id') id, + @Body() data: JoinGameGroupDTO, + ) { + return this.factionservice.joinGroup(person, id, data); + } + + @UseInterceptors(ClassSerializerInterceptor) + @Get('get-faction-members/:id') + async getFactionMembers(@Param('id') factionId) { + return this.factionservice.listFactionMembers(factionId); + } + + // param game ID is passed to @Roles + @Put('promote/:id') + @Roles('admin') + @GameStates('CREATED') + @UsePipes(new ValidationPipe()) + promotePlayer(@Param('id') game, @Body() body: PromotePlayerDTO) { + return this.factionservice.promotePlayer(body); + } + + // used to join a faction + // :id is the id of the game, and is needed for GameStates to check the state of the game + @Put('join-faction/:id') + @UseGuards(new AuthGuard()) + @GameStates('CREATED', 'STARTED') + @UsePipes(new ValidationPipe()) + joinFaction( + @User('id') person, + @Param('id') game, + @Body() data: JoinFactionDTO, + ) { + return this.factionservice.joinFaction(person, data); + } + + // check if person belongs to a faction in a game + @Get('check-faction/:id') + @UseGuards(new AuthGuard()) + checkFaction(@User('id') userId, @Param('id') gameId) { + return this.factionservice.verifyUser(userId, gameId); + } +} diff --git a/src/faction/faction.dto.ts b/src/faction/faction.dto.ts new file mode 100644 index 0000000000000000000000000000000000000000..0b65c47007d6424584d8fdb35ac9d9ee53eaa924 --- /dev/null +++ b/src/faction/faction.dto.ts @@ -0,0 +1,64 @@ +import { + IsUUID, + Length, + Validate, + IsString, + IsNotEmpty, + IsNumber, + Min, + Max, + IsOptional, +} from 'class-validator'; + +import { GameEntity } from '../game/game.entity'; +import { RoleValidation, Uuid } from '../shared/custom-validation'; +import { GameDTO } from '../game/game.dto'; +import { FactionEntity, GameGroupEntity } from './faction.entity'; + +export class FactionDTO { + @IsOptional() + @IsUUID('4') + factionId?: string; + @IsString() + @IsNotEmpty() + @Length(2, 31) + factionName: string; + @IsString() + @IsNotEmpty() + @Length(3, 15) + factionPassword: string; + @IsNumber() + @Min(1) + @Max(3) + multiplier?: number; + game: GameDTO; +} + +export class JoinFactionDTO { + @IsUUID('4') + factionId: string; + @Length(3, 31) + factionPassword: string; + @IsUUID('4') + game: GameEntity; +} + +export class PromotePlayerDTO { + @IsUUID('4') + player: string; + @Validate(RoleValidation) + role: string; +} + +export class GameGroupDTO { + @IsString() + @Length(3, 31) + name: string; + @IsUUID('4') + faction: FactionEntity; +} + +export class JoinGameGroupDTO { + @IsUUID('4') + groupId: GameGroupEntity; +} diff --git a/src/game/faction.entity.ts b/src/faction/faction.entity.ts similarity index 55% rename from src/game/faction.entity.ts rename to src/faction/faction.entity.ts index 135d73a4108b3191e29c9e8921f1fbf5dffc9a37..3aa74cf615d77deb1421d52d46d4159bf7e0d241 100644 --- a/src/game/faction.entity.ts +++ b/src/faction/faction.entity.ts @@ -4,28 +4,48 @@ import { PrimaryGeneratedColumn, OneToMany, ManyToOne, + OneToOne, Timestamp, + JoinColumn, } from 'typeorm'; -import { GameEntity } from './game.entity'; -import { Game_PersonEntity } from './game.entity'; -import { MapDrawingEntity } from './coordinate.entity'; -//Faction, PowerUp, Faction_powerUp, FP_History, Score, Task +import { GameEntity } from '../game/game.entity'; +import { Game_PersonEntity } from '../game/game.entity'; +import { MapDrawingEntity } from '../draw/coordinate.entity'; +import { Exclude } from 'class-transformer'; + +//Faction, PowerUp, Faction_powerUp, FP_History, Score @Entity('Faction') export class FactionEntity { @PrimaryGeneratedColumn('uuid') factionId: string; @Column('text') factionName: string; - @Column({ type: 'text' }) factionPassword: string; @Column({ type: 'float' }) multiplier: number; + @Exclude() + @Column({ type: 'text' }) + factionPassword: string; + @OneToMany(type => Game_PersonEntity, game_persons => game_persons.faction) game_persons: Game_PersonEntity[]; - @ManyToOne(type => GameEntity, game => game.id) - gameId: GameEntity; + @ManyToOne(type => GameEntity, game => game.factions, { + onDelete: 'CASCADE', + }) + game: GameEntity; + @OneToMany(type => MapDrawingEntity, mapDrawings => mapDrawings.faction) + mapDrawings: MapDrawingEntity[]; + + factionObject() { + const { factionId, factionName, game } = this; + return { factionId, factionName, game }; + } + + passwordCheck(pass: string) { + return pass == this.factionPassword ? true : false; + } } -@Entity('PowerUp') +/* @Entity('PowerUp') export class PowerUpEntity { @PrimaryGeneratedColumn('uuid') powerUpId: string; @Column({ type: 'text' }) powerUpName: string; @@ -33,7 +53,9 @@ export class PowerUpEntity { @Column({ type: 'int' }) amount: number; @Column({ type: 'time' }) cooldown: string; - @OneToMany(type => FactionEntity, factions => factions.factionId) + @OneToMany(type => FactionEntity, factions => factions.factionId, { + onDelete: 'CASCADE', + }) factions: Faction_PowerUpEntity[]; } @@ -59,28 +81,23 @@ export class FP_HistoryEntity { faction_PowerUp => faction_PowerUp.histories, ) faction_PowerUp: Faction_PowerUpEntity; -} - -@Entity('Score') -export class ScoreEntity { - @PrimaryGeneratedColumn('uuid') scoreId: string; - @Column({ type: 'float' }) score: number; - @Column({ type: 'timestamp' }) scoreTimeStamp: Timestamp; +} */ - @ManyToOne(type => FactionEntity, factionName => factionName.factionId) - faction: FactionEntity; -} - -@Entity('Task') -export class TaskEntity { - @PrimaryGeneratedColumn('uuid') taskId: string; - @Column({ type: 'text' }) taskName: string; - @Column({ type: 'text' }) taskDescription: string; - @Column({ type: 'text' }) taskWinner: string; - @Column({ type: 'bool' }) taskIsActive: boolean; - - @ManyToOne(type => FactionEntity, faction => faction.factionId) +@Entity('GameGroup') +export class GameGroupEntity { + @PrimaryGeneratedColumn('uuid') id: string; + @Column('text') name: string; + @OneToOne(type => Game_PersonEntity, person => person.leaderGroup, { + onDelete: 'CASCADE', + }) + @JoinColumn({ name: 'leader' }) + leader: Game_PersonEntity; + @OneToMany(type => Game_PersonEntity, person => person.group, { + onDelete: 'CASCADE', + }) + players: Game_PersonEntity[]; + @ManyToOne(type => FactionEntity, faction => faction.factionId, { + onDelete: 'CASCADE', + }) faction: FactionEntity; - /* @ManyToOne(type => PersonEntity, person => person.tasks) - person: PersonEntity; */ } diff --git a/src/faction/faction.module.ts b/src/faction/faction.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..dcb399aa1119851781270477c448ecec7362c804 --- /dev/null +++ b/src/faction/faction.module.ts @@ -0,0 +1,20 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { FactionController } from './faction.controller'; +import { FactionService } from './faction.service'; +import { GameGroupEntity, FactionEntity } from './faction.entity'; +import { Game_PersonEntity } from '../game/game.entity'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([ + FactionEntity, + Game_PersonEntity, + GameGroupEntity, + ]), + ], + controllers: [FactionController], + providers: [FactionService], +}) +export class FactionModule {} diff --git a/src/faction/faction.service.ts b/src/faction/faction.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..67edef1db5d071e1936aba03ded5c5051c1ae713 --- /dev/null +++ b/src/faction/faction.service.ts @@ -0,0 +1,149 @@ +import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository, Not } from 'typeorm'; + +import { FactionEntity, GameGroupEntity } from './faction.entity'; +import { JoinFactionDTO, GameGroupDTO, JoinGameGroupDTO } from './faction.dto'; +import { Game_PersonEntity } from '../game/game.entity'; + +@Injectable() +export class FactionService { + constructor( + @InjectRepository(FactionEntity) + private factionRepository: Repository<FactionEntity>, + @InjectRepository(Game_PersonEntity) + private game_PersonRepository: Repository<Game_PersonEntity>, + @InjectRepository(GameGroupEntity) + private game_GroupRepository: Repository<GameGroupEntity>, + ) {} + + async joinFaction(person, faction: JoinFactionDTO) { + // get faction + const factionInDb = await this.factionRepository.findOne({ + factionId: faction.factionId, + }); + + if (!factionInDb) { + throw new HttpException('No factions exist!', HttpStatus.BAD_REQUEST); + } + //check if password is correct + if (factionInDb.passwordCheck(faction.factionPassword)) { + const gameperson = await this.game_PersonRepository.create({ + faction: factionInDb, + game: faction.game, + role: 'soldier', + person: person, + }); + //check if user is already in a faction + if ( + await this.game_PersonRepository.findOne({ + person: person, + game: faction.game, + }) + ) { + throw new HttpException( + 'You have already joined faction!', + HttpStatus.BAD_REQUEST, + ); + } + // insert to database + return await this.game_PersonRepository.save(gameperson); + } else { + throw new HttpException('Invalid password!', HttpStatus.UNAUTHORIZED); + } + } + + async promotePlayer(body) { + const gamepersonId = body.player; + // get playerdata + const gameperson = await this.game_PersonRepository.findOne({ + where: { gamepersonId }, + }); + if (gameperson) { + const factionleader = await this.game_PersonRepository.create(gameperson); + factionleader.role = body.role; + return await this.game_PersonRepository.save(factionleader); + } + throw new HttpException('player does not exist', HttpStatus.BAD_REQUEST); + } + + // checks the password, creates an entry in GamePerson table with associated role&faction + async createGroup(person, gameId, groupData: GameGroupDTO) { + // get gamePerson ref + const gamePerson = await this.game_PersonRepository.findOne({ + person: person, + game: gameId, + }); + // check if the authenticated person already belongs to a group + if ( + await this.game_PersonRepository.findOne({ + group: Not(null), + person: person, + }) + ) { + throw new HttpException( + 'You already belong to a group!', + HttpStatus.BAD_REQUEST, + ); + } + + // create a group entry and insert it to db + const group = await this.game_GroupRepository.create({ + ...groupData, + leader: gamePerson, + }); + const gameGroup = await this.game_GroupRepository.insert(group); + + // update the gamePerson entry with group data + gamePerson.group = gamePerson.leaderGroup = gameGroup.identifiers[0]['id']; + await this.game_PersonRepository.save(gamePerson); + + return { + message: 'created new group', + }; + } + + async showGroups(factionId) { + return await this.game_GroupRepository.find({ + relations: ['leader', 'players'], + where: { faction: factionId }, + }); + } + + async joinGroup(person, gameId, data: JoinGameGroupDTO) { + const gamePerson = await this.game_PersonRepository.findOne({ + person: person, + game: gameId, + }); + gamePerson.group = data.groupId; + await this.game_PersonRepository.save(gamePerson); + return { + message: 'Joined group', + }; + } + + async listFactionMembers(faction) { + const members = await this.game_PersonRepository.find({ + where: { faction }, + relations: ['person'], + }); + members.sort(function(a, b) { + return a['person']['name'].localeCompare(b['person']['name']); + }); + return members; + } + + async verifyUser(person, game) { + const gameperson = await this.game_PersonRepository.findOne({ + where: { person, game }, + relations: ['faction'], + }); + if (gameperson) { + return { + message: gameperson, + }; + } else { + throw new HttpException('No faction was found', HttpStatus.BAD_REQUEST); + } + } +} diff --git a/src/game/coordinate.entity.ts b/src/game/coordinate.entity.ts deleted file mode 100644 index 851bb65c1b23ad74c06b2eb479710f5d121b08fa..0000000000000000000000000000000000000000 --- a/src/game/coordinate.entity.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { - Entity, - Column, - PrimaryGeneratedColumn, - ManyToOne, - Timestamp, -} from 'typeorm'; -import { GameEntity, Game_PersonEntity } from './game.entity'; -import { FactionEntity } from './faction.entity'; - -@Entity('MapDrawing') -export class MapDrawingEntity { - @PrimaryGeneratedColumn('uuid') mapDrawingId: string; - @Column({ type: 'bool' }) drawingIsActive: boolean; - @Column({ type: 'time' }) drawingValidTill: string; - - @Column({ type: 'json', nullable: true }) data: JSON; - - @ManyToOne(type => FactionEntity, faction => faction.factionId) - faction: FactionEntity; - @ManyToOne(type => GameEntity, gameEntity => gameEntity.id) - gameId: GameEntity; -} - -@Entity('Game_Person_MapDrawing') -export class Game_Person_MapDrawingEntity { - @PrimaryGeneratedColumn('uuid') GPmapDrawingId: string; - @Column({ type: 'timestamp' }) GPCTimeStamp: Timestamp; - - @ManyToOne(type => Game_PersonEntity, game_person => game_person.gamepersonId) - game_person: Game_PersonEntity; - @ManyToOne(type => MapDrawingEntity, map_drawing => map_drawing.mapDrawingId) - map_drawing: MapDrawingEntity; -} diff --git a/src/game/game.controller.spec.ts b/src/game/game.controller.spec.ts deleted file mode 100644 index 23d9a2f9c849e6b63b31ac707abca1cef9874012..0000000000000000000000000000000000000000 --- a/src/game/game.controller.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { GameController } from './game.controller'; - -describe('Game Controller', () => { - let controller: GameController; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [GameController], - }).compile(); - - controller = module.get<GameController>(GameController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); -}); diff --git a/src/game/game.controller.ts b/src/game/game.controller.ts index edff9d8e21ae4bd667b1eecb7162cacede906678..8d7494ba4dfa4a77e22963600e45a7579fca5d5d 100644 --- a/src/game/game.controller.ts +++ b/src/game/game.controller.ts @@ -7,14 +7,18 @@ import { Param, UsePipes, Put, + UseInterceptors, + ClassSerializerInterceptor, + Delete, } from '@nestjs/common'; import { GameService } from './game.service'; import { AuthGuard } from '../shared/auth.guard'; import { User } from '../user/user.decorator'; -import { GameDTO, GameGroupDTO, FlagboxEventDTO } from './game.dto'; +import { GameDTO, FlagboxEventDTO, GameStateDTO } from './game.dto'; import { ValidationPipe } from '../shared/validation.pipe'; -import { Roles } from '../shared/roles.decorator'; +import { Roles, GameStates } from '../shared/guard.decorator'; +import { GameEntity } from './game.entity'; @Controller('game') export class GameController { @@ -27,50 +31,27 @@ export class GameController { return this.gameservice.createNewGame(person, body); } - @Put(':id') + @Put('edit/:id') @Roles('admin') + @GameStates('CREATED') @UsePipes(new ValidationPipe()) async editGame(@Param('id') id: string, @Body() body: GameDTO) { + body.id = id; return this.gameservice.editGame(id, body); } - @Post(':id') - @UseGuards(new AuthGuard()) - @UsePipes(new ValidationPipe()) - async createGroup( - @User('id') person, - @Param('id') id: string, - @Body() data: GameGroupDTO, - ) { - try { - return this.gameservice.createGroup(person, id, data); - } catch (error) {} - } - - @Get('get-groups') - async getGroups() { - return this.gameservice.showGroups(); - } - - @Put('groups/:id') - @UseGuards(new AuthGuard()) - async joinGroup(@User('id') person, @Param('id') id) { - return this.gameservice.joinGroup(person, id); + @Delete('delete/:id') + @Roles('admin') + @GameStates('CREATED') + async deleteGame(@Param('id') id: string) { + return this.gameservice.deleteGame(id); } - @Put('joinfaction') - @UseGuards(new AuthGuard()) - joinFaction( - @User('id') person, - @Param('id') gameId, - @Param('faction') faction: string, - @Body() password: string, - ) { - try { - // return this.gameservice.joinFaction(person, gameId, faction, password); - } catch (error) { - return error; - } + @Put('edit-state/:id') + @Roles('admin') + @UsePipes(new ValidationPipe()) + async updateGameState(@Param('id') id: string, @Body() body: GameStateDTO) { + return this.gameservice.updateGameStatus(body); } @Get('listgames') @@ -78,17 +59,26 @@ export class GameController { return this.gameservice.listGames(); } + // ClassSerializerInterceptor removes excluded columns set in Entities + @UseInterceptors(ClassSerializerInterceptor) @Get(':id') async returnGameInfo(@Param('id') id: string) { return this.gameservice.returnGameInfo(id); } + @Get('get-factions/:id') + @Roles('admin') + async returnGameFactions(@Param('id') id: GameEntity) { + return this.gameservice.listFactions(id); + } + @Get('flag/:id') async flagboxQuery(@Param('id') id: string) { return this.gameservice.flagboxQuery(id); } @Post('flag/:id') + @GameStates('STARTED') async flagboxEvent(@Param('id') id: string, @Body() data: FlagboxEventDTO) { return this.gameservice.flagboxEvent(id, data); } diff --git a/src/game/game.dto.ts b/src/game/game.dto.ts index 952956dd30a375bd1c6be4c417757d69288fbc3c..266a8b1564e5b11fb42da755359c79d37f4b170b 100644 --- a/src/game/game.dto.ts +++ b/src/game/game.dto.ts @@ -1,20 +1,28 @@ import { IsNotEmpty, IsString, - IsDate, Length, - IsInt, - Min, - Max, - IsArray, - IsJSON, IsDateString, IsNumber, + Validate, + Min, + Max, + ValidateNested, + Allow, + IsUUID, + IsIn, + IsOptional, } from 'class-validator'; -import { Timestamp } from 'typeorm'; + import { ObjectivePointEntity } from './game.entity'; +import { CenterJSON } from '../shared/custom-validation'; +import { FactionDTO } from '../faction/faction.dto'; +import { CenterDTO, NodeSettingsDTO } from './game.json.dto'; +import { Type } from 'class-transformer'; export class GameDTO { + @IsOptional() + id: string; @IsString() @IsNotEmpty() @Length(3, 30) @@ -22,37 +30,60 @@ export class GameDTO { @IsNotEmpty() @Length(1, 255) desc: string; - @IsNotEmpty() - //@IsJSON() - center: JSON; - //@IsJSON() - // doesn't accept with IsJSON, WIP to get validation for map and center - // IsJSON checks with json.parse, expecting string + @ValidateNested() + @Type(() => CenterDTO) + center: CenterDTO; + @Allow() + @ValidateNested() + @Type(() => NodeSettingsDTO) + nodesettings?: NodeSettingsDTO; + @Allow() map?: JSON; - nodesettings?: JSON; @IsDateString() @IsNotEmpty() startdate: string; @IsDateString() @IsNotEmpty() enddate: string; - // custom validation for array length (arr>min, arr<max) - //@Validate(ArrayLength, [4, 8]) + @ValidateNested() + @Type(() => FactionDTO) factions?: FactionDTO[]; + @ValidateNested() + @Type(() => FlagboxDTO) objective_points?: FlagboxDTO[]; } -export class FactionDTO { +export class newGameDTO { @IsString() @IsNotEmpty() - @Length(2, 15) - factionName: string; - factionPassword: string; - multiplier: number; - game: GameDTO; + @Length(3, 30) + name: string; + @IsString() + @IsNotEmpty() + @Length(1, 255) + desc: string; + @IsNotEmpty() + @Validate(CenterJSON) + center: JSON; + @IsDateString() + @IsNotEmpty() + startdate: string; + @IsDateString() + @IsNotEmpty() + enddate: string; +} + +export class GameStateDTO { + @IsUUID('4') + id: string; + @IsIn(['CREATED', 'STARTED', 'PAUSED', 'ENDED']) + state: string; } export class FlagboxDTO { + @IsOptional() + @IsUUID('4') + objectivePointId: string; @IsString() @IsNotEmpty() @Length(7) @@ -62,16 +93,22 @@ export class FlagboxDTO { } export class FlagboxEventDTO { + @IsString() + @IsNotEmpty() + @Length(7) node_id: string; - owner: number; - action: number; - capture: number; + @IsNumber() + @Min(0) + @Max(3) + owner: number; // owner = 0, => no owner, owner = 1, => first entry in faction db + @IsNumber() + @Min(0) + @Max(3) + action: number; // 0=no capture ongoing, 1=captured, 2=capture ongoing + @IsNumber() + @Min(0) + @Max(3) + capture: number; // which faction is capturing, same logic as in owner with numbers oP_HistoryTimestamp?: string; objective_point?: ObjectivePointEntity; } - -export class GameGroupDTO { - @IsString() - @Length(3, 31) - name: string; -} diff --git a/src/game/game.entity.ts b/src/game/game.entity.ts index 36813094bc3a87c0903cb94f81260a42be803116..7ae6e29955d02a2c1368b5f720200f009bd92ee1 100644 --- a/src/game/game.entity.ts +++ b/src/game/game.entity.ts @@ -9,13 +9,12 @@ import { JoinColumn, } from 'typeorm'; +import { MapDrawingEntity } from '../draw/coordinate.entity'; import { PersonEntity } from '../user/user.entity'; -import { GameGroupEntity } from './group.entity'; -import { FactionEntity, TaskEntity } from './faction.entity'; -import { - MapDrawingEntity, - Game_Person_MapDrawingEntity, -} from './coordinate.entity'; +import { GameGroupEntity } from '../faction/faction.entity'; +import { FactionEntity } from '../faction/faction.entity'; +import { TaskEntity } from '../task/task.entity'; +import { CenterDTO, NodeSettingsDTO } from './game.json.dto'; // table that stores all created games @Entity('Game') @@ -23,23 +22,24 @@ export class GameEntity { @PrimaryGeneratedColumn('uuid') id: string; @Column('text') name: string; @Column('text') desc: string; - @Column('json') center: JSON; + @Column('json') center: CenterDTO; @Column({ type: 'json', nullable: true }) map: JSON; - @Column({ type: 'json', nullable: true }) nodesettings?: JSON; + @Column({ type: 'json', nullable: true }) nodesettings?: NodeSettingsDTO; + @Column('text') state: string; @Column('timestamp') startdate: Timestamp; @Column('timestamp') enddate: Timestamp; - @OneToMany(type => FactionEntity, factions => factions.factionId) + @OneToMany(type => FactionEntity, factions => factions.game) factions: FactionEntity[]; @OneToMany(type => Game_PersonEntity, game_persons => game_persons.game) game_persons: Game_PersonEntity[]; - @OneToMany(type => GameGroupEntity, group => group.game) - groups: GameGroupEntity[]; @OneToMany( type => ObjectivePointEntity, objective_points => objective_points.game, ) objective_points: ObjectivePointEntity[]; + @OneToMany(type => TaskEntity, tasks => tasks.taskGame) + tasks: TaskEntity[]; gameObject() { const { id, name } = this; @@ -48,23 +48,28 @@ export class GameEntity { } // table that stores players associated with particular game -@Entity('Game_Person') +@Entity('Game_Person', { + orderBy: { + person: 'ASC', + }, +}) export class Game_PersonEntity { @PrimaryGeneratedColumn('uuid') gamepersonId: string; @Column({ type: 'text', nullable: true }) role: string; - @ManyToOne(type => FactionEntity, faction => faction) + @ManyToOne(type => FactionEntity, faction => faction.game_persons, { + onDelete: 'CASCADE', + }) faction: FactionEntity; - @ManyToOne(type => GameEntity, game => game.id) + @ManyToOne(type => GameEntity, game => game.id, { + onDelete: 'CASCADE', + }) game: GameEntity; @ManyToOne(type => PersonEntity, person => person.id) person: PersonEntity; - @OneToOne(type => GameGroupEntity, group => group.leader, { - onDelete: 'CASCADE', - }) - @JoinColumn({ name: 'leaderGroup' }) + @OneToOne(type => GameGroupEntity, group => group.leader) leaderGroup: GameGroupEntity; @ManyToOne(type => GameGroupEntity, group => group.players, { - onDelete: 'CASCADE', + onDelete: 'NO ACTION', }) @JoinColumn({ name: 'group' }) group: GameGroupEntity; @@ -76,9 +81,13 @@ export class ObjectivePointEntity { @Column({ type: 'text' }) objectivePointDescription: string; @Column({ type: 'float' }) objectivePointMultiplier: number; - @ManyToOne(type => MapDrawingEntity, coordinate => coordinate.data) + @ManyToOne(type => MapDrawingEntity, coordinate => coordinate.data, { + onDelete: 'CASCADE', + }) coordinate: MapDrawingEntity; - @ManyToOne(type => GameEntity, game => game.objective_points) + @ManyToOne(type => GameEntity, game => game.objective_points, { + onDelete: 'CASCADE', + }) game: GameEntity; } @@ -87,13 +96,20 @@ export class ObjectivePoint_HistoryEntity { @PrimaryGeneratedColumn('uuid') oP_HistoryId: string; @Column({ type: 'timestamp' }) oP_HistoryTimestamp: Timestamp; @Column('float') action: number; - @ManyToOne(type => FactionEntity, factionEntity => factionEntity.factionId) + @ManyToOne(type => FactionEntity, factionEntity => factionEntity.factionId, { + onDelete: 'CASCADE', + }) capture: FactionEntity; - @ManyToOne(type => FactionEntity, factionentity => factionentity.factionId) + @ManyToOne(type => FactionEntity, factionentity => factionentity.factionId, { + onDelete: 'CASCADE', + }) owner: FactionEntity; @ManyToOne( type => ObjectivePointEntity, objective_point => objective_point.objectivePointId, + { + onDelete: 'CASCADE', + }, ) - objective_point: ObjectivePointEntity; + objective_point: string; } diff --git a/src/game/game.json-nested.dto.ts b/src/game/game.json-nested.dto.ts new file mode 100644 index 0000000000000000000000000000000000000000..df67071f089caf3670e46188f92959cfa4e349ad --- /dev/null +++ b/src/game/game.json-nested.dto.ts @@ -0,0 +1,16 @@ +import { IsNumber } from 'class-validator'; + +export class NodeCoreSettingsDTO { + @IsNumber() + capture_time: number; + @IsNumber() + confirmation_time: number; + @IsNumber() + owner: number; + @IsNumber() + capture: number; + @IsNumber() + buttons_available: number; + @IsNumber() + heartbeat_interval: number; +} diff --git a/src/game/game.json.dto.ts b/src/game/game.json.dto.ts new file mode 100644 index 0000000000000000000000000000000000000000..5b9484aaa3334d6d564398a96c7e2747f86aab84 --- /dev/null +++ b/src/game/game.json.dto.ts @@ -0,0 +1,16 @@ +import { IsNumber, ValidateNested } from 'class-validator'; +import { NodeCoreSettingsDTO } from './game.json-nested.dto'; +import { Type } from 'class-transformer'; + +export class CenterDTO { + @IsNumber() + lat: number; + @IsNumber() + lng: number; +} + +export class NodeSettingsDTO { + @ValidateNested() + @Type(() => NodeCoreSettingsDTO) + node_settings: NodeCoreSettingsDTO; +} diff --git a/src/game/game.module.ts b/src/game/game.module.ts index c908e483b7b1a867d5d8dfb8e8fab54ea212ecfe..1c9e86faf59645bf537e76a8720bb6ab741fe6bd 100644 --- a/src/game/game.module.ts +++ b/src/game/game.module.ts @@ -10,8 +10,8 @@ import { ObjectivePoint_HistoryEntity, } from './game.entity'; import { PersonEntity } from '../user/user.entity'; -import { GameGroupEntity } from './group.entity'; -import { FactionEntity } from './faction.entity'; +import { GameGroupEntity } from '../faction/faction.entity'; +import { FactionEntity } from '../faction/faction.entity'; import { NotificationModule } from '../notifications/notifications.module'; @Module({ diff --git a/src/game/game.service.spec.ts b/src/game/game.service.spec.ts deleted file mode 100644 index f4a1db7e70bf2a0e38c6d430c95e54feb3934fdf..0000000000000000000000000000000000000000 --- a/src/game/game.service.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { GameService } from './game.service'; - -describe('GameService', () => { - let service: GameService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [GameService], - }).compile(); - - service = module.get<GameService>(GameService); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); -}); diff --git a/src/game/game.service.ts b/src/game/game.service.ts index ea11a669de95f43f89f8b3a7f798358e63421bbe..3f6506b649f7dbe09936e283b4658f9765be8ec4 100644 --- a/src/game/game.service.ts +++ b/src/game/game.service.ts @@ -8,11 +8,10 @@ import { ObjectivePointEntity, ObjectivePoint_HistoryEntity, } from './game.entity'; -import { GameDTO, FlagboxEventDTO } from './game.dto'; +import { GameDTO, FlagboxEventDTO, GameStateDTO } from './game.dto'; import { PersonEntity } from '../user/user.entity'; -import { GameGroupEntity } from './group.entity'; -import { FactionEntity } from './faction.entity'; -import { NotificationGateway } from 'src/notifications/notifications.gateway'; +import { FactionEntity } from '../faction/faction.entity'; +import { NotificationGateway } from '../notifications/notifications.gateway'; @Injectable() export class GameService { @@ -21,12 +20,8 @@ export class GameService { private gameRepository: Repository<GameEntity>, @InjectRepository(FactionEntity) private factionRepository: Repository<FactionEntity>, - @InjectRepository(PersonEntity) - private personRepository: Repository<PersonEntity>, @InjectRepository(Game_PersonEntity) private game_PersonRepository: Repository<Game_PersonEntity>, - @InjectRepository(GameGroupEntity) - private game_GroupRepository: Repository<GameGroupEntity>, @InjectRepository(ObjectivePointEntity) private objectivePointRepository: Repository<ObjectivePointEntity>, @InjectRepository(ObjectivePoint_HistoryEntity) @@ -38,191 +33,148 @@ export class GameService { // create a new game async createNewGame(personId: PersonEntity, gameData: GameDTO) { - try { - // checks if a game with the same name exists already - const { name } = gameData; - if (await this.gameRepository.findOne({ where: { name } })) { - throw new HttpException('Game already exists', HttpStatus.BAD_REQUEST); - } - // else add the game to the database - const game = await this.gameRepository.create({ - ...gameData, - factions: gameData.factions, - }); - await this.gameRepository.insert(game); - const gamePerson = await this.game_PersonRepository.create({ - faction: null, - game: game, - person: personId, - }); - gamePerson['role'] = 'admin'; - await this.game_PersonRepository.insert(gamePerson); - return { - message: 'New game added', - }; - } catch (error) { - return error.message; + // checks if a game with the same name exists already + if (await this.gameRepository.findOne({ name: gameData.name })) { + throw new HttpException('Game already exists', HttpStatus.BAD_REQUEST); } + // else add the game to the database + const game = await this.gameRepository.create(gameData); + game.state = 'CREATED'; + await this.gameRepository.insert(game); + // add gamePerson with role admin to the game + const gamePerson = await this.game_PersonRepository.create({ + game: game, + person: personId, + role: 'admin', + }); + await this.game_PersonRepository.insert(gamePerson); + return { + message: 'New game added', + }; } // edit already created game - async editGame(id: string, gameData: GameDTO) { - try { - // checks if a game with the same name exists already - const { name } = gameData; - if (await this.gameRepository.findOne({ name: name, id: Not(id) })) { - throw new HttpException('Game already exists', HttpStatus.BAD_REQUEST); - } - // update game entry in db - const updatedGame = await this.gameRepository.create({ - ...gameData, - factions: null, - objective_points: null, - }); - updatedGame['id'] = id; - const gameId = await this.gameRepository.save(updatedGame); + async editGame(id, gameData: GameDTO) { + // checks if a game with the same name exists already + if ( + await this.gameRepository.findOne({ name: gameData.name, id: Not(id) }) + ) { + throw new HttpException( + 'Game with the same name already exists', + HttpStatus.BAD_REQUEST, + ); + } - // get all the factions that are associated with the game to deny duplicate entries - const factions = await this.factionRepository.find(gameId); - const factionNames = factions.map(({ factionName }) => factionName); - // add the factions to db - if (gameData.factions) { - gameData.factions.map(async faction => { - if (!Object.values(factionNames).includes(faction.factionName)) { - let name = await this.factionRepository.create({ - ...faction, - gameId: gameId, - }); - await this.factionRepository.insert(name); - } - }); - } + // check for duplicate names in gameData + const factionNames = gameData.factions.map( + ({ factionName }) => factionName, + ); + const flagboxNodeIds = gameData.objective_points.map( + ({ objectivePointDescription }) => objectivePointDescription, + ); + if ( + new Set(factionNames).size !== factionNames.length || + new Set(flagboxNodeIds).size !== flagboxNodeIds.length + ) { + throw new HttpException( + 'No duplicate names allowed!', + HttpStatus.BAD_REQUEST, + ); + } - // get old flagboxes to deny duplicate entries - const flagboxes = await this.objectivePointRepository.find({ - game: gameId, + // get factions that have been added previously + let factions = await this.factionRepository.find({ game: id }); + // get flagboxes that have been added previously + let flagboxes = await this.objectivePointRepository.find({ + game: id, + }); + // update game entry in db + const updatedGame = await this.gameRepository.create(gameData); + const gameId = await this.gameRepository.save(updatedGame); + // iterate factions if any were added + if (gameData.factions) { + const factionIds = gameData.factions.map(({ factionId }) => factionId); + // delete all existing factions that are not in submitted data + factions.map(async faction => { + if (!factionIds.includes(faction.factionId)) { + await this.factionRepository.delete(faction); + } }); - const flagboxIds = flagboxes.map( - ({ objectivePointDescription }) => objectivePointDescription, - ); - // insert the flagboxes to db - if (gameData.objective_points) { - gameData.objective_points.map(async flagbox => { - if ( - !Object.values(flagboxIds).includes( - flagbox.objectivePointDescription, - ) - ) { - let newFlagbox = await this.objectivePointRepository.create({ - ...flagbox, - game: gameId, - }); - await this.objectivePointRepository.insert(newFlagbox); - } + // create / update factions present in the submitted data + gameData.factions.map(async faction => { + let name = await this.factionRepository.create({ + ...faction, + game: gameId, }); - } - - // TO DO: ADD FLAGBOX LOCATION TO MAPDRAWING ENTITY + await this.factionRepository.save(name); + }); + } else { + // if no factions are present in data, delete all factions associated with the game + await this.factionRepository.delete({ game: id }); + } - return { - message: 'Game updated', - }; - } catch (error) { - return error; + // insert the flagboxes to db + if (gameData.objective_points) { + const flagboxIds = gameData.objective_points.map( + ({ objectivePointId }) => objectivePointId, + ); + flagboxes.map(async flagbox => { + if (!flagboxIds.includes(flagbox.objectivePointDescription)) { + await this.objectivePointRepository.delete(flagbox); + } + }); + gameData.objective_points.map(async flagbox => { + let newFlagbox = await this.objectivePointRepository.create({ + ...flagbox, + game: gameId, + }); + await this.objectivePointRepository.save(newFlagbox); + }); + } else { + await this.objectivePointRepository.delete({ game: id }); } - } - async deleteGame(id) { - // TODO: Delete factions from Faction table associated with the deleted game - await this.gameRepository.delete({ id }); + // TO DO: ADD FLAGBOX LOCATION TO MAPDRAWING ENTITY + return { - message: 'Game deleted', + message: 'Game updated', }; } - // checks the password, creates an entry in GamePerson table with associated role&faction - async createGroup(person, gameId, groupData) { - try { - // check if the person already is in a group in this game - const checkDuplicate = await this.game_PersonRepository.findOne({ - person: person, - }); - if (checkDuplicate) { - throw new HttpException( - 'You already belong to a group!', - HttpStatus.BAD_REQUEST, - ); - } - - // create a group entry and insert it to db - const group = await this.game_GroupRepository.create({ - ...groupData, - game: gameId, - }); - const gameGroup = await this.game_GroupRepository.insert(group); - - // create game_Person entry and insert it to db - const gamePerson = await this.game_PersonRepository.create({ - role: 'soldier', - faction: null, - game: gameId, - person: person, - leaderGroup: gameGroup.identifiers[0]['id'], - group: gameGroup.identifiers[0]['id'], + async updateGameStatus(game: GameStateDTO) { + const updatedGame = await this.gameRepository.findOne({ id: game.id }); + if (updatedGame) { + updatedGame.state = game.state; + await this.gameRepository.save(updatedGame); + // notify players about game state change + this.notificationGateway.server.emit(game.id, { + type: 'gamestate-update', }); - await this.game_PersonRepository.insert(gamePerson); - return { - message: 'created new group', + message: 'State was updated', }; - } catch (e) { - return e; } + throw new HttpException("Game doesn't exist", HttpStatus.BAD_REQUEST); } - async showGroups() { - try { - return await this.game_GroupRepository.find({ - relations: ['leader', 'players', 'game'], - }); - } catch (e) { - return e; - } + async listFactions(game: GameEntity) { + return this.factionRepository.find({ game }); } - async joinGroup(person, groupId) { - try { - const gameData = await this.game_GroupRepository.findOne({ - where: { id: groupId }, - relations: ['players', 'game'], - }); - const gamePerson = await this.game_PersonRepository.create({ - role: 'soldier', - faction: null, - game: gameData.game, - person: person, - leaderGroup: null, - group: groupId, - }); - await this.game_PersonRepository.insert(gamePerson); - return { - message: 'Joined group', - }; - } catch (e) { - return e; - } + async deleteGame(id) { + // TODO: Delete factions from Faction table associated with the deleted game + await this.gameRepository.delete({ id }); + return { + message: 'Game deleted', + }; } // returns name and id of each game async listGames() { - try { - const games = await this.gameRepository.find(); - return games.map(game => { - return game.gameObject(); - }); - } catch (error) { - return error; - } + const games = await this.gameRepository.find(); + return games.map(game => { + return game.gameObject(); + }); } // returns information about a game identified by id @@ -231,6 +183,10 @@ export class GameService { where: { id: id }, relations: ['factions', 'objective_points'], }); + // sort factions by their name + game.factions.sort(function(a, b) { + return a['factionName'].localeCompare(b['factionName']); + }); return game; } @@ -243,7 +199,7 @@ export class GameService { // add events to history and send updates with socket async flagboxEvent(gameId, data: FlagboxEventDTO) { // get all the factions associated with the game - const factionRef = await this.factionRepository.find({ gameId: gameId }); + const factionRef = await this.factionRepository.find({ game: gameId }); // get reference to the objective const objectiveRef = await this.objectivePointRepository.findOne({ where: { objectivePointDescription: data.node_id, game: gameId }, @@ -252,12 +208,16 @@ export class GameService { const eventUpdate = await this.objectivePoint_HistoryRepository.create({ oP_HistoryTimestamp: data.oP_HistoryTimestamp, action: data.action, - capture: factionRef[data.capture], - owner: factionRef[data.owner], - objective_point: objectiveRef, + // -1 as 0 means null + capture: data.capture !== 0 ? factionRef[data.capture - 1] : null, + owner: data.owner !== 0 ? factionRef[data.owner - 1] : null, + objective_point: objectiveRef.objectivePointId, }); await this.objectivePoint_HistoryRepository.insert(eventUpdate); // send flagbox event to flagbox subscribers - this.notificationGateway.server.emit('flagbox', 'event update'); + this.notificationGateway.server.emit(gameId, { type: 'flagbox-event' }); + return { + message: 'OK', + }; } } diff --git a/src/game/group.entity.ts b/src/game/group.entity.ts deleted file mode 100644 index 0b9f00ed621b41b8515a77126f9564c14a02d74b..0000000000000000000000000000000000000000 --- a/src/game/group.entity.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { - Entity, - Column, - PrimaryGeneratedColumn, - ManyToOne, - OneToMany, - OneToOne, - JoinColumn, - } from 'typeorm'; - -import { Game_PersonEntity, GameEntity } from './game.entity'; - -@Entity('GameGroup') -export class GameGroupEntity { - @PrimaryGeneratedColumn('uuid') id: string; - @Column('text') name: string; - @OneToOne(type => Game_PersonEntity, person => person.leaderGroup, { - onDelete: 'CASCADE' - }) - //@JoinColumn({name:'leader'}) - leader: Game_PersonEntity; - @OneToMany(type => Game_PersonEntity, person => person.group, { - onDelete: 'CASCADE' - }) - players: Game_PersonEntity[]; - @ManyToOne(type => GameEntity, game => game.groups) - game: GameEntity; -} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 1e3e4e147bc386a4ce15a36bf274face5fd14c1e..68871be82d89a37a410b93b252fb6f1e712ad143 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,7 +3,6 @@ import * as rateLimit from 'express-rate-limit'; import { AppModule } from './app.module'; - /* Main.ts starts the server. */ @@ -13,7 +12,7 @@ import { AppModule } from './app.module'; // see https://github.com/nfriedly/express-rate-limit/issues/138 const limiter = (rateLimit as any)({ windowMs: 60 * 1000, // one minute - max: 100 // limit each IP to 100 requests per windowMs + max: 100, // limit each IP to 100 requests per windowMs }); async function bootstrap() { diff --git a/src/mapmarkers/mapmarker.dto.ts b/src/mapmarkers/mapmarker.dto.ts deleted file mode 100644 index f9821f4cbc73b62582263ac63e29c07a75a46b44..0000000000000000000000000000000000000000 --- a/src/mapmarkers/mapmarker.dto.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { IsString, IsJSON } from 'class-validator'; -/* -DTO: MapMarker -- represents servers data handling. -*/ - - -export class MapMarkerDTO { - @IsString() - type:string; - @IsString() - latitude: string; - @IsString() - longitude: string; - @IsString() - timestamp: string; - @IsJSON() - features: JSON; -} \ No newline at end of file diff --git a/src/mapmarkers/mapmarker.entity.ts b/src/mapmarkers/mapmarker.entity.ts deleted file mode 100644 index 0b26cbaa9a869fd81c7669a557b233a32c67b0f4..0000000000000000000000000000000000000000 --- a/src/mapmarkers/mapmarker.entity.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { - Entity, - Column, - PrimaryGeneratedColumn, - Timestamp, - ManyToOne, -} from 'typeorm'; - -import { PersonEntity } from '../user/user.entity'; -import { GameEntity, Game_PersonEntity } from 'src/game/game.entity'; - -/* - Entity: MapMarker - - represents data that database contains on mapmarker - */ - -/* @Entity('MapMarker') - export class MapMarkerEntity { - @PrimaryGeneratedColumn('uuid') id: string; - @Column({ type: 'text', nullable: true }) latitude: string; - @Column({ type: 'text', nullable: true }) longitude: string; - @Column({ type: 'timestamp' }) timestamp: Timestamp; - @Column({ type: 'json', nullable: true }) features: JSON; - @ManyToOne(type => Game_PersonEntity, player => player.markers) - player: Game_PersonEntity; - } - */ diff --git a/src/mapmarkers/mapmarker.service.ts b/src/mapmarkers/mapmarker.service.ts deleted file mode 100644 index f320d17b0892cec302281a1ee6f99b13fb788b78..0000000000000000000000000000000000000000 --- a/src/mapmarkers/mapmarker.service.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { Repository } from 'typeorm'; -import { InjectRepository } from '@nestjs/typeorm'; - -import { MapMarkerEntity } from './mapmarker.entity'; -import { MapMarkerDTO } from './mapmarker.dto'; -import { PersonEntity } from '../user/user.entity'; - -@Injectable() -export class MapMarkerService { - constructor( - //create references to tables as repositories - @InjectRepository(MapMarkerEntity) - private mapmarkerRepository: Repository<MapMarkerEntity>, - @InjectRepository(PersonEntity) - private personRepository: Repository<PersonEntity>, - ) {} - - // insert marker - async insertLocation(personId: string, data: MapMarkerDTO) { - try { - //get functions runtime as timestamp - data.timestamp = new Date(Date.now()).toLocaleString(); - //check from database for the user who uploads the data - const user = await this.personRepository.findOne({ - where: { id: personId }, - }); - //create© entity properties - const location = await this.mapmarkerRepository.create({ - ...data, - }); - // insert created entity NOTE: insert method doesn't check for duplicates. - await this.mapmarkerRepository.insert(location); - // return data and player id&name - return { ...data }; - } catch (error) { - return error; - } - } - - // get all markers - async getAllMarkers() { - try { - // find all markers with specified player - const markers = await this.mapmarkerRepository.find({ - relations: ['player'], - }); - // return markers from database with said playerdata - return markers.map(marker => { - return { ...marker }; - }); - } catch (error) { - return error.message; - } - } -} diff --git a/src/mapmarkers/mapmarkers.controller.spec.ts b/src/mapmarkers/mapmarkers.controller.spec.ts deleted file mode 100644 index 95cbd74e5c45f9713337458335c52257bf5d1220..0000000000000000000000000000000000000000 --- a/src/mapmarkers/mapmarkers.controller.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { MapMarkersController } from './mapmarkers.controller'; - -describe('Mapmarkers Controller', () => { - let controller: MapMarkersController; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [MapMarkersController], - }).compile(); - - controller = module.get<MapMarkersController>(MapMarkersController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); -}); diff --git a/src/mapmarkers/mapmarkers.controller.ts b/src/mapmarkers/mapmarkers.controller.ts deleted file mode 100644 index fef971652de596196169251c202378c67902babd..0000000000000000000000000000000000000000 --- a/src/mapmarkers/mapmarkers.controller.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Controller, Body, Get, Put, UseGuards } from '@nestjs/common'; - -import { MapMarkerService } from './mapmarker.service'; -import { MapMarkerDTO } from './mapmarker.dto'; -import { AuthGuard } from '../shared/auth.guard'; -import { User } from '../user/user.decorator'; - -@Controller('mapmarkers') -export class MapMarkersController { - constructor(private mapmarkerservice: MapMarkerService){} - - // Insert figure location, needs "authorization" header with valid Bearer token and content-type json - @Put('insert-location') - @UseGuards(new AuthGuard()) - async insertLocation(@User('id') person, @Body() data: MapMarkerDTO): Promise<string>{ - try { - return this.mapmarkerservice.insertLocation(person, data); - } catch (error) { - return error; - } - } - - // return all markers through service - @Get('getall') - async getAll(){ - try{ - return this.mapmarkerservice.getAllMarkers(); - }catch(error){ - return error.message; - } - } -} diff --git a/src/mapmarkers/mapmarkers.module.ts b/src/mapmarkers/mapmarkers.module.ts deleted file mode 100644 index a69970ec970f6f70adc8b38351364698c9e61a1e..0000000000000000000000000000000000000000 --- a/src/mapmarkers/mapmarkers.module.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; - -import { MapMarkersController } from './mapmarkers.controller'; -import { MapMarkerService } from './mapmarker.service'; -/*import { MapMarkerEntity } from './mapmarker.entity'; -import { PersonEntity } from '../user/user.entity'; - -@Module({ - imports: [TypeOrmModule.forFeature([MapMarkerEntity, PersonEntity])], - controllers: [MapMarkersController], - providers: [MapMarkerService] -}) -export class MapMarkerModule {}*/ diff --git a/src/notifications/notification.dto.ts b/src/notifications/notification.dto.ts new file mode 100644 index 0000000000000000000000000000000000000000..807af14058161062d61c9b88a1d8e56637004acc --- /dev/null +++ b/src/notifications/notification.dto.ts @@ -0,0 +1,12 @@ +import { IsString, Length, IsUUID, IsIn } from 'class-validator'; + +export class NotificationdDTO { + // alert is for serious messages, note is for smaller updates on a situation + @IsIn(['alert', 'note']) + type: string; + @IsString() + @Length(0, 63) + message: string; + @IsUUID('4') + game: string; +} diff --git a/src/notifications/notification.entity.ts b/src/notifications/notification.entity.ts index 3f8781633ec42bc3132c5df92f4f74045721506b..f9d5dca9290c30da406a63adf56ee06fe716a7b5 100644 --- a/src/notifications/notification.entity.ts +++ b/src/notifications/notification.entity.ts @@ -1,11 +1,23 @@ -import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn } from "typeorm"; +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + ManyToOne, +} from 'typeorm'; + +import { GameEntity } from '../game/game.entity'; // temporary table for warning notifications @Entity('Notifications') export class NotificationEntity { - @PrimaryGeneratedColumn('uuid') id: string; - @Column({type: 'text'}) message: string; - @CreateDateColumn() issued: Date; - // TODO: - // when game creation has been implemented, add logic so that the notifications are tied to games -} \ No newline at end of file + @PrimaryGeneratedColumn('uuid') id: string; + @Column('text') type: string; + @Column({ type: 'text' }) message: string; + @CreateDateColumn() issued: Date; + + @ManyToOne(type => GameEntity, game => game.id, { + onDelete: 'CASCADE', + }) + game: string; +} diff --git a/src/notifications/notifications.gateway.ts b/src/notifications/notifications.gateway.ts index c30114895ffa8d4b40c3cedeaf827c5b3401cb25..5e952d6c78c6f35977f362a7459c0ebf65d1ab5e 100644 --- a/src/notifications/notifications.gateway.ts +++ b/src/notifications/notifications.gateway.ts @@ -6,12 +6,15 @@ import { OnGatewayConnection, OnGatewayDisconnect, } from '@nestjs/websockets'; -import { Logger } from '@nestjs/common'; +import { Logger, UsePipes } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Server, Socket } from 'socket.io'; import { Repository } from 'typeorm'; import { NotificationEntity } from './notification.entity'; +import { GameEntity } from '../game/game.entity'; +import { NotificationdDTO } from './notification.dto'; +import { ValidationPipe } from '../shared/validation.pipe'; @WebSocketGateway() export class NotificationGateway @@ -20,6 +23,8 @@ export class NotificationGateway //create references to tables as repositories @InjectRepository(NotificationEntity) private notificationRepository: Repository<NotificationEntity>, + @InjectRepository(GameEntity) + private gameRepository: Repository<GameEntity>, ) {} @WebSocketServer() server: Server; @@ -40,14 +45,17 @@ export class NotificationGateway } // notifications for when game needs to be paused / stopped - @SubscribeMessage('shutItDown') - async handleMessage(client: Socket, text: string) { - this.logger.log(text); - // send the message to all clients listening to warningToPlayers branch - this.server.emit('warningToPlayers', text); - // create entity properties - const message = await this.notificationRepository.create({ message: text }); - // insert created entity NOTE: insert method doesn't check for duplicates. - await this.notificationRepository.insert(message); + @SubscribeMessage('game-info') + @UsePipes(new ValidationPipe()) + async handleMessage(client: Socket, data: NotificationdDTO) { + // check if the game exists and is either started or paused + const game = await this.gameRepository.findOne({ id: data.game }); + if (game && ['STARTED', 'PAUSED'].includes(game.state)) { + // send the message to all clients listening to gameId branch + this.server.emit(data.game, data); + // create entry for notification in db + const message = await this.notificationRepository.create(data); + await this.notificationRepository.insert(message); + } } } diff --git a/src/notifications/notifications.module.ts b/src/notifications/notifications.module.ts index 97cf59c1f929cfc14b5335b0308e7615ac10dee8..41e2fcaa36d1928dff9a60bd2c6853e240baf16f 100644 --- a/src/notifications/notifications.module.ts +++ b/src/notifications/notifications.module.ts @@ -1,10 +1,12 @@ import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + import { NotificationGateway } from './notifications.gateway'; import { NotificationEntity } from './notification.entity'; -import { TypeOrmModule } from '@nestjs/typeorm'; +import { GameEntity } from '../game/game.entity'; @Module({ - imports: [TypeOrmModule.forFeature([NotificationEntity])], + imports: [TypeOrmModule.forFeature([NotificationEntity, GameEntity])], providers: [NotificationGateway], exports: [NotificationGateway], }) diff --git a/src/score/score.controller.ts b/src/score/score.controller.ts new file mode 100644 index 0000000000000000000000000000000000000000..73fda22e699f5702b1616360bd7e95f0ac8aeb10 --- /dev/null +++ b/src/score/score.controller.ts @@ -0,0 +1,36 @@ +import { Controller, Post, UsePipes, Body, Param, Get } from '@nestjs/common'; + +import { ValidationPipe } from '../shared/validation.pipe'; +import { ScoreService } from './score.service'; +import { ScoreDTO } from './score.dto'; +import { GameEntity } from '../game/game.entity'; +import { Roles, GameStates } from '../shared/guard.decorator'; + +@Controller('score') +export class ScoreController { + constructor(private scoreService: ScoreService) {} + + // adds score manually to Faction + // :id is gameId + @Post('add-score/:id') + @Roles('admin') + @GameStates('STARTED') + @UsePipes(new ValidationPipe()) + async addingScore(@Body() data: ScoreDTO, @Param('id') gameId: GameEntity) { + return this.scoreService.addScore(data, gameId); + } + + // temporary scoreTick path, :id is gameId + @Get('tick-score/:id') + @GameStates('STARTED') + async scoreTick(@Param('id') gameId: GameEntity) { + return this.scoreService.scoreTick(gameId); + } + + // shows scores, :id is gameId + @Get('get-score/:id') + @GameStates('STARTED') + async getScores(@Param('id') gameId: GameEntity) { + return this.scoreService.getScores(gameId); + } +} diff --git a/src/score/score.dto.ts b/src/score/score.dto.ts new file mode 100644 index 0000000000000000000000000000000000000000..99955f56eff34a41ce46111d7fd0cb9cd597386f --- /dev/null +++ b/src/score/score.dto.ts @@ -0,0 +1,10 @@ +import { IsNumber, Min, Max, IsUUID } from 'class-validator'; + +export class ScoreDTO { + @IsNumber() + @Min(1) + @Max(99) + score: number; + @IsUUID('4') + faction: string; +} diff --git a/src/score/score.entity.ts b/src/score/score.entity.ts new file mode 100644 index 0000000000000000000000000000000000000000..6ca9ab77b968060a6dc88c860425119914cec030 --- /dev/null +++ b/src/score/score.entity.ts @@ -0,0 +1,21 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + ManyToOne, + Timestamp, + CreateDateColumn, +} from 'typeorm'; +import { FactionEntity } from '../faction/faction.entity'; + +@Entity('Score') +export class ScoreEntity { + @PrimaryGeneratedColumn('uuid') scoreId: string; + @Column({ type: 'float' }) score: number; + @CreateDateColumn({ type: 'timestamp' }) scoreTimeStamp: Timestamp; + + @ManyToOne(type => FactionEntity, factionName => factionName.factionId, { + onDelete: 'CASCADE', + }) + faction: string; +} diff --git a/src/score/score.module.ts b/src/score/score.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..c04486fc676fca0d7c6a38c74374e03e23a1008c --- /dev/null +++ b/src/score/score.module.ts @@ -0,0 +1,27 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { ScoreController } from './score.controller'; +import { ScoreService } from './score.service'; +import { FactionEntity } from '../faction/faction.entity'; +import { + ObjectivePointEntity, + ObjectivePoint_HistoryEntity, +} from '../game/game.entity'; +import { ScoreEntity } from './score.entity'; +import { NotificationModule } from '../notifications/notifications.module'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([ + ScoreEntity, + ObjectivePointEntity, + ObjectivePoint_HistoryEntity, + FactionEntity, + ]), + NotificationModule, + ], + controllers: [ScoreController], + providers: [ScoreService], +}) +export class ScoreModule {} diff --git a/src/score/score.service.ts b/src/score/score.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..dba8e6e79206322a4dffae937b5af6b2bdd89041 --- /dev/null +++ b/src/score/score.service.ts @@ -0,0 +1,119 @@ +import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; + +import { FactionEntity } from '../faction/faction.entity'; +import { ScoreDTO } from './score.dto'; +import { + ObjectivePoint_HistoryEntity, + ObjectivePointEntity, + GameEntity, +} from '../game/game.entity'; +import { ScoreEntity } from './score.entity'; +import { NotificationGateway } from '../notifications/notifications.gateway'; + +@Injectable() +export class ScoreService { + constructor( + @InjectRepository(ScoreEntity) + private scoreRepository: Repository<ScoreEntity>, + @InjectRepository(ObjectivePointEntity) + private flagRepository: Repository<ObjectivePointEntity>, + @InjectRepository(ObjectivePoint_HistoryEntity) + private flagHistoryRepository: Repository<ObjectivePoint_HistoryEntity>, + @InjectRepository(FactionEntity) + private factionRepository: Repository<FactionEntity>, + private notificationGateway: NotificationGateway, + ) {} + + async addScore(scoreData: ScoreDTO, gameId: GameEntity) { + // check if faction exists + const faction = await this.factionRepository.findOne({ + factionId: scoreData.faction, + }); + if (!faction) { + throw new HttpException('Faction was not found', HttpStatus.BAD_REQUEST); + } + // get the previous score and add it, if it exists + let lastScore = await this.scoreRepository.findOne({ + where: { faction: scoreData.faction }, + order: { scoreTimeStamp: 'DESC' }, + }); + if (lastScore) { + scoreData.score += lastScore.score; + } + // add the score for Faction + const newScore = await this.scoreRepository.create(scoreData); + await this.scoreRepository.insert(newScore); + return { + message: 'Score updated!', + }; + } + + async scoreTick(gameId) { + // get game's flagboxes + const flagboxes = await this.flagRepository.find({ game: gameId }); + // create an array of DTOs for adding score + let scoreData = []; + await Promise.all( + flagboxes.map(async box => { + // get the newest entry in history + let current = await this.flagHistoryRepository.findOne({ + where: { objective_point: box.objectivePointId }, + relations: ['owner'], + order: { oP_HistoryTimestamp: 'DESC' }, + }); + // if result was found, add score to the owner + if (current.owner) { + let index = await scoreData.findIndex( + i => i.faction === current.owner.factionId, + ); + index !== -1 + ? await (scoreData[index]['score'] += box.objectivePointMultiplier) + : await scoreData.push({ + score: box.objectivePointMultiplier, + faction: current.owner.factionId, + }); + } + }), + ); + scoreData.map(async data => { + await this.addScore(data, gameId); + }); + this.notificationGateway.server.emit(gameId, { type: 'score-update' }); + return { + message: 'Scores added', + }; + } + + async getScores(gameId: GameEntity) { + // find games factions + const factions = await this.factionRepository.find({ + where: {game: gameId,}, + relations: ['game'], + }); + let scores = []; + await Promise.all( + factions.map(async factionNow => { + let score = await this.scoreRepository.findOne({ + where: {faction: factionNow}, + relations: ['faction'], + order: {scoreTimeStamp: 'DESC'}, + }); + //if score was found, put info to scores array + if (score.faction) { + let index = await scores.findIndex( + i => i.faction === score.faction, + ); + scores.push(score); + } + })) + return scores; + } +} // + +// Hae kaikki Objective pointit +// aja map funktio pelin objective pointteihin +// jokaisella objective point ID:llä hae historystä +// relaatio, missä uusin timestamp +// katso uusimmista history entrystä omistaja diff --git a/src/shared/array-validation.ts b/src/shared/array-validation.ts deleted file mode 100644 index 494ab42f06d0d067fcb115e6eebb63ef8dca2746..0000000000000000000000000000000000000000 --- a/src/shared/array-validation.ts +++ /dev/null @@ -1,17 +0,0 @@ -import {ValidatorConstraint, ValidatorConstraintInterface, ValidationArguments} from "class-validator"; -import { Logger } from "@nestjs/common"; - -// validates array length -@ValidatorConstraint({ name: "arrayLength", async: true }) -export class ArrayLength implements ValidatorConstraintInterface { - - validate(array: string[], args: ValidationArguments) { - Logger.log(array.length) - return array.length > args.constraints[0] && array.length < args.constraints[1]; // for async validations you must return a Promise<boolean> here - } - - defaultMessage(args: ValidationArguments) { // here you can provide default error message if validation failed - return "Please input all passwords"; - } - -} \ No newline at end of file diff --git a/src/shared/custom-validation.ts b/src/shared/custom-validation.ts new file mode 100644 index 0000000000000000000000000000000000000000..13b96372e0b76a2897364baa15562edf383a2d81 --- /dev/null +++ b/src/shared/custom-validation.ts @@ -0,0 +1,52 @@ +import { + ValidatorConstraint, + ValidatorConstraintInterface, + ValidationArguments, + Validator, +} from 'class-validator'; + +// check if input is null or valid uuid +@ValidatorConstraint({ name: 'uuid', async: true }) +export class Uuid implements ValidatorConstraintInterface { + validate(uuid: string, args: ValidationArguments) { + const validator = new Validator(); + return validator.isUUID(uuid, '4') || uuid == null; // for async validations you must return a Promise<boolean> here + } + + defaultMessage(args: ValidationArguments) { + return 'Not valid uuid'; + } +} + +// checks if role is valid +@ValidatorConstraint({ name: 'roleValidation', async: true }) +export class RoleValidation implements ValidatorConstraintInterface { + validate(role: string, args: ValidationArguments) { + const validRoles = ['admin', 'soldier', 'factionleader']; + return validRoles.includes(role); + } + + defaultMessage(args: ValidationArguments) { + return 'Not valid uuid'; + } +} + +// checks for valid JSON for center +@ValidatorConstraint({ name: 'centerJSON', async: true }) +export class CenterJSON implements ValidatorConstraintInterface { + validate(center: JSON, args: ValidationArguments) { + const validator = new Validator(); + return ( + validator.isNumber(center['lat']) && + validator.isNumber(center['lng']) && + validator.min(center['lat'], -90) && + validator.max(center['lat'], 90) && + validator.min(center['lng'], -180) && + validator.max(center['lng'], 180) + ); + } + + defaultMessage(args: ValidationArguments) { + return 'Error with center JSON'; + } +} diff --git a/src/shared/roles.decorator.ts b/src/shared/guard.decorator.ts similarity index 53% rename from src/shared/roles.decorator.ts rename to src/shared/guard.decorator.ts index 0d14223c3ce5ee1002cf245fa16fd1b57749bae1..b299d6de913f54dd496c344f6dcdafd2942b2c9c 100644 --- a/src/shared/roles.decorator.ts +++ b/src/shared/guard.decorator.ts @@ -1,3 +1,6 @@ import { SetMetadata } from '@nestjs/common'; -export const Roles = (...roles: string[]) => SetMetadata('roles', roles); \ No newline at end of file +export const Roles = (...roles: string[]) => SetMetadata('roles', roles); + +export const GameStates = (...states: string[]) => + SetMetadata('states', states); diff --git a/src/shared/http-error.filter.ts b/src/shared/http-error.filter.ts index cafc414be633ac315931559cad3f4015eef0bf08..8a65068c4275c4a8d576da54b0340c17b6ac8cc4 100644 --- a/src/shared/http-error.filter.ts +++ b/src/shared/http-error.filter.ts @@ -1,23 +1,47 @@ -import { Catch, ExceptionFilter, HttpException, ArgumentsHost, Logger } from "@nestjs/common"; +import { + ExceptionFilter, + Catch, + ArgumentsHost, + Logger, + HttpException, + HttpStatus, +} from '@nestjs/common'; @Catch() export class HttpErrorFilter implements ExceptionFilter { - catch(exception: HttpException, host: ArgumentsHost) { - const ctx = host.switchToHttp(); - const request = ctx.getRequest(); - const response = ctx.getResponse(); - const status = exception.getStatus(); + catch(exception: HttpException, host: ArgumentsHost) { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + const request = ctx.getRequest(); + const status = exception.getStatus + ? exception.getStatus() + : HttpStatus.INTERNAL_SERVER_ERROR; - const errorResponse = { - code: status, - timestamp: new Date().toLocaleDateString(), - path: request.url, - method: request.method, - message: exception.message.error || exception.message || null, - }; + const errorResponse = { + code: status, + timestamp: new Date().toLocaleDateString(), + path: request.url, + method: request.method, + message: + status !== HttpStatus.INTERNAL_SERVER_ERROR + ? exception.message.error || exception.message || null + : 'Internal server error', + }; - Logger.error(`${request.method} ${request.url}`, JSON.stringify(errorResponse), "ExceptionFilter"); - - response.status(404).json({errorResponse}); + if (status === HttpStatus.INTERNAL_SERVER_ERROR) { + Logger.error( + `${request.method} ${request.url}`, + exception.stack, + 'ExceptionFilter', + ); + } else { + Logger.error( + `${request.method} ${request.url}`, + JSON.stringify(errorResponse), + 'ExceptionFilter', + ); } -} \ No newline at end of file + + response.status(status).json(errorResponse); + } +} diff --git a/src/shared/roles.guard.ts b/src/shared/roles.guard.ts index 1f05fc90d4c008277e96d543f8112b4581a91c47..73693f9e1551da1b1b34056a665eb59c1211b3ef 100644 --- a/src/shared/roles.guard.ts +++ b/src/shared/roles.guard.ts @@ -18,7 +18,7 @@ export class RolesGuard implements CanActivate { private readonly reflector: Reflector, @InjectRepository(Game_PersonEntity) private game_PersonRepository: Repository<Game_PersonEntity>, - ) {} + ) {} async canActivate(context: ExecutionContext): Promise<boolean> { // get roles that are allowed access, identified by @Roles('role') decorators in controllers @@ -27,9 +27,16 @@ export class RolesGuard implements CanActivate { return true; } const request = context.switchToHttp().getRequest(); - const gameId = request.params.id - const user = await this.getUserObject(request.headers.authorization); - const role = await this.game_PersonRepository.findOne({person: user['id'], game: gameId}) + // check for authorization header + if (!request.headers.authorization) { + return false; + } + const gameId = request.params.id; + request.user = await this.getUserObject(request.headers.authorization); + const role = await this.game_PersonRepository.findOne({ + person: request.user['id'], + game: gameId, + }); // check that the role matches the criteria and that token is valid for this game return role && roles.includes(role['role']); } @@ -42,7 +49,7 @@ export class RolesGuard implements CanActivate { // get the token const token = auth.split(' ')[1]; try { - return await jwt.verify(token, process.env.SECRET) + return await jwt.verify(token, process.env.SECRET); } catch (err) { const message = `Token error: ${err.message || err.name}`; throw new HttpException(message, HttpStatus.FORBIDDEN); diff --git a/src/shared/states.guard.ts b/src/shared/states.guard.ts new file mode 100644 index 0000000000000000000000000000000000000000..9e85e94e40f92ed6b016e451c592a3025a684cad --- /dev/null +++ b/src/shared/states.guard.ts @@ -0,0 +1,47 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + HttpException, + HttpStatus, +} from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; + +import { GameEntity } from '../game/game.entity'; + +@Injectable() +export class StatesGuard implements CanActivate { + constructor( + private readonly reflector: Reflector, + @InjectRepository(GameEntity) + private gameRepository: Repository<GameEntity>, + ) {} + + // Checks the state for gameId and grants access if it matches the criteria + // allowed states are CREATED, STARTED, PAUSED, ENDED + async canActivate(context: ExecutionContext): Promise<boolean> { + // get game states that are allowed access, identified by @GameStates('state') decorators in controllers + const states = this.reflector.get<string[]>('states', context.getHandler()); + if (!states) { + return true; + } + const request = context.switchToHttp().getRequest(); + const gameId = request.params.id; + const gameRef = await this.gameRepository.findOne({ + id: gameId, + }); + // check that the gameState matches the criteria + if (gameRef && states.includes(gameRef.state)) { + return true; + } else { + throw new HttpException( + `Game is set to ${ + gameRef.state + }, operation only valid in states ${states.join(', ')}`, + HttpStatus.BAD_REQUEST, + ); + } + } +} diff --git a/src/shared/validation.pipe.ts b/src/shared/validation.pipe.ts index 27f078b8d48f7cf160aeac90ff50d4a080edf3d4..cff51f1b068b67886c29dd85587760a1fa016f1e 100644 --- a/src/shared/validation.pipe.ts +++ b/src/shared/validation.pipe.ts @@ -1,44 +1,65 @@ - -import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException, HttpException, HttpStatus } from '@nestjs/common'; +import { + PipeTransform, + Injectable, + ArgumentMetadata, + HttpException, + HttpStatus, +} from '@nestjs/common'; import { validate } from 'class-validator'; import { plainToClass } from 'class-transformer'; +import { AdvancedConsoleLogger } from 'typeorm'; @Injectable() export class ValidationPipe implements PipeTransform<any> { - async transform(value: any, metadata: ArgumentMetadata) { - - if (value instanceof Object && this.isEmpty(value)) { - throw new HttpException( - 'Validation failed: No body submitted', HttpStatus.BAD_REQUEST - ); - } - - const { metatype } = metadata; - if (!metatype || !this.toValidate(metatype)) { - return value; - } - const object = plainToClass(metatype, value); - const errors = await validate(object); - if (errors.length > 0) { - throw new HttpException(`Validation failed: ${this.formatErrors(errors)}`, HttpStatus.BAD_REQUEST); - } - return value; + async transform(value: any, metadata: ArgumentMetadata) { + if (value instanceof Object && this.isEmpty(value)) { + throw new HttpException( + 'Validation failed: No body submitted', + HttpStatus.BAD_REQUEST, + ); } - private toValidate(metatype: Function): boolean { - const types: Function[] = [String, Boolean, Number, Array, Object]; - return !types.includes(metatype); + const { metatype } = metadata; + if (!metatype || !this.toValidate(metatype)) { + return value; } - - private formatErrors(errors: any[]) { - return errors.map(err => { - for (let property in err.constraints) { - return err.constraints[property] - } - }).join(", "); + const object = plainToClass(metatype, value); + const errors = await validate(object, { + whitelist: true, + forbidNonWhitelisted: true, + }); + if (errors.length > 0) { + throw new HttpException( + `Validation failed: ${this.formatErrors(errors)}`, + HttpStatus.BAD_REQUEST, + ); } + return value; + } - private isEmpty(value: any) { - return (Object.keys(value).length > 0) ? false : true; + private toValidate(metatype: Function): boolean { + const types: Function[] = [String, Boolean, Number, Array, Object]; + return !types.includes(metatype); + } + + private formatErrors(errors: any[]) { + return errors + .map(err => { + return this.returnError(err); + }) + .join(', '); + } + + private returnError(err) { + if (err['children'] !== undefined && err['children'].length != 0) { + return this.formatErrors(err['children']); } -} \ No newline at end of file + for (let property in err.constraints) { + return err.constraints[property]; + } + } + + private isEmpty(value: any) { + return Object.keys(value).length > 0 ? false : true; + } +} diff --git a/src/task/task.controller.ts b/src/task/task.controller.ts new file mode 100644 index 0000000000000000000000000000000000000000..8283ba5bd2c4d7da4eb668ac6268d5165930e3f4 --- /dev/null +++ b/src/task/task.controller.ts @@ -0,0 +1,55 @@ +import { + Controller, + Post, + Body, + UsePipes, + Get, + Param, + Delete, +} from '@nestjs/common'; + +import { TaskService } from './task.service'; +import { CreateTaskDTO, EditTaskDTO, DeleteTaskDTO } from './task.dto'; +import { Roles } from '../shared/guard.decorator'; +import { ValidationPipe } from '../shared/validation.pipe'; +import { User } from '../user/user.decorator'; + +@Controller('task') +export class TaskController { + constructor(private taskService: TaskService) {} + + // creates a new task if the user has admin role in the game + // :id is the id of the game + @Post('new-task/:id') + @Roles('admin') + @UsePipes(new ValidationPipe()) + async newTask(@Param('id') id: string, @Body() task: CreateTaskDTO) { + return this.taskService.newTask(task); + } + + // edits a created task if the user has admin role in the game + // :id is the id of the game + @Post('edit-task/:id') + @Roles('admin') + @UsePipes(new ValidationPipe()) + async editTask(@Param('id') id: string, @Body() data: EditTaskDTO) { + return this.taskService.editTask(data); + } + + // deletes a created task if the user has admin role in the game + // :id is the id of the game + @Delete('delete-task/:id') + @Roles('admin') + @UsePipes(new ValidationPipe()) + async deleteTask(@Param('id') id: string, @Body() data: DeleteTaskDTO) { + return this.taskService.deleteTask(data); + } + + // lists all the tasks for the game if the user has game_person entry + // :id is the id of the game + @Get('get-tasks/:id') + @Roles('soldier', 'factionleader', 'admin') + async fetchTasks(@User('id') person, @Param('id') id: string) { + return this.taskService.fetchTasks(person, id); + } +} diff --git a/src/task/task.dto.ts b/src/task/task.dto.ts new file mode 100644 index 0000000000000000000000000000000000000000..a1576e7d12c9d9a6263b445fdba293fe94e8964a --- /dev/null +++ b/src/task/task.dto.ts @@ -0,0 +1,43 @@ +import { + IsString, + Length, + IsBoolean, + Validate, + IsUUID, + Equals, +} from 'class-validator'; +import { FactionEntity } from '../faction/faction.entity'; +import { GameEntity } from '../game/game.entity'; +import { Uuid } from '../shared/custom-validation'; + +export class CreateTaskDTO { + @IsString() + @Length(3, 31) + taskName: string; + @IsString() + @Length(0, 255) + taskDescription: string; + @IsBoolean() + taskIsActive: boolean; + @Validate(Uuid) + faction: FactionEntity; + @Equals(null) + taskWinner: FactionEntity; + // faction unique id + @IsUUID('4') + taskGame: GameEntity; +} + +export class EditTaskDTO { + @IsUUID('4') + taskId: string; + @IsUUID('4') + taskWinner: FactionEntity; + @IsUUID('4') + taskGame: GameEntity; +} + +export class DeleteTaskDTO { + @IsUUID('4') + taskId: string; +} diff --git a/src/task/task.entity.ts b/src/task/task.entity.ts new file mode 100644 index 0000000000000000000000000000000000000000..f1c1cf103fa4338a7f10d0305888d13c097d7a09 --- /dev/null +++ b/src/task/task.entity.ts @@ -0,0 +1,32 @@ +import { + PrimaryGeneratedColumn, + Column, + Entity, + ManyToOne, + JoinColumn, +} from 'typeorm'; + +import { FactionEntity } from '../faction/faction.entity'; +import { GameEntity } from '../game/game.entity'; + +@Entity('Task') +export class TaskEntity { + @PrimaryGeneratedColumn('uuid') taskId: string; + @Column({ type: 'text' }) taskName: string; + @Column({ type: 'text' }) taskDescription: string; + @Column({ type: 'bool' }) taskIsActive: boolean; + + @ManyToOne(type => FactionEntity, faction => faction.factionId, { + onDelete: 'CASCADE', + }) + faction: FactionEntity; + @ManyToOne(type => FactionEntity, faction => faction.factionId, { + onDelete: 'CASCADE', + }) + taskWinner: FactionEntity; + @ManyToOne(type => GameEntity, game => game.id, { + onDelete: 'CASCADE', + }) + @JoinColumn({ name: 'taskGame' }) + taskGame: GameEntity; +} diff --git a/src/task/task.module.ts b/src/task/task.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..66d5ac5fce93d8447317343283a84b5002755ab6 --- /dev/null +++ b/src/task/task.module.ts @@ -0,0 +1,19 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { TaskService } from './task.service'; +import { TaskController } from './task.controller'; +import { TaskEntity } from './task.entity'; +import { FactionEntity } from '../faction/faction.entity'; +import { Game_PersonEntity } from '../game/game.entity'; +import { NotificationModule } from '../notifications/notifications.module'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([TaskEntity, FactionEntity, Game_PersonEntity]), + NotificationModule, + ], + controllers: [TaskController], + providers: [TaskService], +}) +export class TaskModule {} diff --git a/src/task/task.service.ts b/src/task/task.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..d79338e0d21e9ce741c67d4bf94b89ba64e4c9c9 --- /dev/null +++ b/src/task/task.service.ts @@ -0,0 +1,111 @@ +import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; +import { Repository } from 'typeorm'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { TaskEntity } from './task.entity'; +import { CreateTaskDTO, EditTaskDTO, DeleteTaskDTO } from './task.dto'; +import { FactionEntity } from '../faction/faction.entity'; +import { Game_PersonEntity } from '../game/game.entity'; +import { NotificationGateway } from '../notifications/notifications.gateway'; + +@Injectable() +export class TaskService { + constructor( + @InjectRepository(TaskEntity) + private taskRepository: Repository<TaskEntity>, + @InjectRepository(FactionEntity) + private factionRepository: Repository<FactionEntity>, + @InjectRepository(Game_PersonEntity) + private gamePersonRepository: Repository<Game_PersonEntity>, + private notificationGateway: NotificationGateway, + ) {} + + async newTask(task: CreateTaskDTO) { + // check if is not null, check that the faction exists in the game + if ( + task.faction != null && + !(await this.factionRepository.findOne({ + factionId: task.faction.toString(), + game: task.taskGame, + })) + ) { + throw new HttpException('Faction not found', HttpStatus.BAD_REQUEST); + } + const createdTask = await this.taskRepository.create(task); + await this.taskRepository.insert(createdTask); + // notify subscribers about a new task + // if faction was set it notifies only faction members, else everyone + this.notificationGateway.server.emit( + task.faction != null ? task.faction.toString() : task.taskGame.toString(), + { type: 'task-update' }, + ); + return { + message: 'Task added', + }; + } + + async editTask(data: EditTaskDTO) { + const task = await this.taskRepository.findOne(data.taskId); + // checks if task is already closed + if (!task.taskIsActive) { + throw new HttpException('Task is not active', HttpStatus.BAD_REQUEST); + } + // checks if faction is valid + if ( + !(await this.factionRepository.findOne({ + factionId: data.taskWinner.toString(), + game: data.taskGame, + })) + ) { + throw new HttpException('Faction not found', HttpStatus.BAD_REQUEST); + } + task.taskWinner = data.taskWinner; + task.taskIsActive = false; + await this.taskRepository.save(task); + return { + message: 'Task updated and closed', + }; + } + + async deleteTask(data: DeleteTaskDTO) { + const task = await this.taskRepository.findOne({ taskId: data.taskId }); + if (task) { + await this.taskRepository.delete({ taskId: task.taskId }); + return { + message: 'Task deleted', + }; + } + throw new HttpException('Task not found', HttpStatus.BAD_REQUEST); + } + + async fetchTasks(user, taskGame) { + const gamePerson = await this.gamePersonRepository.findOne({ + where: { + person: user, + game: taskGame, + }, + relations: ['faction'], + }); + if (gamePerson.role == 'admin') { + return await this.taskRepository.find({ + where: { taskGame: taskGame }, + relations: ['faction', 'taskWinner'], + }); + } else { + return await this.taskRepository.find({ + relations: ['faction', 'taskWinner'], + where: [ + { + taskGame: taskGame, + faction: gamePerson.faction.factionId, + }, + { + taskGame: taskGame, + faction: null, + }, + ], + order: { taskIsActive: 'DESC' }, + }); + } + } +} diff --git a/src/tracking/geo.dto.ts b/src/tracking/geo.dto.ts new file mode 100644 index 0000000000000000000000000000000000000000..352faf704db72912a548734185d277253d67c789 --- /dev/null +++ b/src/tracking/geo.dto.ts @@ -0,0 +1,14 @@ +import { IsNumber, Min, Max, Allow } from 'class-validator'; + +export class GeoDTO { + @IsNumber() + @Min(-90) + @Max(90) + lat: number; + @IsNumber() + @Min(-180) + @Max(180) + lng: number; + @Allow() + time: number; +} diff --git a/src/tracking/tracking.controller.ts b/src/tracking/tracking.controller.ts new file mode 100644 index 0000000000000000000000000000000000000000..ca0e79ee5195503f61d827dbc170474e66fd1bad --- /dev/null +++ b/src/tracking/tracking.controller.ts @@ -0,0 +1,42 @@ +import { + Controller, + Post, + Param, + UseGuards, + UsePipes, + Body, + Get, +} from '@nestjs/common'; + +import { TrackingService } from './tracking.service'; +import { TrackingDTO } from './tracking.dto'; +import { User } from '../user/user.decorator'; +import { Roles, GameStates } from '../shared/guard.decorator'; +import { ValidationPipe } from '../shared/validation.pipe'; +import { GeoDTO } from './geo.dto'; + +@Controller('tracking') +export class TrackingController { + constructor(private trackingservice: TrackingService) {} + + // inserts tracking data to the database + // :id is the id of the game + @Post('location/:id') + @Roles('soldier') + @GameStates('STARTED') + @UsePipes(new ValidationPipe()) + async trackLocation( + @User('id') userId, + @Param('id') id, + @Body() trackdata: GeoDTO, + ) { + return this.trackingservice.trackLocation(userId, id, trackdata); + } + + @Get('players/:id') + @Roles('admin', 'factionleader') + @GameStates('STARTED', 'PAUSED') + async getPlayerLocations(@User('id') userId, @Param('id') gameId) { + return this.trackingservice.getPlayers(userId, gameId); + } +} diff --git a/src/tracking/tracking.dto.ts b/src/tracking/tracking.dto.ts new file mode 100644 index 0000000000000000000000000000000000000000..81cc71597fa93910cb195b1fd4e30f38c62c1f05 --- /dev/null +++ b/src/tracking/tracking.dto.ts @@ -0,0 +1,10 @@ +import { Game_PersonEntity } from '../game/game.entity'; +import { Allow, ValidateNested } from 'class-validator'; +import { GeoDTO } from './geo.dto'; +import { Type } from 'class-transformer'; + +export class TrackingDTO { + @ValidateNested() + @Type(() => GeoDTO) + data: GeoDTO; +} diff --git a/src/tracking/tracking.entity.ts b/src/tracking/tracking.entity.ts new file mode 100644 index 0000000000000000000000000000000000000000..c2699a09388ce8ad47c7119b46f4614fa52b34ec --- /dev/null +++ b/src/tracking/tracking.entity.ts @@ -0,0 +1,19 @@ +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from 'typeorm'; +import { Game_PersonEntity, GameEntity } from '../game/game.entity'; +import { FactionEntity } from 'src/faction/faction.entity'; +import { GeoDTO } from './geo.dto'; + +@Entity('Tracking') +export class TrackingEntity { + @PrimaryGeneratedColumn('uuid') id: string; + @Column({ type: 'json', nullable: true }) data: GeoDTO[]; + + @ManyToOne(type => Game_PersonEntity, person => person.gamepersonId, { + onDelete: 'CASCADE', + }) + gamepersonId: Game_PersonEntity; + @ManyToOne(type => FactionEntity, faction => faction.factionId) + faction: FactionEntity; + @ManyToOne(type => GameEntity, game => game.id) + game: GameEntity; +} diff --git a/src/tracking/tracking.module.ts b/src/tracking/tracking.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..45b167fc526ab41addbb87575518c9acae884482 --- /dev/null +++ b/src/tracking/tracking.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { TrackingController } from './tracking.controller'; +import { TrackingService } from './tracking.service'; +import { TrackingEntity } from './tracking.entity'; +import { Game_PersonEntity } from '../game/game.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([TrackingEntity, Game_PersonEntity])], + controllers: [TrackingController], + providers: [TrackingService], +}) +export class TrackingModule {} diff --git a/src/tracking/tracking.service.ts b/src/tracking/tracking.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..9941256dde52240d7c90b45d61b7b32ef4e1c242 --- /dev/null +++ b/src/tracking/tracking.service.ts @@ -0,0 +1,102 @@ +import { Injectable, HttpStatus, HttpException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; + +import { Game_PersonEntity } from '../game/game.entity'; +import { TrackingEntity } from './tracking.entity'; +import { TrackingDTO } from './tracking.dto'; +import { FactionEntity } from '../faction/faction.entity'; +import { NotificationGateway } from 'src/notifications/notifications.gateway'; +import { GeoDTO } from './geo.dto'; + +@Injectable() +export class TrackingService { + constructor( + @InjectRepository(TrackingEntity) + private trackingrepository: Repository<TrackingEntity>, + @InjectRepository(Game_PersonEntity) + private gamepersonrepository: Repository<Game_PersonEntity>, + ) {} + + async trackLocation(personId, gameId, trackdata: GeoDTO) { + // find player + let gameperson = await this.gamepersonrepository.findOne({ + where: { game: gameId, person: personId }, + relations: ['faction'], + }); + if (!gameperson) { + throw new HttpException( + 'You have not joined this game', + HttpStatus.BAD_REQUEST, + ); + } + let trackedperson = await this.trackingrepository.findOne({ + gamepersonId: gameperson, + }); + + // if player has pushed tracking data, update entry + if (trackedperson) { + trackdata['time'] = Date.now(); + //add coordinates + trackedperson.data.push(trackdata); + //add timestamp + await this.trackingrepository.save(trackedperson); + return { code: 201, message: 'Location updated!' }; + } else { + // first entry will be empty + trackdata['time'] = Date.now(); + // initialize data + trackedperson = await this.trackingrepository.create(trackedperson); + trackedperson.data = [trackdata]; + trackedperson.faction = gameperson.faction; + trackedperson.game = gameId; + trackedperson.gamepersonId = gameperson; + await this.trackingrepository.save(trackedperson); + + return { code: 201, message: 'Entry Created!' }; + } + } + + // get player data while game is running + async getPlayers(userId, gameId) { + // get gameperson + const gameperson = await this.gamepersonrepository.findOne({ + where: { person: userId, game: gameId }, + relations: ['faction'], + }); + + let playerdata; + // get playerdata + if (gameperson.faction) { + playerdata = await this.trackingrepository.find({ + where: { faction: gameperson.faction }, + relations: ['faction', 'gamepersonId'], + }); + } else { + playerdata = await this.trackingrepository.find({ + where: { game: gameId }, + relations: ['faction', 'gamepersonId'], + }); + } + + // parse data + const currentdata = await Promise.all( + playerdata.map(async player => { + return { + gamepersonId: player['gamepersonId']['gamepersonId'], + gamepersonRole: player['gamepersonId']['role'], + factionId: player['faction']['factionId'], + coordinates: player['data'].pop(), + }; + }), + ); + + return currentdata; + } + + private async mapFunction(data): Promise<Number> { + return await data.map(type => { + return type; + }); + } +} diff --git a/src/user/user.controller.spec.ts b/src/user/user.controller.spec.ts deleted file mode 100644 index 95e4e6222d74fb868ba0480ec5da337c636359c5..0000000000000000000000000000000000000000 --- a/src/user/user.controller.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { UserController } from './user.controller'; - -describe('User Controller', () => { - let controller: UserController; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [UserController], - }).compile(); - - controller = module.get<UserController>(UserController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); -}); diff --git a/src/user/user.entity.ts b/src/user/user.entity.ts index 1e44121e0eed81e1a1b7a7f35e9a95e557c89e5e..716020ae49d675641863a1711e424b85dbd67608 100644 --- a/src/user/user.entity.ts +++ b/src/user/user.entity.ts @@ -7,13 +7,18 @@ import { } from 'typeorm'; import * as bcrypt from 'bcryptjs'; import * as jwt from 'jsonwebtoken'; + import { Game_PersonEntity } from '../game/game.entity'; +import { Exclude } from 'class-transformer'; @Entity('Person') export class PersonEntity { @PrimaryGeneratedColumn('uuid') id: string; @Column({ type: 'text', unique: true }) name: string; - @Column('text') password: string; + + @Exclude() + @Column('text') + password: string; @OneToMany(type => Game_PersonEntity, game_persons => game_persons.person) game_persons: Game_PersonEntity[]; @@ -30,12 +35,6 @@ export class PersonEntity { return { name, token }; } - // returns username and the id - nameObject() { - const { id, name } = this; - return { id, name }; - } - async comparePassword(attempt: string) { return await bcrypt.compareSync(attempt, this.password); } diff --git a/src/user/user.service.spec.ts b/src/user/user.service.spec.ts deleted file mode 100644 index 873de8ac4d8e77fc827e4f3939f0068391a3800c..0000000000000000000000000000000000000000 --- a/src/user/user.service.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { UserService } from './user.service'; - -describe('UserService', () => { - let service: UserService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [UserService], - }).compile(); - - service = module.get<UserService>(UserService); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); -}); diff --git a/tests/valid_registration.robot b/tests/00_valid_registration.robot similarity index 78% rename from tests/valid_registration.robot rename to tests/00_valid_registration.robot index f62f83add9b43e2e855b85d2191bac366c002fc9..afca0e6e04b31d625a78852f7af513dae811e0b7 100644 --- a/tests/valid_registration.robot +++ b/tests/00_valid_registration.robot @@ -7,7 +7,8 @@ Suite Teardown Close Browser *** Test Cases *** Registration Process Open Registration - Generate Username 12 - Generate Password 6 + Generate Valid Username + Input Valid Username + Input Valid Password Submit Credentials Registration Log Out diff --git a/tests/valid_login.robot b/tests/01_valid_login.robot similarity index 81% rename from tests/valid_login.robot rename to tests/01_valid_login.robot index 01d1140d5ac71edae110307c7b7dbaac11373325..b31ec3b335b0a19f5227c1a8544c4e2ceb105a38 100644 --- a/tests/valid_login.robot +++ b/tests/01_valid_login.robot @@ -7,8 +7,8 @@ Resource resource.robot Valid Login Open Browser To Login Page Open Login - Input Username ville - Input Password koira + Input Username ${VALID USER} + Input Password ${VALID PASSWORD} Submit Credentials Login Wait For Log Out Button To Appear Log Out diff --git a/tests/02_invalid_registration.robot b/tests/02_invalid_registration.robot new file mode 100644 index 0000000000000000000000000000000000000000..2c4670a94617a1e3a157b26abc748457e00b4dc1 --- /dev/null +++ b/tests/02_invalid_registration.robot @@ -0,0 +1,49 @@ +*** Settings *** +Documentation A test suite for invalid registration. +Resource resource.robot +Suite Setup Open Browser To Login Page +Suite Teardown Close Browser +Test Template Registration With Invalid Options Should Fail + +*** Variables *** +${ge_u} = Generate Username +${ge_p} = Generate Password +${ge_vp} = Generate Differing Password + +*** Test Cases *** +Short Username ${ge_u} 2 ${ge_p} 32 ${SHORT_U} +Long Username ${ge_u} 32 ${ge_p} 32 ${LONG_U} +Short Password ${ge_u} 31 ${ge_p} 2 ${SHORT_P} +Long Password ${ge_u} 31 ${ge_p} 256 ${LONG_P} +Short Username/Password ${ge_u} 2 ${ge_p} 2 ${SHORT_UP} +Long Username/Password ${ge_u} 32 ${ge_p} 256 ${LONG_UP} +Short Username And Long Password ${ge_u} 2 ${ge_p} 256 ${SU_LP} +Long Username And Short Password ${ge_u} 32 ${ge_p} 2 ${LU_SP} +#Differing Password +#Existing Account Correct Password +#Existing Account New Password +Empty Username And Password ${ge_u} 0 ${ge_p} 0 ${SHORT_UP} + + +*** Keywords *** +Registration With Invalid Options Should Fail + [Arguments] ${gene_usern} ${GNUM_U} ${gene_passwords} ${GNUM_P} ${error_text} + Open Registration + Run Keyword ${gene_usern} ${GNUM_U} + Run Keyword ${gene_passwords} ${GNUM_P} + Submit Credentials Registration + Registration Should Have Failed ${error_text} + Close Registration Screen + +Registration Should Have Failed #Checks the error message. + [Arguments] ${error_text} + Element Text Should Be css=h2 ${error_text} + #Title Should Be Error Page #If there's going to be an error page. + +Differing Password + Open Registration + Generate Username 31 + Generate Differing Password 8 + Submit Credentials Registration + #Registration Should Have Failed + Close Registration Screen \ No newline at end of file diff --git a/tests/invalid_login.robot b/tests/invalid_login.robot deleted file mode 100644 index 287bb6699b6dc037350e903dfc97042e8910b703..0000000000000000000000000000000000000000 --- a/tests/invalid_login.robot +++ /dev/null @@ -1,29 +0,0 @@ -*** Settings *** -Documentation A test suite for invalid login. -Resource resource.robot -Suite Setup Open Browser To Login Page -Suite Teardown Close Browser -Test Template Login With Invalid Credentials Should Fail -*** Test Cases *** -Invalid Username invalid ${VALID PASSWORD} ${INVALID_U} -Invalid Password ${VALID USER} invalid ${INVALID_P} -Invalid Username And Password invalid whatever ${INVALID_U} -Empty Username ${EMPTY} ${VALID PASSWORD} ${SHORT_U} -Empty Password ${VALID USER} ${EMPTY} ${SHORT_P} -Empty Username And Password ${EMPTY} ${EMPTY} ${SHORT_UP} - -*** Keywords *** -Login With Invalid Credentials Should Fail - [Arguments] ${username} ${password} ${error_text} - Open Login - Input Username ${username} - Input Password ${password} - Submit Credentials Login - Login Should Have Failed ${error_text} - Close Login Screen - -Login Should Have Failed #Checks the error message. - [Arguments] ${error_text} - Element Text Should Be css=h2 ${error_text} - #Title Should Be Error Page #If there's going to be an error page. - diff --git a/tests/resource.robot b/tests/resource.robot index 94829d0a25dc28045c03b38b702752bc6dd29386..0aa3c238a2396ae7a2127fb0994cf7e90d6b10dc 100644 --- a/tests/resource.robot +++ b/tests/resource.robot @@ -1,121 +1,96 @@ *** Settings *** -Documentation A resource file with reusable keywords and variables. -Library SeleniumLibrary run_on_failure=nothing -Library String - +Documentation A resource file with reusable keywords and variables. +Library SeleniumLibrary run_on_failure=nothing +Library String *** Variables *** -${SERVER} %{SITE_URL} -${BROWSER} firefox -${DELAY} 0.5 -${VALID USER} ville -${VALID PASSWORD} koira -${LOGIN URL} http://${SERVER}/ -${WELCOME URL} #You can use this if there's a different page after login page. -${LOC_USER} id=registerUsernameInput #Generated username. -${LOC_PASSWORD} id=registerPasswordInput #Generated password first time. -${LOC_PASSWORD2} id=registerPasswordVerifyInput #Generated password verify. -${ZOOMIN} //*[@id="root"]/div/div[1]/div[2]/div[2]/div[1]/a[1] #Zoom in button location -${ZOOMOUT} //*[@id="root"]/div/div[1]/div[2]/div[2]/div[1]/a[2] #Zoom out button location -${INVALID_U} User does not exist -${INVALID_P} Invalid password -${SHORT_P} Validation failed: password must be longer than or equal to 3 characters -${SHORT_U} Validation failed: name must be longer than or equal to 3 characters -${SHORT_UP} Validation failed: name must be longer than or equal to 3 characters, password must be longer than or equal to 3 characters -${LONG_U} Validation failed: name must be shorter than or equal to 31 characters -${LONG_P} Validation failed: password must be shorter than or equal to 255 characters -${LONG_UP} Validation failed: name must be shorter than or equal to 31 characters, password must be shorter than or equal to 255 characters -${SU_LP} Validation failed: name must be longer than or equal to 3 characters, password must be shorter than or equal to 255 characters -${LU_SP} Validation failed: name must be shorter than or equal to 31 characters, password must be longer than or equal to 3 characters - +${SERVER} %{SITE_URL} +${BROWSER} ff +${DELAY} 0.5 +${VALID PASSWORD} = koira +${LOGIN URL} https://${SERVER}/ +${WELCOME URL} #You can use this if there's a different page after login page. +${LOC_USER} id=registerUsernameInput #Generated username. +${LOC_PASSWORD} id=registerPasswordInput #Generated password first time. +${LOC_PASSWORD2} id=registerPasswordVerifyInput #Generated password verify. +${ZOOMIN} //*[@id="root"]/div/div[1]/div[2]/div[2]/div[1]/a[1] #Zoom in button location +${ZOOMOUT} //*[@id="root"]/div/div[1]/div[2]/div[2]/div[1]/a[2] #Zoom out button location +${INVALID_U} User does not exist +${INVALID_P} Invalid password +${SHORT_P} Validation failed: password must be longer than or equal to 3 characters +${SHORT_U} Validation failed: name must be longer than or equal to 3 characters +${SHORT_UP} Validation failed: name must be longer than or equal to 3 characters, password must be longer than or equal to 3 characters +${LONG_U} Validation failed: name must be shorter than or equal to 31 characters +${LONG_P} Validation failed: password must be shorter than or equal to 255 characters +${LONG_UP} Validation failed: name must be shorter than or equal to 31 characters, password must be shorter than or equal to 255 characters +${SU_LP} Validation failed: name must be longer than or equal to 3 characters, password must be shorter than or equal to 255 characters +${LU_SP} Validation failed: name must be shorter than or equal to 31 characters, password must be longer than or equal to 3 characters *** Keywords *** #Valid Login Open Browser To Login Page - Open Browser ${LOGIN URL} ${BROWSER} - Maximize Browser Window - Set Selenium Speed ${DELAY} - Login Page Should be Open - + Open Browser ${LOGIN URL} ${BROWSER} + Maximize Browser Window + Set Selenium Speed ${DELAY} + Login Page Should be Open Login Page Should be Open - Title Should Be React App - - + Title Should Be React App Go To Login Page - Go To ${LOGIN URL} - Login Page Should be Open - + Go To ${LOGIN URL} + Login Page Should be Open Open Login - Click Button id=loginButton - + Click Button id=loginButton Input Username - [Arguments] ${username} - Input Text id=loginUsernameInput ${username} - + [Arguments] ${username} + Input Text id=loginUsernameInput ${username} Input Password - [Arguments] ${password} - Input Text id=loginPasswordInput ${password} - + [Arguments] ${password} + Input Text id=loginPasswordInput ${password} Submit Credentials Login - Click Button id=submitLoginButton - -Welcome Page Should Be Open #You can use this if there's a different page after login page. - Location Should Be ${WELCOME URL} - + Click Button id=submitLoginButton +Welcome Page Should Be Open #You can use this if there's a different page after login page. + Location Should Be ${WELCOME URL} Log Out - Click Button id=logoutButton - + Click Button id=logoutButton Close Login Screen - Click Element id=closeLoginFormX - + Click Element id=closeLoginFormX Wait For Log Out Button To Appear - Wait Until Page Contains Element id=logoutButton 1 - + Wait Until Page Contains Element id=logoutButton 1 #Registration Open Registration - Click Button id=registerButton - - -Generate Username #Generates a random username lenght=8 chars=[LETTERS][NUMBERS] - [Arguments] ${GNUM_U} - ${GENE_username} = Generate Random String ${GNUM_U} [LETTERS][NUMBERS] - Input Text ${LOC_USER} ${GENE_username} - -Generate Password #Generates a random password lenght=8 chars=[LETTERS][NUMBERS] - [Arguments] ${GNUM_P} - ${GENE_password} = Generate Random String ${GNUM_P} [LETTERS][NUMBERS] - Input Text ${LOC_PASSWORD} ${GENE_password} - Input Text ${LOC_PASSWORD2} ${GENE_password} - + Click Button id=registerButton +Generate Valid Username + ${GENE_username} = Generate Random String 12 [LETTERS][NUMBERS] + Set Global Variable ${VALID USER} ${GENE_username} +Input Valid Username + Input Text ${LOC_USER} ${VALID USER} +Input Valid Password + Input Text ${LOC_PASSWORD} ${VALID PASSWORD} + Input Text ${LOC_PASSWORD2} ${VALID PASSWORD} +Generate Username #Generates a random username lenght=8 chars=[LETTERS][NUMBERS] + [Arguments] ${GNUM_U} + ${GENE_username} = Generate Random String ${GNUM_U} [LETTERS][NUMBERS] + Input Text ${LOC_USER} ${GENE_username} +Generate Password #Generates a random password lenght=8 chars=[LETTERS][NUMBERS] + [Arguments] ${GNUM_P} + ${GENE_password} = Generate Random String ${GNUM_P} [LETTERS][NUMBERS] + Input Text ${LOC_PASSWORD} ${GENE_password} + Input Text ${LOC_PASSWORD2} ${GENE_password} Generate Differing Password - [Arguments] ${GNUM_VP} - ${GENE_dpassword} = Generate Random String ${GNUM_VP} [LETTERS][NUMBERS] - ${GENE_dpassword2} = Generate Random String ${GNUM_VP} [LETTERS][NUMBERS] - Input Text ${LOC_PASSWORD} ${GENE_dpassword} - Input Text ${LOC_PASSWORD2} ${GENE_dpassword2} - + [Arguments] ${GNUM_VP} + ${GENE_dpassword} = Generate Random String ${GNUM_VP} [LETTERS][NUMBERS] + ${GENE_dpassword2} = Generate Random String ${GNUM_VP} [LETTERS][NUMBERS] + Input Text ${LOC_PASSWORD} ${GENE_dpassword} + Input Text ${LOC_PASSWORD2} ${GENE_dpassword2} Submit Credentials Registration - Click Button id=submitRegisterButton - + Click Button id=submitRegisterButton Close Registration Screen - Click Element id=closeRegisterFormX - + Click Element id=closeRegisterFormX #Zoom frontpage Wait For Zoom Button To Appear - Wait Until Page Contains Element //*[@id="root"]/div/div[1]/div[2]/div[2]/div[1]/a[1] 1 - + Wait Until Page Contains Element //*[@id="root"]/div/div[1]/div[2]/div[2]/div[1]/a[1] 1 Zoom In On Frontpage - Repeat Keyword 3 times Click Element ${ZOOMIN} - - + Repeat Keyword 3 times Click Element ${ZOOMIN} Zoom Out On Frontpage - Repeat Keyword 3 times Click Element ${ZOOMOUT} - - -Move Around On The Map Frontpage #en saanut toimimaan - #Press Key //*[@id="root"]/div/div[1]/div[1] ARROW_LEFT - Press Combination Key. - - - - - - + Repeat Keyword 3 times Click Element ${ZOOMOUT} +Move Around On The Map Frontpage #en saanut toimimaan + #Press Key //*[@id="root"]/div/div[1]/div[1] ARROW_LEFT + Press Combination Key. \ No newline at end of file