From 933fe2635b8ad7796520abe501ec21e934e9b435 Mon Sep 17 00:00:00 2001 From: legonzaur Date: Sun, 29 Dec 2024 12:04:19 +0100 Subject: [PATCH] working on words --- .../dto/conceptinturnresponse.dto.ts | 0 .../dto/conceptresponse.dto.ts | 0 {src/games => events}/dto/gameresponse.dto.ts | 2 +- .../dto/playerresponse.dto.ts | 2 +- {src/users => events}/dto/userresponse.dto.ts | 0 {src/word => events}/dto/wordresponse.dto.ts | 6 +- .../events/lobby.ts => events/lobby.events.ts | 7 +-- events/word.events.ts | 26 ++++++++ src/games/controllers/lobby.controller.ts | 25 ++++---- src/games/services/lobby.service.ts | 2 +- src/users/guards/auth.guard.ts | 17 ++++-- src/users/user.pipe.ts | 17 ++++++ src/users/users.controller.ts | 2 +- src/users/users.service.ts | 2 +- src/word/controllers/word.controller.ts | 61 ++++++++++++++----- src/word/entities/word.entity.ts | 5 +- src/word/pipe/word.pipe.ts | 20 ++++++ src/word/services/word.service.ts | 34 ++++++++++- .../express-session.d.ts | 0 19 files changed, 182 insertions(+), 46 deletions(-) rename {src/games => events}/dto/conceptinturnresponse.dto.ts (100%) rename {src/games => events}/dto/conceptresponse.dto.ts (100%) rename {src/games => events}/dto/gameresponse.dto.ts (92%) rename {src/games => events}/dto/playerresponse.dto.ts (82%) rename {src/users => events}/dto/userresponse.dto.ts (100%) rename {src/word => events}/dto/wordresponse.dto.ts (64%) rename src/games/events/lobby.ts => events/lobby.events.ts (60%) create mode 100644 events/word.events.ts create mode 100644 src/users/user.pipe.ts create mode 100644 src/word/pipe/word.pipe.ts rename {src/types.d.ts => types.d.ts}/express-session.d.ts (100%) diff --git a/src/games/dto/conceptinturnresponse.dto.ts b/events/dto/conceptinturnresponse.dto.ts similarity index 100% rename from src/games/dto/conceptinturnresponse.dto.ts rename to events/dto/conceptinturnresponse.dto.ts diff --git a/src/games/dto/conceptresponse.dto.ts b/events/dto/conceptresponse.dto.ts similarity index 100% rename from src/games/dto/conceptresponse.dto.ts rename to events/dto/conceptresponse.dto.ts diff --git a/src/games/dto/gameresponse.dto.ts b/events/dto/gameresponse.dto.ts similarity index 92% rename from src/games/dto/gameresponse.dto.ts rename to events/dto/gameresponse.dto.ts index 7fd1840..b3f755c 100644 --- a/src/games/dto/gameresponse.dto.ts +++ b/events/dto/gameresponse.dto.ts @@ -1,5 +1,5 @@ import { Expose, Type } from 'class-transformer'; -import { UserResponseDTO } from 'src/users/dto/userresponse.dto'; +import { UserResponseDTO } from './userresponse.dto'; import { PlayerResponseDTO } from './playerresponse.dto'; import { ConceptInTurnResponseDTO } from './conceptinturnresponse.dto'; diff --git a/src/games/dto/playerresponse.dto.ts b/events/dto/playerresponse.dto.ts similarity index 82% rename from src/games/dto/playerresponse.dto.ts rename to events/dto/playerresponse.dto.ts index ae2e45b..4ac8a53 100644 --- a/src/games/dto/playerresponse.dto.ts +++ b/events/dto/playerresponse.dto.ts @@ -1,5 +1,5 @@ import { Expose, Type } from 'class-transformer'; -import { UserResponseDTO } from 'src/users/dto/userresponse.dto'; +import { UserResponseDTO } from './userresponse.dto'; export class PlayerResponseDTO { constructor(partial: Partial) { diff --git a/src/users/dto/userresponse.dto.ts b/events/dto/userresponse.dto.ts similarity index 100% rename from src/users/dto/userresponse.dto.ts rename to events/dto/userresponse.dto.ts diff --git a/src/word/dto/wordresponse.dto.ts b/events/dto/wordresponse.dto.ts similarity index 64% rename from src/word/dto/wordresponse.dto.ts rename to events/dto/wordresponse.dto.ts index ecde5aa..2d84c37 100644 --- a/src/word/dto/wordresponse.dto.ts +++ b/events/dto/wordresponse.dto.ts @@ -1,5 +1,4 @@ -import { Expose, Type } from 'class-transformer'; -import { UserResponseDTO } from 'src/users/dto/userresponse.dto'; +import { Expose } from 'class-transformer'; export class WordResponseDTO { constructor(partial: Partial) { @@ -13,4 +12,7 @@ export class WordResponseDTO { @Expose() ownerId: string; + + @Expose() + enabled: boolean; } diff --git a/src/games/events/lobby.ts b/events/lobby.events.ts similarity index 60% rename from src/games/events/lobby.ts rename to events/lobby.events.ts index 16c453f..3a98aa2 100644 --- a/src/games/events/lobby.ts +++ b/events/lobby.events.ts @@ -1,6 +1,5 @@ -import { GameResponseDTO } from 'src/games/dto/gameresponse.dto'; -import { PlayerResponseDTO } from 'src/games/dto/playerresponse.dto'; -import { Game } from 'src/games/entities/game.entity'; +import { GameResponseDTO } from './dto/gameresponse.dto'; +import { PlayerResponseDTO } from './dto/playerresponse.dto'; export class GameCreateEvent extends GameResponseDTO { type: 'create'; @@ -15,7 +14,7 @@ export class GameJoinEvent { gameId: number; constructor( public player: PlayerResponseDTO, - game?: Game, + game?: Partial, ) { this.gameId = game.id; } diff --git a/events/word.events.ts b/events/word.events.ts new file mode 100644 index 0000000..7a901c1 --- /dev/null +++ b/events/word.events.ts @@ -0,0 +1,26 @@ +import { WordResponseDTO } from './dto/wordresponse.dto'; + +export class WordCreateEvent { + type: 'create'; + constructor(public word: WordResponseDTO) {} +} + +export class WordEditEvent { + type: 'edit'; + constructor(public word: WordResponseDTO) {} +} + +export class WordDeleteEvent { + type: 'delete'; + constructor(public id: number) {} +} + +export class WordEnableEvent { + type: 'enable'; + constructor(public id: number) {} +} + +export class WordDisableEvent { + type: 'disable'; + constructor(public id: number) {} +} diff --git a/src/games/controllers/lobby.controller.ts b/src/games/controllers/lobby.controller.ts index 4955379..b1a4c24 100644 --- a/src/games/controllers/lobby.controller.ts +++ b/src/games/controllers/lobby.controller.ts @@ -17,8 +17,9 @@ import { GamePipe } from '../pipe/game.pipe'; import { Game } from '../entities/game.entity'; import { Observable, fromEvent, map } from 'rxjs'; import { EventEmitter2 } from '@nestjs/event-emitter'; -import { GameResponseDTO } from '../dto/gameresponse.dto'; -import { SessionData } from 'express-session'; +import { GameResponseDTO } from '../../../events/dto/gameresponse.dto'; +import { GetUser } from 'src/users/user.pipe'; +import { User } from 'src/users/entities/user.entity'; @ApiTags('lobby') @Controller('games') @@ -41,15 +42,15 @@ export class LobbyController { }) @ApiResponse({ status: 403, description: 'Forbidden.' }) @UseGuards(AuthGuard) - async create(@Session() session: SessionData) { - const games = await this.lobbyService.findByOwner(session.user); + async create(@GetUser() user: User) { + const games = await this.lobbyService.findByOwner(user); if (games) { throw new HttpException( 'You can only create one game at a time', HttpStatus.BAD_REQUEST, ); } - const createdGame = await this.lobbyService.create(session.user); + const createdGame = await this.lobbyService.create(user); return new GameResponseDTO(createdGame); } @@ -85,10 +86,10 @@ export class LobbyController { @UseGuards(AuthGuard) async remove( @Param('id') id: string, - @Session() session: SessionData, @Param('id', GamePipe) game: Game, + @GetUser() user: User, ) { - if (game.owner.id != session.user.id) { + if (game.owner.id != user.id) { throw new HttpException( 'You can only delete games you own', HttpStatus.FORBIDDEN, @@ -102,20 +103,20 @@ export class LobbyController { @UseGuards(AuthGuard) async join( @Param('id') id: string, - @Session() session: SessionData, @Param('id', GamePipe) game: Game, + @GetUser() user: User, ) { if (game.started) { throw new HttpException('Game already started', HttpStatus.BAD_REQUEST); } - if (game.players.some((p) => p.user.id == session.user.id)) { + if (game.players.some((p) => p.user.id == user.id)) { throw new HttpException( 'You are already in this game', HttpStatus.BAD_REQUEST, ); } - return await this.lobbyService.joinGame(game, session.user); + return await this.lobbyService.joinGame(game, user); } @Post(':id/leave') @@ -123,12 +124,12 @@ export class LobbyController { @UseGuards(AuthGuard) leave( @Param('id') id: string, - @Session() session: SessionData, @Param('id', GamePipe) game: Game, + @GetUser() user: User, ) { if (game.started) { throw new HttpException('Game already started', HttpStatus.BAD_REQUEST); } - return this.lobbyService.leaveGame(game, session.user); + return this.lobbyService.leaveGame(game, user); } } diff --git a/src/games/services/lobby.service.ts b/src/games/services/lobby.service.ts index 6d1acd4..8c8c433 100644 --- a/src/games/services/lobby.service.ts +++ b/src/games/services/lobby.service.ts @@ -11,7 +11,7 @@ import { GameCreateEvent, GameDeleteEvent, GameJoinEvent, -} from 'src/games/events/lobby'; +} from 'events/lobby.events'; @Injectable() export class LobbyService { diff --git a/src/users/guards/auth.guard.ts b/src/users/guards/auth.guard.ts index f58d3cd..3a32adf 100644 --- a/src/users/guards/auth.guard.ts +++ b/src/users/guards/auth.guard.ts @@ -6,18 +6,25 @@ import { HttpStatus, } from '@nestjs/common'; import { Request } from 'express'; -import { Observable } from 'rxjs'; +import { SessionData } from 'express-session'; +import { UsersService } from '../users.service'; @Injectable() export class AuthGuard implements CanActivate { - canActivate( - context: ExecutionContext, - ): boolean | Promise | Observable { + constructor(private userService: UsersService) {} + async canActivate(context: ExecutionContext): Promise { const http = context.switchToHttp(); const request = http.getRequest(); - if (!(request.session as Record).user) { + if (!(request.session as SessionData).user) { throw new HttpException('Please log in', HttpStatus.UNAUTHORIZED); } + const user = await this.userService.findUser(request.session.user.id); + if (!user) { + throw new HttpException( + 'Session expired. Please log in', + HttpStatus.UNAUTHORIZED, + ); + } return true; } } diff --git a/src/users/user.pipe.ts b/src/users/user.pipe.ts new file mode 100644 index 0000000..0401e0a --- /dev/null +++ b/src/users/user.pipe.ts @@ -0,0 +1,17 @@ +import { + createParamDecorator, + HttpException, + HttpStatus, +} from '@nestjs/common'; + +export const GetUser = createParamDecorator( + (data: unknown, req: Record) => { + if (!req.user) { + throw new HttpException( + 'You are not authentified', + HttpStatus.UNAUTHORIZED, + ); + } + return req.user; + }, +); diff --git a/src/users/users.controller.ts b/src/users/users.controller.ts index face072..ac258a2 100644 --- a/src/users/users.controller.ts +++ b/src/users/users.controller.ts @@ -10,7 +10,7 @@ import { ParseIntPipe, } from '@nestjs/common'; import { UsersService } from './users.service'; -import { UserResponseDTO } from './dto/userresponse.dto'; +import { UserResponseDTO } from '../../events/dto/userresponse.dto'; import { ApiTags } from '@nestjs/swagger'; import { ConfigService } from '@nestjs/config'; diff --git a/src/users/users.service.ts b/src/users/users.service.ts index c201543..b633d02 100644 --- a/src/users/users.service.ts +++ b/src/users/users.service.ts @@ -2,7 +2,7 @@ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; -import { UserResponseDTO } from './dto/userresponse.dto'; +import { UserResponseDTO } from '../../events/dto/userresponse.dto'; import { URLSearchParams } from 'url'; import { User } from './entities/user.entity'; import { InjectRepository } from '@nestjs/typeorm'; diff --git a/src/word/controllers/word.controller.ts b/src/word/controllers/word.controller.ts index 9cbe0be..32fabfc 100644 --- a/src/word/controllers/word.controller.ts +++ b/src/word/controllers/word.controller.ts @@ -6,8 +6,9 @@ import { HttpException, HttpStatus, Param, + ParseIntPipe, Post, - Session, + Put, Sse, UseGuards, } from '@nestjs/common'; @@ -15,19 +16,34 @@ import { import { WordService } from '../services/word.service'; import { ApiBody, ApiOperation, ApiTags } from '@nestjs/swagger'; import { AuthGuard } from 'src/users/guards/auth.guard'; -import { SessionData } from 'express-session'; -import { UsersService } from 'src/users/users.service'; -import { WordResponseDTO } from '../dto/wordresponse.dto'; +import { WordResponseDTO } from '../../../events/dto/wordresponse.dto'; import { WordRecieveDTO } from '../dto/wordrecieve.dto'; +import { WordPipe } from '../pipe/word.pipe'; +import { Word } from '../entities/word.entity'; +import { GetUser } from 'src/users/user.pipe'; +import { User } from 'src/users/entities/user.entity'; +import { fromEvent, map, Observable } from 'rxjs'; +import { EventEmitter2 } from '@nestjs/event-emitter'; @ApiTags('words') @Controller('words') export class WordController { constructor( private readonly wordService: WordService, - private readonly userService: UsersService, + private eventEmitter: EventEmitter2, ) {} + @Sse('/subscribe') + subscribe(): Observable<{ data: string }> { + return fromEvent(this.eventEmitter, this.wordService.sse_prefix).pipe( + map((payload) => { + return { + data: JSON.stringify(payload), + }; + }), + ); + } + @Delete() @ApiOperation({ summary: 'Delete a word' }) @UseGuards(AuthGuard) @@ -43,21 +59,38 @@ export class WordController { @ApiOperation({ summary: 'Submit a word' }) @ApiBody({ type: WordRecieveDTO }) @UseGuards(AuthGuard) - async create(@Body('value') value: string, @Session() session: SessionData) { - const user = await this.userService.findUser(session.user.id); - if (!user) { - throw new HttpException( - 'You are not authentified', - HttpStatus.UNAUTHORIZED, - ); - } + async create(@Body('value') value: string, @GetUser() user: User) { const word = await this.wordService.create(value, user); return new WordResponseDTO(word); } @Get('/') - @ApiOperation({ summary: 'List all words' }) + @ApiOperation({ summary: 'List enabled words' }) async read() { + return (await this.wordService.listActive()).map( + (w) => new WordResponseDTO(w), + ); + } + + @Get('/all') + @ApiOperation({ summary: 'List all words' }) + async readAll() { return (await this.wordService.list()).map((w) => new WordResponseDTO(w)); } + + @Put('/enable/:id') + @ApiOperation({ summary: 'Enable a word' }) + @UseGuards(AuthGuard) + async enable(@Param('id', ParseIntPipe, WordPipe) word: Word) { + await this.wordService.enable(word); + return new WordResponseDTO(word); + } + + @Put('/disable/:id') + @ApiOperation({ summary: 'Disable a word' }) + @UseGuards(AuthGuard) + async disable(@Param('id', ParseIntPipe, WordPipe) word: Word) { + await this.wordService.enable(word); + return new WordResponseDTO(word); + } } diff --git a/src/word/entities/word.entity.ts b/src/word/entities/word.entity.ts index 569fee0..918c41f 100644 --- a/src/word/entities/word.entity.ts +++ b/src/word/entities/word.entity.ts @@ -15,7 +15,7 @@ export class Word { @PrimaryGeneratedColumn() id: number; - @Column() + @Column({ unique: true }) value: string; @JoinColumn({ name: 'ownerId' }) @@ -25,6 +25,9 @@ export class Word { @Column({ name: 'ownerId' }) ownerId: string; + @Column({ default: false }) + enabled: boolean; + @ManyToMany(() => Topic, (topic) => topic.words) @JoinTable() topics: Topic[]; diff --git a/src/word/pipe/word.pipe.ts b/src/word/pipe/word.pipe.ts new file mode 100644 index 0000000..f5f5e78 --- /dev/null +++ b/src/word/pipe/word.pipe.ts @@ -0,0 +1,20 @@ +import { + HttpException, + HttpStatus, + Injectable, + PipeTransform, +} from '@nestjs/common'; +import { WordService } from '../services/word.service'; + +@Injectable() +export class WordPipe implements PipeTransform { + constructor(private readonly wordService: WordService) {} + + async transform(id: number) { + const word = await this.wordService.getById(id); + if (!word) { + throw new HttpException('Word not found', HttpStatus.NOT_FOUND); + } + return word; + } +} diff --git a/src/word/services/word.service.ts b/src/word/services/word.service.ts index 509512a..4ab4a53 100644 --- a/src/word/services/word.service.ts +++ b/src/word/services/word.service.ts @@ -3,11 +3,20 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Word } from '../entities/word.entity'; import { User } from 'src/users/entities/user.entity'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { + WordCreateEvent, + WordDeleteEvent, + WordDisableEvent, + WordEnableEvent, +} from '../../../events/word.events'; @Injectable() export class WordService { + sse_prefix = 'sse.word'; constructor( @InjectRepository(Word) private wordRepository: Repository, + private eventEmitter: EventEmitter2, ) {} async getById(id: number) { @@ -15,7 +24,8 @@ export class WordService { } async delete(word: Word) { - return await this.wordRepository.remove(word); + await this.wordRepository.remove(word); + this.eventEmitter.emit(this.sse_prefix, new WordDeleteEvent(word.id)); } async create(value: string, owner: User) { @@ -23,11 +33,29 @@ export class WordService { word.owner = owner; word.value = value; await this.wordRepository.save(word); + this.eventEmitter.emit(this.sse_prefix, new WordCreateEvent(word)); return word; } - async list() { - const words = await this.wordRepository.find({}); + async listActive() { + const words = await this.wordRepository.findBy({ enabled: true }); return words; } + + async list() { + const words = await this.wordRepository.find(); + return words; + } + + async enable(word: Word) { + word.enabled = true; + await this.wordRepository.save(word); + this.eventEmitter.emit(this.sse_prefix, new WordEnableEvent(word.id)); + } + + async disable(word: Word) { + word.enabled = false; + await this.wordRepository.save(word); + this.eventEmitter.emit(this.sse_prefix, new WordDisableEvent(word.id)); + } } diff --git a/src/types.d.ts/express-session.d.ts b/types.d.ts/express-session.d.ts similarity index 100% rename from src/types.d.ts/express-session.d.ts rename to types.d.ts/express-session.d.ts