Skip to content

Commit

Permalink
Merge pull request #355 from bounswe/feature/BE/321/follow-unfollow
Browse files Browse the repository at this point in the history
added follow and unfollow endpoints for game, added tokenDecoder middleware
  • Loading branch information
omersafakbebek authored Oct 30, 2023
2 parents 134d100 + ac4e6d9 commit 31e3753
Show file tree
Hide file tree
Showing 9 changed files with 160 additions and 51 deletions.
12 changes: 9 additions & 3 deletions ludos/backend/src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Module } from '@nestjs/common';
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { AppController } from './controllers/app.controller';
import { AppService } from './services/app.service';
import { ConfigModule } from '@nestjs/config';
Expand All @@ -13,6 +13,8 @@ import { JwtConfigService } from './services/config/jwt-config.service';
import { GameController } from './controllers/game.controller';
import { GameService } from './services/game.service';
import { GameRepository } from './repositories/game.repository';
import { Game } from './entities/game.entity';
import { TokenDecoderMiddleware } from './middlewares/tokenDecoder.middleware';

@Module({
imports: [
Expand All @@ -27,7 +29,7 @@ import { GameRepository } from './repositories/game.repository';
useClass: TypeOrmConfigService,
inject: [TypeOrmConfigService],
}),
TypeOrmModule.forFeature([User]),
TypeOrmModule.forFeature([User, Game]),
],
controllers: [AppController, UserController, GameController],
providers: [
Expand All @@ -38,4 +40,8 @@ import { GameRepository } from './repositories/game.repository';
GameService,
],
})
export class AppModule {}
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(TokenDecoderMiddleware).forRoutes('*');
}
}
60 changes: 52 additions & 8 deletions ludos/backend/src/controllers/game.controller.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,31 @@
import { Body, Controller, Get, HttpCode, NotFoundException, Param, Post } from '@nestjs/common';
import {
Body,
Controller,
Get,
HttpCode,
HttpStatus,
NotFoundException,
Param,
Post,
Put,
Req,
UseGuards,
} from '@nestjs/common';
import {
ApiBadRequestResponse,
ApiBearerAuth,
ApiConflictResponse,
ApiCreatedResponse,
ApiForbiddenResponse,
ApiNotFoundResponse,
ApiOperation,
ApiTags,
} from '@nestjs/swagger';
import { GameService } from '../services/game.service';
import { GameCreateResponseDto } from '../dtos/game/response/create.response';
import { GameCreateDto } from '../dtos/game/request/create.dto';
import { GameCreateResponseDto } from '../dtos/game/response/create.response';
import { AuthorizedRequest } from '../interfaces/common/authorized-request.interface';
import { GameService } from '../services/game.service';
import { AuthGuard } from '../services/guards/auth.guard';

@ApiTags('game')
@Controller('game')
Expand All @@ -34,16 +51,43 @@ export class GameController {
}

@ApiOperation({ summary: 'Get Game by ID Endpoint' })
@ApiBearerAuth()
@Get(':id')
public async getGame(@Param('id') id: string) {
console.log(id);
const game = await this.gameService.getGame(id);
public async getGame(@Req() req: AuthorizedRequest, @Param('id') id: string) {
const game = await this.gameService.getGame(id, req.user && req.user.id);
if (game) {
return game;
} else {
throw new NotFoundException('Game not found');
}
}
}

@ApiBearerAuth()
@ApiOperation({ summary: 'Follow a game' })
@ApiNotFoundResponse({ description: 'Game is not found!' })
@ApiConflictResponse({ description: 'Game is already being followed!' })
@ApiForbiddenResponse({ description: 'User should login' })
@UseGuards(AuthGuard)
@Put('/follow/:gameId')
public async followGame(
@Req() req: AuthorizedRequest,
@Param('gameId') gameId: string,
) {
await this.gameService.followGame(req.user.id, gameId);
return HttpStatus.OK;
}

@ApiBearerAuth()
@ApiOperation({ summary: 'Unfollow a game' })
@ApiNotFoundResponse({ description: 'Game is not found!' })
@ApiConflictResponse({ description: 'Game is not being followed!' })
@ApiForbiddenResponse({ description: 'User should login' })
@UseGuards(AuthGuard)
@Put('/unfollow/:gameId')
public async unfollowGame(
@Req() req: AuthorizedRequest,
@Param('gameId') gameId: string,
) {
await this.gameService.unfollowGame(req.user.id, gameId);
return HttpStatus.OK;
}
}
25 changes: 21 additions & 4 deletions ludos/backend/src/entities/game.entity.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import {
Column,
Entity,
JoinTable,
ManyToMany,
PrimaryGeneratedColumn,
VirtualColumn,
} from 'typeorm';
import { User } from './user.entity';

