working on words

This commit is contained in:
2024-12-29 12:04:19 +01:00
parent de8b4c5000
commit 933fe2635b
19 changed files with 182 additions and 46 deletions
@@ -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';
@@ -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<PlayerResponseDTO>) {
@@ -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<WordResponseDTO>) {
@@ -13,4 +12,7 @@ export class WordResponseDTO {
@Expose()
ownerId: string;
@Expose()
enabled: boolean;
}
@@ -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<GameResponseDTO>,
) {
this.gameId = game.id;
}
+26
View File
@@ -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) {}
}
+13 -12
View File
@@ -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);
}
}
+1 -1
View File
@@ -11,7 +11,7 @@ import {
GameCreateEvent,
GameDeleteEvent,
GameJoinEvent,
} from 'src/games/events/lobby';
} from 'events/lobby.events';
@Injectable()
export class LobbyService {
+12 -5
View File
@@ -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<boolean> | Observable<boolean> {
constructor(private userService: UsersService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const http = context.switchToHttp();
const request = http.getRequest<Request>();
if (!(request.session as Record<string, any>).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;
}
}
+17
View File
@@ -0,0 +1,17 @@
import {
createParamDecorator,
HttpException,
HttpStatus,
} from '@nestjs/common';
export const GetUser = createParamDecorator(
(data: unknown, req: Record<string, string>) => {
if (!req.user) {
throw new HttpException(
'You are not authentified',
HttpStatus.UNAUTHORIZED,
);
}
return req.user;
},
);
+1 -1
View File
@@ -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';
+1 -1
View File
@@ -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';
+47 -14
View File
@@ -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);
}
}
+4 -1
View File
@@ -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[];
+20
View File
@@ -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;
}
}
+31 -3
View File
@@ -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<Word>,
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));
}
}