diff --git a/backend/code/prisma/dbml/schema.dbml b/backend/code/prisma/dbml/schema.dbml index 76ee46e..9a1325f 100644 --- a/backend/code/prisma/dbml/schema.dbml +++ b/backend/code/prisma/dbml/schema.dbml @@ -11,7 +11,7 @@ Table users { refreshedHash String intraId String [unique] profileFinished Boolean [not null, default: false] - Username String + Username String [unique] firstName String lastName String discreption String [not null, default: ''] @@ -49,6 +49,7 @@ Table blocked_friends { blocked_by_id String [unique, not null] Blocked users [not null] blocked_id String [unique, not null] + dmRoomId String } Table matches { @@ -136,7 +137,7 @@ Ref: blocked_friends.blocked_id > users.userId Ref: matches.participant1Id > users.userId -Ref: messages.roomId > rooms.id +Ref: messages.roomId > rooms.id [delete: Cascade] Ref: rooms.ownerId > users.userId diff --git a/backend/code/prisma/schema.prisma b/backend/code/prisma/schema.prisma index d116cd5..190f1c4 100644 --- a/backend/code/prisma/schema.prisma +++ b/backend/code/prisma/schema.prisma @@ -25,7 +25,7 @@ model User { intraId String? @unique profileFinished Boolean @default(false) - Username String? + Username String? @unique firstName String? lastName String? discreption String @default("") @@ -66,6 +66,7 @@ model BlockedUsers { blocked_by_id String @unique Blocked User @relation("blocked", fields: [blocked_id], references: [userId]) blocked_id String @unique + dmRoomId String? @@map("blocked_friends") } @@ -92,7 +93,7 @@ model Message { id String @id @default(cuid()) authorId String - room Room @relation(fields: [roomId], references: [id]) + room Room @relation(fields: [roomId], references: [id], onDelete: Cascade) roomId String content String? diff --git a/backend/code/src/app.module.ts b/backend/code/src/app.module.ts index 6d88f2e..7540be5 100644 --- a/backend/code/src/app.module.ts +++ b/backend/code/src/app.module.ts @@ -11,6 +11,7 @@ import { CloudinaryModule } from './cloudinary/cloudinary.module'; import { MessagesModule } from './messages/messages.module'; import { EventEmitterModule } from '@nestjs/event-emitter'; import { Gateways } from './gateways/gateways.gateway'; +import { GameModule } from './game/game.module'; @Module({ imports: [ @@ -23,6 +24,7 @@ import { Gateways } from './gateways/gateways.gateway'; CloudinaryModule, MessagesModule, EventEmitterModule.forRoot(), + GameModule, ], controllers: [AppController], providers: [PrismaService, Gateways], diff --git a/backend/code/src/auth/auth.controller.ts b/backend/code/src/auth/auth.controller.ts index ac37fb6..25ab35c 100644 --- a/backend/code/src/auth/auth.controller.ts +++ b/backend/code/src/auth/auth.controller.ts @@ -49,7 +49,7 @@ export class AuthController { @ApiExcludeEndpoint() @Get('login/42/return') @UseGuards(FtOauthGuard) - @Redirect(process.env.FRONT_URL + '/Home') + @Redirect(process.env.FRONT_URL ? process.env.FRONT_URL + '/Home' : '/') //WARNING: login42Return() { return; } diff --git a/backend/code/src/auth/auth.module.ts b/backend/code/src/auth/auth.module.ts index 411b8a3..cd060d7 100644 --- a/backend/code/src/auth/auth.module.ts +++ b/backend/code/src/auth/auth.module.ts @@ -7,13 +7,21 @@ import { FtStrategy } from './stratgies/ft.strategy'; import { JwtUtilsModule } from './utils/jwt_utils/jwt_utils.module'; import { UsersService } from 'src/users/users.service'; import { JwtConsts } from './constants/constants'; +import { CloudinaryService } from 'src/cloudinary/cloudinary.service'; @Module({ imports: [ JwtModule.register({ secret: JwtConsts.at_secret }), JwtUtilsModule, ], - providers: [AuthService, AtStrategy, RtStrategy, FtStrategy, UsersService], + providers: [ + AuthService, + AtStrategy, + RtStrategy, + FtStrategy, + UsersService, + CloudinaryService, + ], controllers: [AuthController], }) export class AuthModule {} diff --git a/backend/code/src/auth/stratgies/ft.strategy.ts b/backend/code/src/auth/stratgies/ft.strategy.ts index 3858a7c..04e5bea 100644 --- a/backend/code/src/auth/stratgies/ft.strategy.ts +++ b/backend/code/src/auth/stratgies/ft.strategy.ts @@ -3,12 +3,14 @@ import { PassportStrategy } from '@nestjs/passport'; import { Strategy, Profile, VerifyCallback } from 'passport-42'; import { JwtUtils } from '../utils/jwt_utils/jwt_utils'; import { UsersService } from 'src/users/users.service'; +import { CloudinaryService } from 'src/cloudinary/cloudinary.service'; @Injectable() export class FtStrategy extends PassportStrategy(Strategy, '42') { constructor( private jwtUtils: JwtUtils, private usersService: UsersService, + private cloudinaryService: CloudinaryService, ) { super({ clientID: process.env.FT_CLIENT_ID, @@ -46,16 +48,29 @@ export class FtStrategy extends PassportStrategy(Strategy, '42') { email: profile._json.email, firstName: profile.name.givenName, lastName: profile.name.familyName, + Username: profile.username, + }); + + const avatarturl = `https://ui-avatars.com/api/?name=${new_user.firstName}-${new_user.lastName}&background=7940CF&color=fff`; + const result = await this.cloudinaryService.upload( + new_user.userId, + avatarturl, + ); + + await this.usersService.updateUser(new_user.userId, { + avatar: `v${result.version}/${result.public_id}.${result.format}`, }); const tokens = await this.jwtUtils.generateTokens( new_user.email, new_user.userId, ); + await this.jwtUtils.updateRefreshedHash( new_user.userId, tokens.refresh_token, ); + res.cookie('X-Access-Token', tokens.access_token, { httpOnly: true }); res.cookie('X-Refresh-Token', tokens.refresh_token, { httpOnly: true }); return cb(null, profile); diff --git a/backend/code/src/cloudinary/cloudinary.service.ts b/backend/code/src/cloudinary/cloudinary.service.ts index cb4b5a8..51a9bee 100644 --- a/backend/code/src/cloudinary/cloudinary.service.ts +++ b/backend/code/src/cloudinary/cloudinary.service.ts @@ -5,11 +5,14 @@ import { v2 as cloudinary } from 'cloudinary'; export class CloudinaryService { constructor() {} - async upload(file: any) { - return await cloudinary.uploader.upload(file.path, { - folder: 'tran-avatar', + async upload(userId: string, url: string) { + return await cloudinary.uploader.upload(url, { + folder: 'nest-blog', overwrite: true, - resource_type: 'auto', + resource_type: 'image', + unique_filename: false, + filename_override: userId, + use_filename: true, }); } } diff --git a/backend/code/src/friends/friends.service.ts b/backend/code/src/friends/friends.service.ts index 384be90..567979a 100644 --- a/backend/code/src/friends/friends.service.ts +++ b/backend/code/src/friends/friends.service.ts @@ -107,18 +107,48 @@ export class FriendsService { 'You cannot block yourself', HttpStatus.FORBIDDEN, ); + + const commonRoom = await this.prisma.room.findFirst({ + where: { + AND: [ + { + type: 'dm', + }, + { + members: { + some: { + userId: userId, + }, + }, + }, + { + members: { + some: { + userId: friendId, + }, + }, + }, + ], + }, + select: { + id: true, + }, + }); + const friendshipId = [userId, friendId].sort().join('-'); await this.prisma.friend.deleteMany({ where: { id: friendshipId, }, }); + await this.prisma.blockedUsers.upsert({ where: { id: friendshipId, }, create: { id: friendshipId, + ...(commonRoom && { dmRoomId: commonRoom.id }), Blcoked_by: { connect: { userId, diff --git a/backend/code/src/game/game.controller.ts b/backend/code/src/game/game.controller.ts new file mode 100644 index 0000000..55c8fb4 --- /dev/null +++ b/backend/code/src/game/game.controller.ts @@ -0,0 +1,12 @@ +import { Controller, Post } from '@nestjs/common'; +import { GameService } from './game.service'; + +@Controller('game') +export class GameController { + constructor(private readonly gameService: GameService) {} + + @Post('start') + startGame() { + // return this.gameService.startGame(); + } +} diff --git a/backend/code/src/game/game.module.ts b/backend/code/src/game/game.module.ts new file mode 100644 index 0000000..d375e00 --- /dev/null +++ b/backend/code/src/game/game.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { GameService } from './game.service'; +import { GameController } from './game.controller'; + +@Module({ + controllers: [GameController], + providers: [GameService], +}) +export class GameModule {} diff --git a/backend/code/src/game/game.service.ts b/backend/code/src/game/game.service.ts new file mode 100644 index 0000000..9fb6661 --- /dev/null +++ b/backend/code/src/game/game.service.ts @@ -0,0 +1,28 @@ +import { Injectable } from '@nestjs/common'; +import { OnEvent } from '@nestjs/event-emitter'; + +@Injectable() +export class GameService { + constructor() { + // this.launchGame(); + } + + private waitingPlayers: string[] = []; + + @OnEvent('game.start') + handleGameStartEvent(client: any) { + this.waitingPlayers.push(client.id); + console.log('client subscribed to the queue'); + } + + private launchGame() { + setInterval(() => { + console.log('waitingPlayers'); + if (this.waitingPlayers.length >= 2) { + console.log('Game launched!'); + const two_players = this.waitingPlayers.splice(0, 2); + console.log(two_players); + } + }, 1000); + } +} diff --git a/backend/code/src/gateways/gateways.gateway.ts b/backend/code/src/gateways/gateways.gateway.ts index 16154f4..de3e20e 100644 --- a/backend/code/src/gateways/gateways.gateway.ts +++ b/backend/code/src/gateways/gateways.gateway.ts @@ -1,12 +1,13 @@ import { OnGatewayConnection, + SubscribeMessage, WebSocketGateway, WebSocketServer, } from '@nestjs/websockets'; import { Server, Socket } from 'socket.io'; import { MessageFormatDto } from 'src/messages/dto/message-format.dto'; import {} from '@nestjs/platform-socket.io'; -import { OnEvent } from '@nestjs/event-emitter'; +import { EventEmitter2, OnEvent } from '@nestjs/event-emitter'; import { PrismaService } from 'src/prisma/prisma.service'; @WebSocketGateway(3004, { cors: { @@ -15,7 +16,10 @@ import { PrismaService } from 'src/prisma/prisma.service'; transports: ['websocket'], }) export class Gateways implements OnGatewayConnection { - constructor(private prisma: PrismaService) {} + constructor( + private prisma: PrismaService, + private readonly eventEmitter: EventEmitter2, + ) {} handleConnection(client: Socket) { const userId = client.data.user.sub; const rooms = this.prisma.roomMember.findMany({ @@ -46,9 +50,29 @@ export class Gateways implements OnGatewayConnection { const chanellname: string = `Romm:${message.roomId}`; this.server.to(chanellname).emit('message', message); } + @OnEvent('addFriendNotif') sendFriendReq(notif: any) { const channellname: string = `notif:${notif.recipientId}`; this.server.to(channellname).emit('message', notif); } + + @SubscribeMessage('startGame') + handleGameStartEvent(client: any) { + this.eventEmitter.emit('game.start', client); + } + + @SubscribeMessage('movePaddle') + handleMovePaddleEvent(client: any, data: any) { + this.server.to(data.channel).emit('movePaddle', data); + } + + @OnEvent('game.launched') + handleGameLaunchedEvent(clients: any) { + const game_channel = `Game:${clients[0].id}:${clients[1].id}`; + clients.forEach((client: any) => { + client.join(game_channel); + }); + this.server.to(game_channel).emit('game.launched', game_channel); + } } diff --git a/backend/code/src/main.ts b/backend/code/src/main.ts index 76fcfef..44be4ac 100644 --- a/backend/code/src/main.ts +++ b/backend/code/src/main.ts @@ -41,9 +41,10 @@ async function bootstrap() { .setDescription('The Transcendence API description') .setVersion('1.0') .addTag('Auth') - .addTag('friends') .addTag('profile') + .addTag('friends') .addTag('rooms') + .addTag('Messages') .build(); const document = SwaggerModule.createDocument(app, options); SwaggerModule.setup('api', app, document); diff --git a/backend/code/src/messages/dto/message-format.dto.ts b/backend/code/src/messages/dto/message-format.dto.ts index 0e5555c..dd30a55 100644 --- a/backend/code/src/messages/dto/message-format.dto.ts +++ b/backend/code/src/messages/dto/message-format.dto.ts @@ -1,3 +1,4 @@ +import { ApiProperty } from '@nestjs/swagger'; import { Message } from '@prisma/client'; export class MessageFormatDto { @@ -8,9 +9,15 @@ export class MessageFormatDto { this.roomId = messageData.roomId; this.authorId = messageData.authorId; } + + @ApiProperty({ example: 'clnx16e7a00003b6moh6yipir' }) id: string; + @ApiProperty({ example: 'Hello World' }) content: string; + @ApiProperty({ example: '2021-08-16T14:00:00.000Z' }) time: Date; + @ApiProperty({ example: 'clnx17wal00003b6leivni4oe' }) roomId: string; + @ApiProperty({ example: 'clnx18i8x00003b6lrp84ufb3' }) authorId: string; } diff --git a/backend/code/src/messages/messages.controller.ts b/backend/code/src/messages/messages.controller.ts index b0abaa4..a0e7038 100644 --- a/backend/code/src/messages/messages.controller.ts +++ b/backend/code/src/messages/messages.controller.ts @@ -1,13 +1,32 @@ -import { Body, Controller, Param, Post, UseGuards } from '@nestjs/common'; +import { + Body, + Controller, + Get, + Param, + Post, + Query, + UseGuards, +} from '@nestjs/common'; import { MessagesService } from './messages.service'; import { CreateMessgaeDto } from './dto/create-messgae.dto'; import { GetCurrentUser } from 'src/auth/decorator/get_current_user.decorator'; import { AtGuard } from 'src/auth/guards/at.guard'; +import { ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { MessageFormatDto } from './dto/message-format.dto'; +import { QueryOffsetDto } from 'src/friends/dto/query-ofsset-dto'; +@ApiTags('Messages') @Controller('messages') export class MessagesController { constructor(private readonly messagesService: MessagesService) {} - @Post('send/:id') + + @ApiResponse({ type: MessageFormatDto }) + @ApiParam({ + description: 'roomId', + name: 'id', + example: 'clnx17wal00003b6leivni4oe', + }) + @Post('room/:id') @UseGuards(AtGuard) sendMessages( @Param('id') channelId: string, @@ -16,4 +35,14 @@ export class MessagesController { ) { return this.messagesService.sendMessages(userId, channelId, messageDto); } + + @Get('room/:id') + @UseGuards(AtGuard) + getMessages( + @Param('id') channelId: string, + @Query() { offset, limit }: QueryOffsetDto, + @GetCurrentUser('userId') userId: string, + ) { + return this.messagesService.getMessages(userId, channelId, offset, limit); + } } diff --git a/backend/code/src/messages/messages.service.ts b/backend/code/src/messages/messages.service.ts index 9c20c87..1b77c86 100644 --- a/backend/code/src/messages/messages.service.ts +++ b/backend/code/src/messages/messages.service.ts @@ -15,17 +15,36 @@ export class MessagesService { throw new HttpException('Message is too long', HttpStatus.BAD_REQUEST); } - //TODO: check user is not banned - // check user is in channel - // check user is not muted - //FIXME: owner room memebr - const roomMember = await this.prisma.roomMember.findFirst({ - where: { - userId, - roomId: channelId, + const room = await this.prisma.room.findUnique({ + where: { id: channelId }, + select: { + ownerId: true, + type: true, + members: { + where: { + userId: userId, + }, + }, }, }); + if (room.type === 'dm') { + const blocked = await this.prisma.blockedUsers.findFirst({ + where: { + dmRoomId: channelId, + }, + }); + if (blocked) { + throw new HttpException( + 'You are blocked from this dm', + HttpStatus.UNAUTHORIZED, + ); + } + } + + const roomMember = room.members[0]; + console.log(roomMember); + if (!roomMember) { throw new HttpException( 'User is not in channel', @@ -33,6 +52,13 @@ export class MessagesService { ); } + if (roomMember.is_banned) { + throw new HttpException( + 'you are banned from this channel', + HttpStatus.UNAUTHORIZED, + ); + } + if (roomMember.is_mueted) { const now = new Date(); if (now < roomMember.mute_expires) { @@ -60,4 +86,41 @@ export class MessagesService { this.eventEmitter.emit('sendMessages', responseMessage); return responseMessage; } + + async getMessages( + userId: string, + channelId: string, + offset: number, + limit: number, + ) { + const roomMember = await this.prisma.roomMember.findFirst({ + where: { + userId, + roomId: channelId, + }, + }); + + if (!roomMember) { + throw new HttpException( + 'User is not in channel', + HttpStatus.UNAUTHORIZED, + ); + } + //TESTING: for later testing + const messages = await this.prisma.message.findMany({ + where: { + roomId: channelId, + ...(roomMember.is_banned && { + createdAt: { lte: roomMember.bannedAt }, + }), + }, + orderBy: { + createdAt: 'desc', + }, + skip: offset, + take: limit, + }); + + return messages.map((message) => new MessageFormatDto(message)); + } } diff --git a/backend/code/src/profile/dto/profile.dto.ts b/backend/code/src/profile/dto/profile.dto.ts index 956d17f..df8a02b 100644 --- a/backend/code/src/profile/dto/profile.dto.ts +++ b/backend/code/src/profile/dto/profile.dto.ts @@ -8,23 +8,6 @@ type ProfileDtoProps = Partial & roomMember: RoomMember[]; owned_rooms: Room[]; }>; -/* - isLogged: boolean, - id:string, - bio:string, - phone:string, - name:{ - first:string, - last:string - }, - picture:{ - thumbnail:string, - medium:string, - large:string - }, - email:string, - tfa:boolean, -*/ export type NAME = { first: string; @@ -54,6 +37,7 @@ export class ProfileDto { medium: `https://res.cloudinary.com/trandandan/image/upload/c_thumb,h_72,w_72/${userData.avatar}`, large: `https://res.cloudinary.com/trandandan/image/upload/c_thumb,h_128,w_128/${userData.avatar}`, }; + this.username = userData.Username; } @ApiProperty({ example: 'cln8xxhut0000stofeef' }) @@ -81,4 +65,7 @@ export class ProfileDto { picture: PICTURE; @ApiProperty({ example: 'example@mail.com' }) email: string; + + @ApiProperty({ example: 'dexter' }) + username: string; } diff --git a/backend/code/src/profile/dto/update-profile.dto.ts b/backend/code/src/profile/dto/update-profile.dto.ts index 425fdbb..fe6276c 100644 --- a/backend/code/src/profile/dto/update-profile.dto.ts +++ b/backend/code/src/profile/dto/update-profile.dto.ts @@ -48,4 +48,10 @@ export class UpdateProfileDto { @IsOptional() @IsBoolean() finishProfile: boolean; + + @ApiProperty({ required: false }) + @IsOptional() + @IsString() + @IsNotEmpty() + Username: string; } diff --git a/backend/code/src/profile/profile.controller.ts b/backend/code/src/profile/profile.controller.ts index 33e21d4..27ee69c 100644 --- a/backend/code/src/profile/profile.controller.ts +++ b/backend/code/src/profile/profile.controller.ts @@ -36,6 +36,7 @@ export class ProfileController { @ApiOkResponse({ type: ProfileDto }) @UseGuards(AtGuard) async getMe(@GetCurrentUser('userId') userId: string): Promise { + console.log(userId); return await this.profileService.getProfile(userId); } diff --git a/backend/code/src/rooms/dto/create-room.dto.ts b/backend/code/src/rooms/dto/create-room.dto.ts index f8dcd4e..af780ce 100644 --- a/backend/code/src/rooms/dto/create-room.dto.ts +++ b/backend/code/src/rooms/dto/create-room.dto.ts @@ -26,4 +26,9 @@ export class CreateRoomDto { @IsNotEmpty() @Length(8, 32) password?: string; + + @IsNotEmpty() + @IsString() + @IsOptional() + secondMember: string; } diff --git a/backend/code/src/rooms/dto/update-room.dto.ts b/backend/code/src/rooms/dto/update-room.dto.ts index 98e1476..e6ce19c 100644 --- a/backend/code/src/rooms/dto/update-room.dto.ts +++ b/backend/code/src/rooms/dto/update-room.dto.ts @@ -13,8 +13,11 @@ export class UpdateRoomDto extends PartialType(CreateRoomDto) { @IsOptional() type: RoomType; - @ApiProperty({ required: false }) + @ApiProperty() @IsString() @IsNotEmpty() roomId: string; + + @ApiProperty({ required: false }) + password: string; } diff --git a/backend/code/src/rooms/rooms.service.ts b/backend/code/src/rooms/rooms.service.ts index b60ea45..80f8f8f 100644 --- a/backend/code/src/rooms/rooms.service.ts +++ b/backend/code/src/rooms/rooms.service.ts @@ -28,6 +28,27 @@ export class RoomsService { } else if (roomData.type == 'protected' && roomData.password) { roomData.password = await bcrypt.hash(roomData.password, 10); } + if (roomData.type === 'dm' && !('secondMember' in roomData)) { + throw new HttpException('something went wrong', HttpStatus.BAD_REQUEST); + } + + const secondMember: string | undefined = roomData.secondMember; + delete roomData.secondMember; + + if (roomData.type === 'dm') { + const friendshipId = [roomOwnerId, secondMember].sort().join('-'); + const blocked = await this.prisma.blockedUsers.findUnique({ + where: { + id: friendshipId, + }, + }); + if (blocked) { + throw new HttpException( + 'an error occured while creating the dm room', + HttpStatus.FORBIDDEN, + ); + } + } const room = await this.prisma.room.create({ data: { @@ -37,6 +58,7 @@ export class RoomsService { }, }, }); + await this.prisma.roomMember.create({ data: { user: { @@ -48,6 +70,21 @@ export class RoomsService { is_admin: true, }, }); + + if (roomData.type === 'dm') { + await this.prisma.roomMember.create({ + data: { + user: { + connect: { userId: secondMember }, + }, + room: { + connect: { id: room.id }, + }, + is_admin: true, + }, + }); + } + return new RoomDataDto(room); } @@ -116,24 +153,36 @@ export class RoomsService { delete roomData.roomId; const room = await this.prisma.room.findUnique({ where: { id: roomId }, - select: { ownerId: true }, + select: { ownerId: true, type: true }, }); + + if (room.type == 'dm') + throw new HttpException( + 'dm room can not be updated', + HttpStatus.BAD_REQUEST, + ); + if (!room) throw new HttpException('room not found', HttpStatus.NOT_FOUND); + if (room.ownerId !== userId) { throw new UnauthorizedException('you are not the owner of this room'); } + if (roomData.type == 'protected' && !roomData.password) { throw new HttpException( 'missing password for protected room', HttpStatus.BAD_REQUEST, ); } + if (roomData.type == 'protected' && roomData.password) { roomData.password = await bcrypt.hash(roomData.password, 10); } + if (roomData.type == 'public' || roomData.type == 'private') { roomData.password = null; } + const room_updated = await this.prisma.room.update({ where: { id: roomId }, data: roomData, @@ -430,8 +479,26 @@ export class RoomsService { id: true, name: true, type: true, + ownerId: true, + members: { + where: { + userId: userId, + }, + select: { + is_admin: true, + }, + }, }, }); - return rooms; + return rooms.map((room) => { + const is_owner = room.ownerId === userId; + return { + id: room.id, + name: room.name, + type: room.type, + is_admin: room.members[0].is_admin, + is_owner, + }; + }); } } diff --git a/backend/code/src/users/dto/create-user.dto.ts b/backend/code/src/users/dto/create-user.dto.ts index 4bd709f..e364a3a 100644 --- a/backend/code/src/users/dto/create-user.dto.ts +++ b/backend/code/src/users/dto/create-user.dto.ts @@ -50,4 +50,6 @@ export class CreateUserDto { @IsString() @IsNotEmpty() tfaEnabled?: boolean; + + Username?: string; }