@Entity('games')
export class Game {
Expand All @@ -17,9 +25,6 @@ export class Game {
@Column({ type: 'float', default: 0 })
userRating: number;

@Column({ type: 'int', default: 0 })
followers: number;

@Column('jsonb')
systemRequirements: {
minimum: {
Expand Down Expand Up @@ -90,4 +95,16 @@ export class Game {

@Column('text', { array: true, default: '{}' })
reviews: string[];

@ManyToMany(() => User, (user) => user.followedGames)
@JoinTable({ name: 'game_user_follows' })
followerList: User[];

@VirtualColumn({
query: (alias) =>
`SELECT COUNT(*) as count FROM game_user_follows WHERE "gamesId" = ${alias}.id`,
})
followers: number;

isFollowed: boolean;
}
5 changes: 5 additions & 0 deletions ludos/backend/src/entities/user.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import {
BeforeInsert,
BeforeUpdate,
Index,
ManyToMany,
} from 'typeorm';
import * as bcrypt from 'bcrypt';
import { Game } from './game.entity';
@Entity('users')
export class User {
@PrimaryGeneratedColumn('uuid')
Expand All @@ -22,6 +24,9 @@ export class User {
@Column()
password: string;

@ManyToMany(() => Game, (game) => game.followerList)
followedGames: Game[];

@BeforeInsert()
@BeforeUpdate()
async hashPasswordBeforeInsertAndUpdate() {
Expand Down
23 changes: 23 additions & 0 deletions ludos/backend/src/middlewares/tokenDecoder.middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { AuthorizedRequest } from '../interfaces/common/authorized-request.interface';
import { Payload } from '../interfaces/user/payload.interface';
import { JwtService } from '@nestjs/jwt';

@Injectable()
export class TokenDecoderMiddleware implements NestMiddleware {
constructor(private jwtService: JwtService) {}

async use(req: AuthorizedRequest, res: Response, next: NextFunction) {
const token = this.extractTokenFromHeader(req);
try {
const payload: Payload = await this.jwtService.verifyAsync(token);
req.user = payload;
} catch {}
next();
}
private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}
10 changes: 9 additions & 1 deletion ludos/backend/src/repositories/game.repository.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common';
import { Game } from '../entities/game.entity';
import { DataSource, Repository } from 'typeorm';
import { Game } from '../entities/game.entity';

@Injectable()
export class GameRepository extends Repository<Game> {
Expand All @@ -18,4 +18,12 @@ export class GameRepository extends Repository<Game> {
return this.findOneBy({ id });
}

public findGameByIdWithFollowerList(id: string): Promise<Game> {
return this.findOne({ where: { id }, relations: ['followerList'] });
}

public async updateGame(input: Partial<Game>): Promise<void> {
const game = this.create(input);
await this.save(game);
}
}
1 change: 0 additions & 1 deletion ludos/backend/src/repositories/user.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ export class UserRepository extends Repository<User> {
public findUserByUsername(username: string): Promise<User> {
return this.findOneBy({ username });
}

public findUserById(id: string): Promise<User> {
return this.findOneBy({ id });
}
Expand Down
47 changes: 38 additions & 9 deletions ludos/backend/src/services/game.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,19 @@ import {
InternalServerErrorException,
NotFoundException,
} from '@nestjs/common';
import { GameRepository } from '../repositories/game.repository';
import { Game } from 'entities/game.entity';
import { GameCreateDto } from '../dtos/game/request/create.dto';
import { JwtService } from '@nestjs/jwt';
import { GameCreateResponseDto } from '../dtos/game/response/create.response';
import { Game } from 'entities/game.entity';
import { GameRepository } from '../repositories/game.repository';
import { UserRepository } from '../repositories/user.repository';

@Injectable()
export class GameService {
constructor(
private readonly gameRepository: GameRepository,
private readonly jwtService: JwtService,
private readonly userRepository: UserRepository,
) {}


public async createGame(
input: GameCreateDto,
): Promise<GameCreateResponseDto> {
Expand All @@ -39,14 +38,44 @@ export class GameService {
}
}

public async getGame(id: string, userId?: string): Promise<Game> {
const game = await this.gameRepository.findGameByIdWithFollowerList(id);
if (!game) {
throw new NotFoundException('Game not found');
}
game.isFollowed = userId
? game.followerList.some((user) => user.id === userId)
: false;
return game;
}
async followGame(userId: string, gameId: string): Promise<void> {
const game = await this.gameRepository.findGameByIdWithFollowerList(gameId);
if (!game) {
throw new NotFoundException('Game Not Found!');
}
if (game.followerList.filter((user) => user.id === userId).length != 0) {
throw new ConflictException('The game is followed already!');
}
const user = await this.userRepository.findUserById(userId);
if (!user) {
throw new NotFoundException('User Not Found!');
}

public async getGame(id: string): Promise<Game> {
const game = await this.gameRepository.findGameById(id);
game.followerList.push(user);

await this.gameRepository.updateGame(game);
}

async unfollowGame(userId: string, gameId: string): Promise<void> {
const game = await this.gameRepository.findGameByIdWithFollowerList(gameId);
if (!game) {
throw new NotFoundException('Game not found');
throw new ConflictException('The game is not followed!');
}
if (game.followerList.filter((user) => user.id === userId).length == 0) {
throw new NotFoundException('The Game is not followed!');
}
game.followerList = game.followerList.filter((user) => user.id !== userId);

return game;
await this.gameRepository.updateGame(game);
}
}
28 changes: 3 additions & 25 deletions ludos/backend/src/services/guards/auth.guard.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,13 @@
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Request } from 'express';
import { Payload } from '../../interfaces/user/payload.interface';
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { AuthorizedRequest } from '../../interfaces/common/authorized-request.interface';

@Injectable()
export class AuthGuard implements CanActivate {
constructor(private jwtService: JwtService) {}

async canActivate(context: ExecutionContext): Promise<boolean> {
const request: AuthorizedRequest = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request);
if (!token) {
throw new UnauthorizedException('access_token_should_be_provided');
}
try {
const payload: Payload = await this.jwtService.verifyAsync(token);
request.user = payload;
} catch {
throw new UnauthorizedException('token_unauthorized');
if (!request.user) {
return false;
}
return true;
}

private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}

0 comments on commit 31e3753

Please sign in to comment.