diff --git a/src/app.module.ts b/src/app.module.ts index dbb03cf7baa35d2d4deafdbff6ba67bd64381c92..f2c7b7765304436488bbadb3fdfce4fbdc1fbd9a 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -2,19 +2,23 @@ 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 { 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 { GameModule } from './game/game.module'; -import { RolesGuard } from './shared/roles.guard'; 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 { TrackingModule } from './tracking/tracking.module'; - +import { GameModule } from './game/game.module'; +import { ScoreModule } from './score/score.module'; @Module({ imports: [ @@ -26,6 +30,7 @@ import { TrackingModule } from './tracking/tracking.module'; DrawModule, FactionModule, TrackingModule, + ScoreModule, ], controllers: [AppController], providers: [ @@ -42,6 +47,10 @@ import { TrackingModule } from './tracking/tracking.module'; 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 index 9cee16c6a496d5e9c68a33eabf57514e88cdf7c1..ef5447f61cfd1dd07d1f81a8ee1fce8e8256da2a 100644 --- a/src/draw/coordinate.entity.ts +++ b/src/draw/coordinate.entity.ts @@ -17,9 +17,13 @@ export class MapDrawingEntity { @Column({ type: 'json', nullable: true }) data: JSON; - @ManyToOne(type => FactionEntity, faction => faction.mapDrawings) + @ManyToOne(type => FactionEntity, faction => faction.mapDrawings, { + onDelete: 'CASCADE', + }) faction: FactionEntity; - @ManyToOne(type => GameEntity, gameEntity => gameEntity.id) + @ManyToOne(type => GameEntity, gameEntity => gameEntity.id, { + onDelete: 'CASCADE', + }) gameId: GameEntity; } @@ -28,8 +32,20 @@ export class Game_Person_MapDrawingEntity { @PrimaryGeneratedColumn('uuid') GPmapDrawingId: string; @Column({ type: 'timestamp' }) GPCTimeStamp: Timestamp; - @ManyToOne(type => Game_PersonEntity, game_person => game_person.gamepersonId) + @ManyToOne( + type => Game_PersonEntity, + game_person => game_person.gamepersonId, + { + onDelete: 'CASCADE', + }, + ) game_person: Game_PersonEntity; - @ManyToOne(type => MapDrawingEntity, map_drawing => map_drawing.mapDrawingId) + @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 index 21306d7ae91e565a5459138229a384e21fd68abd..26f6cf8c56142ef190ad3bcbfd7ce9d32caa371e 100644 --- a/src/draw/draw.controller.ts +++ b/src/draw/draw.controller.ts @@ -11,7 +11,8 @@ import { import { AuthGuard } from '../shared/auth.guard'; import { DrawService } from './draw.service'; -import { Roles } from '../shared/roles.decorator'; +import { Roles, GameStates } from '../shared/guard.decorator'; +import { MapDrawingDTO, ReturnDrawingsDTO } from './mapdrawing.dto'; /* DrawController @@ -25,16 +26,17 @@ export class DrawController { constructor(private drawService: DrawService) {} @Put('mapdrawing/:id') - @UsePipes(new ValidationPipe()) @Roles('admin', 'factionleader') - async draw(@Param('id') gameId, @Body() data) { + @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) { + async drawMap(@Param('id') id, @Body() data: ReturnDrawingsDTO) { return this.drawService.drawMap(id, data); } } diff --git a/src/draw/draw.service.ts b/src/draw/draw.service.ts index e3b62b5132223f466fa94661d4748b3f7cd497f5..d5ddf23e9574771bc769e2b6d58b59a4e95ad10d 100644 --- a/src/draw/draw.service.ts +++ b/src/draw/draw.service.ts @@ -3,6 +3,7 @@ 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 { @@ -11,15 +12,13 @@ export class DrawService { private mapDrawingRepository: Repository<MapDrawingEntity>, ) {} - async draw(gameId, data: 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; + const mapDrawing = await this.mapDrawingRepository.insert(drawing); + return mapDrawing.identifiers; } else { //päivittää mapDrawingin return await this.mapDrawingRepository.save(drawing); @@ -27,13 +26,21 @@ export class DrawService { } // draw map based on game and - async drawMap(id, data: MapDrawingEntity) { - data['gameId'] = id; - data['drawingIsActive'] = true; - // get faction - const mapDrawings = await this.mapDrawingRepository.create(data); - + async drawMap(id, data: ReturnDrawingsDTO) { // return mapdrawings with given faction and gameid - return await this.mapDrawingRepository.find(mapDrawings); + 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 index 03eca2d1057490558e271252d12a2cc1ef883273..f7e7c781a0cbd308edf5f8c14f068b449f43a05e 100644 --- a/src/draw/mapdrawing.dto.ts +++ b/src/draw/mapdrawing.dto.ts @@ -1,26 +1,27 @@ -import { IsUUID } from 'class-validator'; +import { IsUUID, IsOptional, IsBoolean, Allow } from 'class-validator'; -import { GameDTO } from '../game/game.dto'; -import { GameEntity, Game_PersonEntity } from '../game/game.entity'; import { FactionEntity } from '../faction/faction.entity'; -import { FactionDTO } from '../faction/faction.dto'; -import { MapDrawingEntity } from '../draw/coordinate.entity'; +import { GameEntity } from '../game/game.entity'; export class MapDrawingDTO { + @IsOptional() + @IsUUID('4') + mapDrawingId: string; + @Allow() data: JSON; - gameId: GameDTO; - faction?: FactionDTO; - isActive?: boolean; - validUntil?: string; + @IsOptional() + @IsUUID('4') + gameId: GameEntity; + @IsOptional() + @IsUUID('4') + faction?: FactionEntity; + @IsBoolean() + drawingIsActive?: boolean; + drawingValidTill?: string; } -export class DrawMapDTO { +export class ReturnDrawingsDTO { + @IsOptional() @IsUUID('4') - mapDrawingId: MapDrawingEntity; - - gameId: GameEntity; factionId: FactionEntity; - - gamepersonId: Game_PersonEntity; - data: JSON; } diff --git a/src/faction/faction.controller.ts b/src/faction/faction.controller.ts index dec9308d456e93f4c6f26fee5f5f802af4e5857e..da7e5950626bbcd5385d80755d208e59be589b48 100644 --- a/src/faction/faction.controller.ts +++ b/src/faction/faction.controller.ts @@ -21,7 +21,7 @@ import { JoinGameGroupDTO, } from './faction.dto'; import { FactionService } from './faction.service'; -import { Roles } from '../shared/roles.decorator'; +import { Roles, GameStates } from '../shared/guard.decorator'; @Controller('faction') export class FactionController { @@ -30,6 +30,7 @@ export class FactionController { // 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, @@ -50,6 +51,7 @@ export class FactionController { // 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, @@ -66,17 +68,31 @@ export class FactionController { // param game ID is passed to @Roles @Put('promote/:id') - @UseGuards(new AuthGuard()) - @UsePipes(new ValidationPipe()) @Roles('admin') + @GameStates('CREATED') + @UsePipes(new ValidationPipe()) promotePlayer(@Param('id') game, @Body() body: PromotePlayerDTO) { return this.factionservice.promotePlayer(body); } - @Put('join-faction') + // 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, @Body() data: JoinFactionDTO) { + 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 index a5b9f044bdf74edefa37ac077e09a3e384e5e5c3..0b65c47007d6424584d8fdb35ac9d9ee53eaa924 100644 --- a/src/faction/faction.dto.ts +++ b/src/faction/faction.dto.ts @@ -7,14 +7,18 @@ import { IsNumber, Min, Max, + IsOptional, } from 'class-validator'; import { GameEntity } from '../game/game.entity'; -import { RoleValidation } from '../shared/custom-validation'; +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) diff --git a/src/faction/faction.entity.ts b/src/faction/faction.entity.ts index 42f3aca413b36455786494818f2ca06f50f50f2e..3aa74cf615d77deb1421d52d46d4159bf7e0d241 100644 --- a/src/faction/faction.entity.ts +++ b/src/faction/faction.entity.ts @@ -28,7 +28,9 @@ export class FactionEntity { @OneToMany(type => Game_PersonEntity, game_persons => game_persons.faction) game_persons: Game_PersonEntity[]; - @ManyToOne(type => GameEntity, game => game.factions) + @ManyToOne(type => GameEntity, game => game.factions, { + onDelete: 'CASCADE', + }) game: GameEntity; @OneToMany(type => MapDrawingEntity, mapDrawings => mapDrawings.faction) mapDrawings: MapDrawingEntity[]; @@ -43,7 +45,7 @@ export class FactionEntity { } } -@Entity('PowerUp') +/* @Entity('PowerUp') export class PowerUpEntity { @PrimaryGeneratedColumn('uuid') powerUpId: string; @Column({ type: 'text' }) powerUpName: string; @@ -51,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[]; } @@ -77,17 +81,7 @@ 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('GameGroup') export class GameGroupEntity { @@ -102,6 +96,8 @@ export class GameGroupEntity { onDelete: 'CASCADE', }) players: Game_PersonEntity[]; - @ManyToOne(type => FactionEntity, faction => faction.factionId) + @ManyToOne(type => FactionEntity, faction => faction.factionId, { + onDelete: 'CASCADE', + }) faction: FactionEntity; } diff --git a/src/faction/faction.service.ts b/src/faction/faction.service.ts index 76242dfe1bceda0776435b43c4fdc4cb60fe272c..67edef1db5d071e1936aba03ded5c5051c1ae713 100644 --- a/src/faction/faction.service.ts +++ b/src/faction/faction.service.ts @@ -35,7 +35,12 @@ export class FactionService { person: person, }); //check if user is already in a faction - if (await this.game_PersonRepository.findOne(gameperson)) { + if ( + await this.game_PersonRepository.findOne({ + person: person, + game: faction.game, + }) + ) { throw new HttpException( 'You have already joined faction!', HttpStatus.BAD_REQUEST, @@ -94,7 +99,6 @@ export class FactionService { await this.game_PersonRepository.save(gamePerson); return { - code: 201, message: 'created new group', }; } @@ -114,7 +118,6 @@ export class FactionService { gamePerson.group = data.groupId; await this.game_PersonRepository.save(gamePerson); return { - code: 200, message: 'Joined group', }; } @@ -129,4 +132,18 @@ export class FactionService { }); 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/game.controller.ts b/src/game/game.controller.ts index 015baf9673563cd8732ce15493e80e23c19e0e6b..8d7494ba4dfa4a77e22963600e45a7579fca5d5d 100644 --- a/src/game/game.controller.ts +++ b/src/game/game.controller.ts @@ -9,14 +9,15 @@ import { 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, 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') @@ -32,11 +33,27 @@ export class GameController { @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); } + @Delete('delete/:id') + @Roles('admin') + @GameStates('CREATED') + async deleteGame(@Param('id') id: string) { + return this.gameservice.deleteGame(id); + } + + @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') async listGames() { return this.gameservice.listGames(); @@ -61,6 +78,7 @@ export class GameController { } @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 913b6c0f76637ecc94c82c8853cb5428e411f06d..266a8b1564e5b11fb42da755359c79d37f4b170b 100644 --- a/src/game/game.dto.ts +++ b/src/game/game.dto.ts @@ -9,6 +9,9 @@ import { Max, ValidateNested, Allow, + IsUUID, + IsIn, + IsOptional, } from 'class-validator'; import { ObjectivePointEntity } from './game.entity'; @@ -18,6 +21,8 @@ import { CenterDTO, NodeSettingsDTO } from './game.json.dto'; import { Type } from 'class-transformer'; export class GameDTO { + @IsOptional() + id: string; @IsString() @IsNotEmpty() @Length(3, 30) @@ -68,7 +73,17 @@ export class newGameDTO { 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) @@ -85,7 +100,7 @@ export class FlagboxEventDTO { @IsNumber() @Min(0) @Max(3) - owner: number; // owner = 0, => first entry in faction db, owner = 1, => second entry etc + owner: number; // owner = 0, => no owner, owner = 1, => first entry in faction db @IsNumber() @Min(0) @Max(3) diff --git a/src/game/game.entity.ts b/src/game/game.entity.ts index 5f019e83c0b3af08bd423809b79ec5039850bbd2..7ae6e29955d02a2c1368b5f720200f009bd92ee1 100644 --- a/src/game/game.entity.ts +++ b/src/game/game.entity.ts @@ -25,6 +25,7 @@ export class GameEntity { @Column('json') center: CenterDTO; @Column({ type: 'json', nullable: true }) map: JSON; @Column({ type: 'json', nullable: true }) nodesettings?: NodeSettingsDTO; + @Column('text') state: string; @Column('timestamp') startdate: Timestamp; @Column('timestamp') enddate: Timestamp; @@ -55,18 +56,20 @@ export class GameEntity { export class Game_PersonEntity { @PrimaryGeneratedColumn('uuid') gamepersonId: string; @Column({ type: 'text', nullable: true }) role: string; - @ManyToOne(type => FactionEntity, faction => faction.game_persons) + @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', - }) + @OneToOne(type => GameGroupEntity, group => group.leader) leaderGroup: GameGroupEntity; @ManyToOne(type => GameGroupEntity, group => group.players, { - onDelete: 'CASCADE', + onDelete: 'NO ACTION', }) @JoinColumn({ name: 'group' }) group: GameGroupEntity; @@ -78,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; } @@ -89,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.service.ts b/src/game/game.service.ts index 573bb2255ba2d65f828ad3900ff064e1fc471d13..3f6506b649f7dbe09936e283b4658f9765be8ec4 100644 --- a/src/game/game.service.ts +++ b/src/game/game.service.ts @@ -8,7 +8,7 @@ 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 { FactionEntity } from '../faction/faction.entity'; import { NotificationGateway } from '../notifications/notifications.gateway'; @@ -39,6 +39,7 @@ export class GameService { } // 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({ @@ -48,70 +49,114 @@ export class GameService { }); await this.game_PersonRepository.insert(gamePerson); return { - code: 201, message: 'New game added', }; } // edit already created game - async editGame(id: string, gameData: GameDTO) { + 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 already exists', HttpStatus.BAD_REQUEST); + throw new HttpException( + 'Game with the same name already exists', + HttpStatus.BAD_REQUEST, + ); + } + + // 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 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); - updatedGame['id'] = id; const gameId = await this.gameRepository.save(updatedGame); - - // get all the factions that are associated with the game to deny duplicate entries - const factions = await this.factionRepository.find({ game: gameId }); - const factionNames = factions.map(({ factionName }) => factionName); - // add the factions to db + // iterate factions if any were added if (gameData.factions) { - gameData.factions.map(async faction => { - if (!Object.values(factionNames).includes(faction.factionName)) { - let name = await this.factionRepository.create({ - ...faction, - game: gameId, - }); - await this.factionRepository.insert(name); + 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); } }); + // create / update factions present in the submitted data + gameData.factions.map(async faction => { + let name = await this.factionRepository.create({ + ...faction, + game: gameId, + }); + 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 }); } - // get old flagboxes to deny duplicate entries - const flagboxes = await this.objectivePointRepository.find({ - game: gameId, - }); - 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); + 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 }); } // TO DO: ADD FLAGBOX LOCATION TO MAPDRAWING ENTITY return { - code: 200, message: 'Game updated', }; } + 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', + }); + return { + message: 'State was updated', + }; + } + throw new HttpException("Game doesn't exist", HttpStatus.BAD_REQUEST); + } + async listFactions(game: GameEntity) { return this.factionRepository.find({ game }); } @@ -120,7 +165,6 @@ export class GameService { // TODO: Delete factions from Faction table associated with the deleted game await this.gameRepository.delete({ id }); return { - code: 200, message: 'Game deleted', }; } @@ -164,15 +208,15 @@ 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 { - code: 201, message: 'OK', }; } 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 194c5c38155d0f976d286456a53814a08f517050..41e2fcaa36d1928dff9a60bd2c6853e240baf16f 100644 --- a/src/notifications/notifications.module.ts +++ b/src/notifications/notifications.module.ts @@ -3,9 +3,10 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { NotificationGateway } from './notifications.gateway'; import { NotificationEntity } from './notification.entity'; +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/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/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/task/task.controller.ts b/src/task/task.controller.ts index 2cb030ff4332cc890e212f9749f1c84f0aa81248..8283ba5bd2c4d7da4eb668ac6268d5165930e3f4 100644 --- a/src/task/task.controller.ts +++ b/src/task/task.controller.ts @@ -1,8 +1,16 @@ -import { Controller, Post, Body, UsePipes, Get, Param } from '@nestjs/common'; +import { + Controller, + Post, + Body, + UsePipes, + Get, + Param, + Delete, +} from '@nestjs/common'; import { TaskService } from './task.service'; -import { CreateTaskDTO, EditTaskDTO } from './task.dto'; -import { Roles } from '../shared/roles.decorator'; +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'; @@ -28,7 +36,16 @@ export class TaskController { return this.taskService.editTask(data); } - // lists all the tasks for the game if user has game_person entry + // 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') diff --git a/src/task/task.dto.ts b/src/task/task.dto.ts index a8830d7a770f8337c81b1922dcf02751eaf4e6ad..a1576e7d12c9d9a6263b445fdba293fe94e8964a 100644 --- a/src/task/task.dto.ts +++ b/src/task/task.dto.ts @@ -36,3 +36,8 @@ export class EditTaskDTO { @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 index 85ee3c26d3830bdb81ebe89bccac93051e0562c7..f1c1cf103fa4338a7f10d0305888d13c097d7a09 100644 --- a/src/task/task.entity.ts +++ b/src/task/task.entity.ts @@ -16,11 +16,17 @@ export class TaskEntity { @Column({ type: 'text' }) taskDescription: string; @Column({ type: 'bool' }) taskIsActive: boolean; - @ManyToOne(type => FactionEntity, faction => faction.factionId) + @ManyToOne(type => FactionEntity, faction => faction.factionId, { + onDelete: 'CASCADE', + }) faction: FactionEntity; - @ManyToOne(type => FactionEntity, faction => faction.factionId) + @ManyToOne(type => FactionEntity, faction => faction.factionId, { + onDelete: 'CASCADE', + }) taskWinner: FactionEntity; - @ManyToOne(type => GameEntity, game => game.id) + @ManyToOne(type => GameEntity, game => game.id, { + onDelete: 'CASCADE', + }) @JoinColumn({ name: 'taskGame' }) taskGame: GameEntity; } diff --git a/src/task/task.service.ts b/src/task/task.service.ts index abada374a0c79ae4627c0968b80ce8798fadab04..d79338e0d21e9ce741c67d4bf94b89ba64e4c9c9 100644 --- a/src/task/task.service.ts +++ b/src/task/task.service.ts @@ -3,7 +3,7 @@ import { Repository } from 'typeorm'; import { InjectRepository } from '@nestjs/typeorm'; import { TaskEntity } from './task.entity'; -import { CreateTaskDTO, EditTaskDTO } from './task.dto'; +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'; @@ -36,8 +36,8 @@ export class TaskService { // 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() : 'global', - 'new task', + task.faction != null ? task.faction.toString() : task.taskGame.toString(), + { type: 'task-update' }, ); return { message: 'Task added', @@ -67,6 +67,17 @@ export class TaskService { }; } + 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: { @@ -78,23 +89,22 @@ export class TaskService { if (gamePerson.role == 'admin') { return await this.taskRepository.find({ where: { taskGame: taskGame }, - relations: ['faction'], + relations: ['faction', 'taskWinner'], }); } else { return await this.taskRepository.find({ - relations: ['faction'], + relations: ['faction', 'taskWinner'], where: [ { taskGame: taskGame, faction: gamePerson.faction.factionId, - taskIsActive: true, }, { taskGame: taskGame, faction: null, - taskIsActive: true, }, ], + 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 index 140b51e0ce816c2ac5c8b2b11ced8eee2b7d869f..ca0e79ee5195503f61d827dbc170474e66fd1bad 100644 --- a/src/tracking/tracking.controller.ts +++ b/src/tracking/tracking.controller.ts @@ -1,32 +1,42 @@ import { Controller, - Get, Post, Param, UseGuards, UsePipes, Body, + Get, } from '@nestjs/common'; + import { TrackingService } from './tracking.service'; -import { Roles } from '../shared/roles.decorator'; -import { AuthGuard } from '../shared/auth.guard'; -import { ValidationPipe } from '../shared/validation.pipe'; import { TrackingDTO } from './tracking.dto'; -import { User } from 'src/user/user.decorator'; +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') - @UseGuards(new AuthGuard()) - @UsePipes(new ValidationPipe()) @Roles('soldier') + @GameStates('STARTED') + @UsePipes(new ValidationPipe()) async trackLocation( @User('id') userId, @Param('id') id, - @Body() trackdata: TrackingDTO, + @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 index 8dc949c954931fb730a455ba2096f89fe9bb5fec..81cc71597fa93910cb195b1fd4e30f38c62c1f05 100644 --- a/src/tracking/tracking.dto.ts +++ b/src/tracking/tracking.dto.ts @@ -1,8 +1,10 @@ import { Game_PersonEntity } from '../game/game.entity'; -import { Allow } from 'class-validator'; +import { Allow, ValidateNested } from 'class-validator'; +import { GeoDTO } from './geo.dto'; +import { Type } from 'class-transformer'; export class TrackingDTO { - @Allow() - data: JSON; - gamepersonId: Game_PersonEntity; + @ValidateNested() + @Type(() => GeoDTO) + data: GeoDTO; } diff --git a/src/tracking/tracking.entity.ts b/src/tracking/tracking.entity.ts index c64f64f1bb90a8f7cbcb662956f883f895f17221..c2699a09388ce8ad47c7119b46f4614fa52b34ec 100644 --- a/src/tracking/tracking.entity.ts +++ b/src/tracking/tracking.entity.ts @@ -1,17 +1,19 @@ -import { - Entity, - PrimaryGeneratedColumn, - Column, - ManyToOne, - OneToOne, - JoinColumn, -} from 'typeorm'; +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: JSON; - @ManyToOne(type => Game_PersonEntity, person => person.gamepersonId) + @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 index a6f3ff1734adba3435360213c13cf024fe3cb288..45b167fc526ab41addbb87575518c9acae884482 100644 --- a/src/tracking/tracking.module.ts +++ b/src/tracking/tracking.module.ts @@ -1,9 +1,10 @@ import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + import { TrackingController } from './tracking.controller'; import { TrackingService } from './tracking.service'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { Game_PersonEntity } from 'src/game/game.entity'; import { TrackingEntity } from './tracking.entity'; +import { Game_PersonEntity } from '../game/game.entity'; @Module({ imports: [TypeOrmModule.forFeature([TrackingEntity, Game_PersonEntity])], diff --git a/src/tracking/tracking.service.ts b/src/tracking/tracking.service.ts index 06f665c4f8471c0863ab8c22da34c0fa9fb9d36b..9941256dde52240d7c90b45d61b7b32ef4e1c242 100644 --- a/src/tracking/tracking.service.ts +++ b/src/tracking/tracking.service.ts @@ -1,9 +1,13 @@ import { Injectable, HttpStatus, HttpException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; + import { Game_PersonEntity } from '../game/game.entity'; -import { InjectRepository } from '@nestjs/typeorm'; 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 { @@ -14,11 +18,11 @@ export class TrackingService { private gamepersonrepository: Repository<Game_PersonEntity>, ) {} - async trackLocation(personId, gameId, trackdata: TrackingDTO) { + async trackLocation(personId, gameId, trackdata: GeoDTO) { // find player let gameperson = await this.gamepersonrepository.findOne({ - game: gameId, - person: personId, + where: { game: gameId, person: personId }, + relations: ['faction'], }); if (!gameperson) { throw new HttpException( @@ -32,28 +36,64 @@ export class TrackingService { // if player has pushed tracking data, update entry if (trackedperson) { + trackdata['time'] = Date.now(); //add coordinates - trackedperson.data['geometry']['coordinates'].push( - await this.mapFunction(trackdata.data['geometry']['coordinates']), - ); + trackedperson.data.push(trackdata); //add timestamp - trackedperson.data['geometry']['properties']['time'].push( - new Date(Date.now()), - ); - - return await this.trackingrepository.save(trackedperson); + await this.trackingrepository.save(trackedperson); + return { code: 201, message: 'Location updated!' }; } else { // first entry will be empty - // initialize coordinates - trackdata.data['geometry']['coordinates'] = []; - // initialize timestamp - trackdata.data['geometry']['properties']['time'] = []; - trackedperson = await this.trackingrepository.create(trackdata); + 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; - return await this.trackingrepository.save(trackedperson); + 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;