diff --git a/backend/code/prisma/dbml/schema.dbml b/backend/code/prisma/dbml/schema.dbml index 1edca56..58f63be 100644 --- a/backend/code/prisma/dbml/schema.dbml +++ b/backend/code/prisma/dbml/schema.dbml @@ -27,6 +27,7 @@ Table users { roomMember room_members [not null] blocked_by blocked_friends [not null] blocked blocked_friends [not null] + Message messages [not null] } Table friends { @@ -75,6 +76,7 @@ Table messages { id String [pk] authorId String [not null] room rooms [not null] + author users [not null] roomId String [not null] content String } @@ -145,6 +147,8 @@ Ref: matches.participant2Id > users.userId Ref: messages.roomId > rooms.id [delete: Cascade] +Ref: messages.authorId > users.userId + Ref: rooms.ownerId > users.userId Ref: room_members.userId > users.userId diff --git a/backend/code/prisma/migrations/20231030183113_/migration.sql b/backend/code/prisma/migrations/20231030183113_/migration.sql new file mode 100644 index 0000000..fdfd67a --- /dev/null +++ b/backend/code/prisma/migrations/20231030183113_/migration.sql @@ -0,0 +1,52 @@ +/* + Warnings: + + - A unique constraint covering the columns `[createdAt]` on the table `messages` will be added. If there are existing duplicate values, this will fail. + - A unique constraint covering the columns `[Username]` on the table `users` will be added. If there are existing duplicate values, this will fail. + +*/ +-- CreateEnum +CREATE TYPE "NotifType" AS ENUM ('addFriend'); + +-- DropForeignKey +ALTER TABLE "messages" DROP CONSTRAINT "messages_roomId_fkey"; + +-- AlterTable +ALTER TABLE "blocked_friends" ADD COLUMN "dmRoomId" TEXT; + +-- AlterTable +ALTER TABLE "room_members" ADD COLUMN "bannedAt" TIMESTAMP(3); + +-- AlterTable +ALTER TABLE "users" ADD COLUMN "tfaSecret" TEXT, +ADD COLUMN "tfaStatus" BOOLEAN NOT NULL DEFAULT false; + +-- CreateTable +CREATE TABLE "notifications" ( + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "id" TEXT NOT NULL, + "recipientId" TEXT NOT NULL, + "content" "NotifType" NOT NULL, + "is_read" BOOLEAN NOT NULL DEFAULT false, + "readAt" TIMESTAMP(3), + + CONSTRAINT "notifications_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "notifications_createdAt_key" ON "notifications"("createdAt"); + +-- CreateIndex +CREATE UNIQUE INDEX "messages_createdAt_key" ON "messages"("createdAt"); + +-- CreateIndex +CREATE UNIQUE INDEX "users_Username_key" ON "users"("Username"); + +-- AddForeignKey +ALTER TABLE "matches" ADD CONSTRAINT "matches_participant2Id_fkey" FOREIGN KEY ("participant2Id") REFERENCES "users"("userId") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "messages" ADD CONSTRAINT "messages_roomId_fkey" FOREIGN KEY ("roomId") REFERENCES "rooms"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "messages" ADD CONSTRAINT "messages_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "users"("userId") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/backend/code/prisma/schema.prisma b/backend/code/prisma/schema.prisma index f88ae47..ceaf261 100644 --- a/backend/code/prisma/schema.prisma +++ b/backend/code/prisma/schema.prisma @@ -25,10 +25,10 @@ model User { intraId String? @unique profileFinished Boolean @default(false) - Username String? @unique + Username String? @unique firstName String? lastName String? - discreption String @default("") + discreption String @default("") avatar String? tfaEnabled Boolean @default(false) tfaSecret String? @@ -41,6 +41,7 @@ model User { roomMember RoomMember[] blocked_by BlockedUsers[] @relation("blocked_by") blocked BlockedUsers[] @relation("blocked") + Message Message[] @@map("users") } @@ -64,12 +65,12 @@ model BlockedUsers { updatedAt DateTime @updatedAt createdAt DateTime @default(now()) - id String @id - Blcoked_by User @relation("blocked_by", fields: [blocked_by_id], references: [userId]) - blocked_by_id String @unique - Blocked User @relation("blocked", fields: [blocked_id], references: [userId]) - blocked_id String @unique - dmRoomId String? + id String @id + Blcoked_by User @relation("blocked_by", fields: [blocked_by_id], references: [userId]) + blocked_by_id String @unique + Blocked User @relation("blocked", fields: [blocked_id], references: [userId]) + blocked_id String @unique + dmRoomId String? @@map("blocked_friends") } @@ -98,12 +99,12 @@ model Message { id String @id @default(cuid()) authorId String room Room @relation(fields: [roomId], references: [id], onDelete: Cascade) + author User @relation(fields: [authorId], references: [userId]) roomId String content String? - - @@map("messages") @@unique([createdAt]) + @@map("messages") } model Room { @@ -142,16 +143,16 @@ model RoomMember { } model Notification { - createdAt DateTime @default(now()) + createdAt DateTime @default(now()) - id String @id @default(cuid()) + id String @id @default(cuid()) recipientId String content NotifType - is_read Boolean @default(false) + is_read Boolean @default(false) readAt DateTime? - @@map("notifications") @@unique([createdAt]) + @@map("notifications") } enum NotifType { diff --git a/backend/code/src/friends/friends.service.ts b/backend/code/src/friends/friends.service.ts index 567979a..c591434 100644 --- a/backend/code/src/friends/friends.service.ts +++ b/backend/code/src/friends/friends.service.ts @@ -3,6 +3,7 @@ import { PrismaService } from 'src/prisma/prisma.service'; import { UsersService } from 'src/users/users.service'; import { FriendResponseDto } from './dto/frined-response.dto'; import { EventEmitter2 } from '@nestjs/event-emitter'; +import { PICTURE } from 'src/profile/dto/profile.dto'; @Injectable() export class FriendsService { @@ -208,6 +209,7 @@ export class FriendsService { userId: true, firstName: true, lastName: true, + avatar: true, }, }, to: { @@ -215,15 +217,37 @@ export class FriendsService { userId: true, firstName: true, lastName: true, + avatar: true, }, }, }, }); - return friends.map((friend) => { + + return friends.map((friend: any) => { if (friend.from.userId === userId) { - return friend.to; + friend = friend.to as any; + const avatar: PICTURE = { + thumbnail: `https://res.cloudinary.com/trandandan/image/upload/c_thumb,h_48,w_48/${friend.avatar}`, + medium: `https://res.cloudinary.com/trandandan/image/upload/c_thumb,h_72,w_72/${friend.avatar}`, + large: `https://res.cloudinary.com/trandandan/image/upload/c_thumb,h_128,w_128/${friend.avatar}`, + }; + delete friend.avatar; + return { + ...friend.to, + avatar, + }; } else { - return friend.from; + friend = friend.from as any; + const avatar: PICTURE = { + thumbnail: `https://res.cloudinary.com/trandandan/image/upload/c_thumb,h_48,w_48/${friend.avatar}`, + medium: `https://res.cloudinary.com/trandandan/image/upload/c_thumb,h_72,w_72/${friend.avatar}`, + large: `https://res.cloudinary.com/trandandan/image/upload/c_thumb,h_128,w_128/${friend.avatar}`, + }; + delete friend.avatar; + return { + ...friend.from, + avatar, + }; } }); } diff --git a/backend/code/src/gateways/gateways.gateway.ts b/backend/code/src/gateways/gateways.gateway.ts index de3e20e..ad2fee1 100644 --- a/backend/code/src/gateways/gateways.gateway.ts +++ b/backend/code/src/gateways/gateways.gateway.ts @@ -58,15 +58,10 @@ export class Gateways implements OnGatewayConnection { } @SubscribeMessage('startGame') - handleGameStartEvent(client: any) { + handleGameStartEvent(client: Socket) { 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}`; diff --git a/backend/code/src/messages/dto/message-format.dto.ts b/backend/code/src/messages/dto/message-format.dto.ts index dd30a55..be954fd 100644 --- a/backend/code/src/messages/dto/message-format.dto.ts +++ b/backend/code/src/messages/dto/message-format.dto.ts @@ -1,13 +1,19 @@ import { ApiProperty } from '@nestjs/swagger'; -import { Message } from '@prisma/client'; +import { Message, User } from '@prisma/client'; +import { PICTURE } from 'src/profile/dto/profile.dto'; export class MessageFormatDto { - constructor(messageData: Message) { + constructor(messageData: Message & { author: Partial }) { this.id = messageData.id; this.content = messageData.content; this.time = messageData.createdAt; this.roomId = messageData.roomId; this.authorId = messageData.authorId; + this.avatar = { + thumbnail: `https://res.cloudinary.com/trandandan/image/upload/c_thumb,h_48,w_48/${messageData.author.avatar}`, + medium: `https://res.cloudinary.com/trandandan/image/upload/c_thumb,h_72,w_72/${messageData.author.avatar}`, + large: `https://res.cloudinary.com/trandandan/image/upload/c_thumb,h_128,w_128/${messageData.author.avatar}`, + }; } @ApiProperty({ example: 'clnx16e7a00003b6moh6yipir' }) @@ -20,4 +26,7 @@ export class MessageFormatDto { roomId: string; @ApiProperty({ example: 'clnx18i8x00003b6lrp84ufb3' }) authorId: string; + + @ApiProperty({ example: 'clnx18i8x00003b6lrp84ufb3' }) + avatar: PICTURE; } diff --git a/backend/code/src/messages/messages.controller.ts b/backend/code/src/messages/messages.controller.ts index a0e7038..2a295b7 100644 --- a/backend/code/src/messages/messages.controller.ts +++ b/backend/code/src/messages/messages.controller.ts @@ -36,6 +36,7 @@ export class MessagesController { return this.messagesService.sendMessages(userId, channelId, messageDto); } + @ApiResponse({ type: MessageFormatDto, isArray: true }) @Get('room/:id') @UseGuards(AtGuard) getMessages( diff --git a/backend/code/src/messages/messages.service.ts b/backend/code/src/messages/messages.service.ts index b085358..682f041 100644 --- a/backend/code/src/messages/messages.service.ts +++ b/backend/code/src/messages/messages.service.ts @@ -80,6 +80,13 @@ export class MessagesService { roomId: channelId, authorId: userId, }, + include: { + author: { + select: { + avatar: true, + }, + }, + }, }); const responseMessage: MessageFormatDto = new MessageFormatDto(messageData); @@ -117,6 +124,13 @@ export class MessagesService { orderBy: { createdAt: 'desc', }, + include: { + author: { + select: { + avatar: true, + }, + }, + }, skip: offset, take: limit, }); diff --git a/backend/code/src/rooms/dto/create-room.dto.ts b/backend/code/src/rooms/dto/create-room.dto.ts index af780ce..906524f 100644 --- a/backend/code/src/rooms/dto/create-room.dto.ts +++ b/backend/code/src/rooms/dto/create-room.dto.ts @@ -6,12 +6,15 @@ import { IsOptional, IsString, Length, + ValidateIf, } from 'class-validator'; export class CreateRoomDto { - @ApiProperty() + // @IgnoreName('type', { message: 'room name is not required on type dm' }) + @ApiProperty({ description: 'name of the room required if type is not dm' }) @IsString() @IsNotEmpty() + @ValidateIf((o) => o.type !== 'dm') name: string; @ApiProperty() @@ -27,8 +30,9 @@ export class CreateRoomDto { @Length(8, 32) password?: string; + @ApiProperty({ description: 'second member of the room' }) + @ValidateIf((o) => o.type === 'dm') @IsNotEmpty() @IsString() - @IsOptional() secondMember: string; } diff --git a/backend/code/src/rooms/rooms.service.ts b/backend/code/src/rooms/rooms.service.ts index 0d4e0cf..52b5e12 100644 --- a/backend/code/src/rooms/rooms.service.ts +++ b/backend/code/src/rooms/rooms.service.ts @@ -532,7 +532,10 @@ export class RoomsService { skip: offset, take: limit, where: { - ...(joined && { members: { some: { userId: userId } } }), + ...(joined && { + members: { some: { userId: userId } }, + NOT: { type: 'dm' }, + }), ...(!joined && { OR: [{ type: 'public' }, { type: 'protected' }] }), }, select: { @@ -553,15 +556,51 @@ export class RoomsService { }, }); if (!joined) 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, - }; - }); + + type roomsData = { + is_admin: boolean; + id: string; + name: string; + type: string; + is_owner: boolean; + countMembers: number; + last_message: { + createdAt: Date; + content: string; + } | null; + }; + const roomsData: roomsData[] = await Promise.all( + rooms.map(async (room) => { + const countMembers = await this.prisma.roomMember.count({ + where: { + roomId: room.id, + }, + }); + + const last_message = await this.prisma.message.findFirst({ + where: { + roomId: room.id, + }, + orderBy: { + createdAt: 'desc', + }, + select: { + content: true, + createdAt: true, + }, + }); + 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, + countMembers, + last_message, + }; + }), + ); + return roomsData; } }