From ac08a5c4f4c80195a284159d5dbb0a2cc73ec40a Mon Sep 17 00:00:00 2001 From: nicolas-arnaud Date: Tue, 28 Feb 2023 09:45:38 +0100 Subject: [PATCH] avatars --- .gitignore | 1 - back/volume/.gitignore | 2 + back/volume/package-lock.json | 130 ++++++++++++++++++ back/volume/package.json | 3 + back/volume/src/api/api.controller.ts | 1 - back/volume/src/api/api.module.ts | 5 +- back/volume/src/app.module.ts | 6 +- back/volume/src/auth/42.strategy.ts | 10 +- .../src/auth/requestWithUser.interface.ts | 8 ++ back/volume/src/chat/chat.gateway.ts | 111 ++++++++------- back/volume/src/chat/chat.module.ts | 22 +-- back/volume/src/chat/chat.service.ts | 74 +++++----- back/volume/src/chat/model/channel.entity.ts | 38 ++--- .../src/chat/model/create-channel.dto.ts | 8 +- back/volume/src/chat/model/message.entity.ts | 20 +-- .../src/chat/model/update-channel.dto.ts | 24 ++-- back/volume/src/db/db.module.ts | 5 +- back/volume/src/types.d.ts | 6 +- back/volume/src/users/user.dto.ts | 17 +-- back/volume/src/users/user.entity.ts | 47 ++++--- back/volume/src/users/users.controller.ts | 66 ++++++++- back/volume/src/users/users.service.ts | 50 ++++--- 22 files changed, 443 insertions(+), 211 deletions(-) create mode 100644 back/volume/src/auth/requestWithUser.interface.ts diff --git a/.gitignore b/.gitignore index f33a766..4c49bd7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1 @@ .env -*/volume/.env diff --git a/back/volume/.gitignore b/back/volume/.gitignore index 047e6a4..324572d 100644 --- a/back/volume/.gitignore +++ b/back/volume/.gitignore @@ -1,3 +1,5 @@ +avatars/ +.env # Logs logs diff --git a/back/volume/package-lock.json b/back/volume/package-lock.json index 6f8b825..875da92 100644 --- a/back/volume/package-lock.json +++ b/back/volume/package-lock.json @@ -16,8 +16,10 @@ "@nestjs/mapped-types": "^1.2.2", "@nestjs/passport": "^9.0.3", "@nestjs/platform-express": "^9.0.0", + "@nestjs/platform-socket.io": "^9.3.9", "@nestjs/platform-ws": "^9.2.0", "@nestjs/schematics": "^9.0.0", + "@nestjs/swagger": "^6.2.1", "@nestjs/testing": "^9.0.0", "@nestjs/typeorm": "^9.0.1", "@nestjs/websockets": "^9.2.0", @@ -25,6 +27,7 @@ "@types/cookie-parser": "^1.4.3", "@types/express": "^4.17.13", "@types/express-session": "^1.17.6", + "@types/multer": "^1.4.7", "@types/node": "^16.0.0", "@types/passport": "^1.0.12", "@types/ws": "^8.5.3", @@ -1731,6 +1734,40 @@ "@nestjs/core": "^9.0.0" } }, + "node_modules/@nestjs/platform-socket.io": { + "version": "9.3.9", + "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-9.3.9.tgz", + "integrity": "sha512-fWlET24udsVjIolSjrIIj8vGqixnTXrQnrEKF1nqFpE8BW9O8Eji00Ih/A2z0MU/8fTHEiokyBIDAX5IKvhKzQ==", + "dependencies": { + "socket.io": "4.6.0", + "tslib": "2.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^9.0.0", + "@nestjs/websockets": "^9.0.0", + "rxjs": "^7.1.0" + } + }, + "node_modules/@nestjs/platform-socket.io/node_modules/socket.io": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.6.0.tgz", + "integrity": "sha512-b65bp6INPk/BMMrIgVvX12x3Q+NqlGqSlTuvKQWt0BUJ3Hyy3JangBl7fEoWZTXbOKlCqNPbQ6MbWgok/km28w==", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "debug": "~4.3.2", + "engine.io": "~6.4.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/@nestjs/platform-ws": { "version": "9.3.9", "resolved": "https://registry.npmjs.org/@nestjs/platform-ws/-/platform-ws-9.3.9.tgz", @@ -1837,6 +1874,37 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, + "node_modules/@nestjs/swagger": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-6.2.1.tgz", + "integrity": "sha512-9M2vkfJHIzLqDZwvM5TEZO0MxRCvIb0xVy0LsmWwxH1lrb0z/4MhU+r2CWDhBtTccVJrKxVPiU2s3T3b9uUJbg==", + "dependencies": { + "@nestjs/mapped-types": "1.2.2", + "js-yaml": "4.1.0", + "lodash": "4.17.21", + "path-to-regexp": "3.2.0", + "swagger-ui-dist": "4.15.5" + }, + "peerDependencies": { + "@fastify/static": "^6.0.0", + "@nestjs/common": "^9.0.0", + "@nestjs/core": "^9.0.0", + "class-transformer": "*", + "class-validator": "*", + "reflect-metadata": "^0.1.12" + }, + "peerDependenciesMeta": { + "@fastify/static": { + "optional": true + }, + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, "node_modules/@nestjs/testing": { "version": "9.3.9", "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-9.3.9.tgz", @@ -2259,6 +2327,14 @@ "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==" }, + "node_modules/@types/multer": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.7.tgz", + "integrity": "sha512-/SNsDidUFCvqqcWDwxv2feww/yqhNeTRL5CVoL3jU4Goc4kKEL10T7Eye65ZqPNi4HRx8sAEX59pV1aEH7drNA==", + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/node": { "version": "16.18.3", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.3.tgz", @@ -9641,6 +9717,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/swagger-ui-dist": { + "version": "4.15.5", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-4.15.5.tgz", + "integrity": "sha512-V3eIa28lwB6gg7/wfNvAbjwJYmDXy1Jo1POjyTzlB6wPcHiGlRxq39TSjYGVjQrUSAzpv+a7nzp7mDxgNy57xA==" + }, "node_modules/symbol-observable": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", @@ -12147,6 +12228,30 @@ "tslib": "2.5.0" } }, + "@nestjs/platform-socket.io": { + "version": "9.3.9", + "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-9.3.9.tgz", + "integrity": "sha512-fWlET24udsVjIolSjrIIj8vGqixnTXrQnrEKF1nqFpE8BW9O8Eji00Ih/A2z0MU/8fTHEiokyBIDAX5IKvhKzQ==", + "requires": { + "socket.io": "4.6.0", + "tslib": "2.5.0" + }, + "dependencies": { + "socket.io": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.6.0.tgz", + "integrity": "sha512-b65bp6INPk/BMMrIgVvX12x3Q+NqlGqSlTuvKQWt0BUJ3Hyy3JangBl7fEoWZTXbOKlCqNPbQ6MbWgok/km28w==", + "requires": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "debug": "~4.3.2", + "engine.io": "~6.4.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.1" + } + } + } + }, "@nestjs/platform-ws": { "version": "9.3.9", "resolved": "https://registry.npmjs.org/@nestjs/platform-ws/-/platform-ws-9.3.9.tgz", @@ -12226,6 +12331,18 @@ } } }, + "@nestjs/swagger": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-6.2.1.tgz", + "integrity": "sha512-9M2vkfJHIzLqDZwvM5TEZO0MxRCvIb0xVy0LsmWwxH1lrb0z/4MhU+r2CWDhBtTccVJrKxVPiU2s3T3b9uUJbg==", + "requires": { + "@nestjs/mapped-types": "1.2.2", + "js-yaml": "4.1.0", + "lodash": "4.17.21", + "path-to-regexp": "3.2.0", + "swagger-ui-dist": "4.15.5" + } + }, "@nestjs/testing": { "version": "9.3.9", "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-9.3.9.tgz", @@ -12590,6 +12707,14 @@ "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==" }, + "@types/multer": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.7.tgz", + "integrity": "sha512-/SNsDidUFCvqqcWDwxv2feww/yqhNeTRL5CVoL3jU4Goc4kKEL10T7Eye65ZqPNi4HRx8sAEX59pV1aEH7drNA==", + "requires": { + "@types/express": "*" + } + }, "@types/node": { "version": "16.18.3", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.3.tgz", @@ -18033,6 +18158,11 @@ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true }, + "swagger-ui-dist": { + "version": "4.15.5", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-4.15.5.tgz", + "integrity": "sha512-V3eIa28lwB6gg7/wfNvAbjwJYmDXy1Jo1POjyTzlB6wPcHiGlRxq39TSjYGVjQrUSAzpv+a7nzp7mDxgNy57xA==" + }, "symbol-observable": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", diff --git a/back/volume/package.json b/back/volume/package.json index 0c4be16..bc15b31 100644 --- a/back/volume/package.json +++ b/back/volume/package.json @@ -28,8 +28,10 @@ "@nestjs/mapped-types": "^1.2.2", "@nestjs/passport": "^9.0.3", "@nestjs/platform-express": "^9.0.0", + "@nestjs/platform-socket.io": "^9.3.9", "@nestjs/platform-ws": "^9.2.0", "@nestjs/schematics": "^9.0.0", + "@nestjs/swagger": "^6.2.1", "@nestjs/testing": "^9.0.0", "@nestjs/typeorm": "^9.0.1", "@nestjs/websockets": "^9.2.0", @@ -37,6 +39,7 @@ "@types/cookie-parser": "^1.4.3", "@types/express": "^4.17.13", "@types/express-session": "^1.17.6", + "@types/multer": "^1.4.7", "@types/node": "^16.0.0", "@types/passport": "^1.0.12", "@types/ws": "^8.5.3", diff --git a/back/volume/src/api/api.controller.ts b/back/volume/src/api/api.controller.ts index 27b39b9..6d71651 100644 --- a/back/volume/src/api/api.controller.ts +++ b/back/volume/src/api/api.controller.ts @@ -25,4 +25,3 @@ export class ApiController { return [1, 2, 3] } } - diff --git a/back/volume/src/api/api.module.ts b/back/volume/src/api/api.module.ts index d1d2a8a..e8e762b 100644 --- a/back/volume/src/api/api.module.ts +++ b/back/volume/src/api/api.module.ts @@ -2,7 +2,6 @@ import { Module } from '@nestjs/common' import { ApiController } from './api.controller' @Module({ - controllers: [ApiController], + controllers: [ApiController] }) - -export class ApiModule { } +export class ApiModule {} diff --git a/back/volume/src/app.module.ts b/back/volume/src/app.module.ts index bec653f..87da858 100644 --- a/back/volume/src/app.module.ts +++ b/back/volume/src/app.module.ts @@ -28,7 +28,7 @@ import { UsersModule } from './users/users.module' ChatModule, DbModule, PongModule, - UsersModule, - ], + UsersModule + ] }) -export class AppModule { } +export class AppModule {} diff --git a/back/volume/src/auth/42.strategy.ts b/back/volume/src/auth/42.strategy.ts index f825941..d4db811 100644 --- a/back/volume/src/auth/42.strategy.ts +++ b/back/volume/src/auth/42.strategy.ts @@ -1,9 +1,11 @@ import { Injectable } from '@nestjs/common' import { ConfigService } from '@nestjs/config' import { PassportStrategy } from '@nestjs/passport' -import { Strategy, Profile, VerifyCallback } from 'passport-42' +import { Strategy, type Profile, type VerifyCallback } from 'passport-42' import { UsersService } from 'src/users/users.service' import { User } from 'src/users/user.entity' +import { get } from 'https' +import { createWriteStream } from 'fs' @Injectable() export class FtStrategy extends PassportStrategy(Strategy, '42') { @@ -34,8 +36,12 @@ export class FtStrategy extends PassportStrategy(Strategy, '42') { const newUser = new User() newUser.id_42 = profile.id as number newUser.username = profile.displayName as string - newUser.avatar = profile._json.image.versions.small as string + newUser.avatar = id_42 + '.jpg' this.usersService.create(newUser) + const file = createWriteStream('avatars/' + id_42 + '.jpg') + get(profile._json.image.versions.small, function (response) { + response.pipe(file) + }) } return cb(null, profile) } diff --git a/back/volume/src/auth/requestWithUser.interface.ts b/back/volume/src/auth/requestWithUser.interface.ts new file mode 100644 index 0000000..8836e11 --- /dev/null +++ b/back/volume/src/auth/requestWithUser.interface.ts @@ -0,0 +1,8 @@ +import { type Request } from 'express' +import type User from 'src/users/user.entity' + +interface RequestWithUser extends Request { + user: User +} + +export default RequestWithUser diff --git a/back/volume/src/chat/chat.gateway.ts b/back/volume/src/chat/chat.gateway.ts index c7f5cb6..39f2ee5 100644 --- a/back/volume/src/chat/chat.gateway.ts +++ b/back/volume/src/chat/chat.gateway.ts @@ -1,17 +1,18 @@ import { - OnGatewayConnection, - OnGatewayDisconnect, + type OnGatewayConnection, + type OnGatewayDisconnect, + MessageBody, SubscribeMessage, WebSocketGateway, - WebSocketServer, -} from '@nestjs/websockets'; -import { Socket, Server } from 'socket.io'; -import { User } from 'src/users/user.entity'; -import { UsersService } from 'src/users/users.service'; -import { UnauthorizedException } from '@nestjs/common'; -import { ChatService } from './chat.service'; -import { Channel } from './model/channel.entity'; -import { Message } from './model/message.entity'; + WebSocketServer +} from '@nestjs/websockets' +import { Socket, Server } from 'socket.io' +import { type User } from 'src/users/user.entity' +import { UsersService } from 'src/users/users.service' +import { UnauthorizedException } from '@nestjs/common' +import { ChatService } from './chat.service' +import { Channel } from './model/channel.entity' +import { Message } from './model/message.entity' import { CreateChannelDto } from './model/create-channel.dto' @@ -20,71 +21,81 @@ import { CreateChannelDto } from './model/create-channel.dto' origin: [ 'http://localhost:5000', 'http://localhost:80', - 'http://localhost:8080', - ], - }, + 'http://localhost:8080' + ] + } }) export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { @WebSocketServer() - server: Server; + server: Server - constructor( - private userService: UsersService, - private chatservice: ChatService, - ) { } + constructor ( + private readonly userService: UsersService, + private readonly chatService: ChatService + ) {} - async handleConnection(socket: Socket) { + async handleConnection (socket: Socket) { try { - const user: User = await this.userService.findOne(socket.data.user.id); + const user: User = await this.userService.findOne(socket.data.user.id) if (!user) { - socket.emit('Error', new UnauthorizedException()); - socket.disconnect(); - return; + socket.emit('Error', new UnauthorizedException()) + // socket.disconnect(); + return } else { - socket.data.user = user; - const channels = await this.chatservice.getChannelsForUser(user.id); + socket.data.user = user + const channels = await this.chatService.getChannelsForUser(user.id) // Only emit rooms to the specific connected client - return this.server.to(socket.id).emit('channel', channels); + return this.server.to(socket.id).emit('channel', channels) } } catch { - socket.emit('Error', new UnauthorizedException()); - socket.disconnect(); - return; + socket.emit('Error', new UnauthorizedException()) + // socket.disconnect(); } } - handleDisconnect(socket: Socket) { - socket.disconnect(); + handleDisconnect (socket: Socket) { + // socket.disconnect(); } - async onCreateChannel(socket: Socket, channel: CreateChannelDto): Promise { - return this.chatservice.createChannel(channel, socket.data.user); + @SubscribeMessage('createChannel') + async onCreateChannel ( + socket: Socket, + @MessageBody() channeldto: CreateChannelDto + ): Promise { + const channel = new Channel() + channel.name = channeldto.name + const owner = await this.userService.findOne(channeldto.owner) + channel.owners.push(owner) + channel.password = channeldto.password + /// .../// + return await this.chatService.createChannel(channel, socket.data.user) } @SubscribeMessage('joinChannel') - async onJoinChannel(socket: Socket, channel: Channel) { - //add user to channel - const messages = await this.chatservice.findMessagesInChannelForUser( + async onJoinChannel (socket: Socket, channel: Channel) { + // add user to channel + const messages = await this.chatService.findMessagesInChannelForUser( channel, - socket.data.user, - ); - this.server.to(socket.id).emit('messages', messages); + socket.data.user + ) + this.server.to(socket.id).emit('messages', messages) } @SubscribeMessage('leaveChannel') - async onLeaveChannel(socket: Socket) { - await this.chatservice.deleteBySocketId(socket.id); + async onLeaveChannel (socket: Socket) { + await this.chatService.deleteBySocketId(socket.id) } @SubscribeMessage('addMessage') - async onAddMessage(socket: Socket, message: Message) { - const createdMessage: Message = await this.chatservice.createMessage({ + async onAddMessage (socket: Socket, message: Message) { + const createdMessage: Message = await this.chatService.createMessage({ ...message, - author: socket.data.user, - }); - const channel = await this.chatservice.getChannel( - createdMessage.channel.id, - ); - //send new Message to all joined Users currently online of the channel + author: socket.data.user + }) + const channel = await this.chatService.getChannel( + createdMessage.channel.id + ) + const users = await this.userService.findOnlineInChannel(channel) + /// TODO: Send message to users } } diff --git a/back/volume/src/chat/chat.module.ts b/back/volume/src/chat/chat.module.ts index d3806dd..25d6832 100644 --- a/back/volume/src/chat/chat.module.ts +++ b/back/volume/src/chat/chat.module.ts @@ -1,20 +1,20 @@ -import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { AuthModule } from 'src/auth/auth.module'; -import { UsersModule } from 'src/users/users.module'; -import { ChatGateway } from './chat.gateway'; -import { ChatService } from './chat.service'; -import { UsersService } from 'src/users/users.service'; -import { Channel } from './model/channel.entity'; -import { Message } from './model/message.entity'; +import { Module } from '@nestjs/common' +import { TypeOrmModule } from '@nestjs/typeorm' +import { AuthModule } from 'src/auth/auth.module' +import { UsersModule } from 'src/users/users.module' +import { ChatGateway } from './chat.gateway' +import { ChatService } from './chat.service' +import { UsersService } from 'src/users/users.service' +import { Channel } from './model/channel.entity' +import { Message } from './model/message.entity' @Module({ imports: [ AuthModule, UsersModule, TypeOrmModule.forFeature([Channel]), - TypeOrmModule.forFeature([Message]), + TypeOrmModule.forFeature([Message]) ], - providers: [ChatGateway, ChatService], + providers: [ChatGateway, ChatService] }) export class ChatModule {} diff --git a/back/volume/src/chat/chat.service.ts b/back/volume/src/chat/chat.service.ts index 49c9e8d..639b4a0 100644 --- a/back/volume/src/chat/chat.service.ts +++ b/back/volume/src/chat/chat.service.ts @@ -1,61 +1,57 @@ -import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; -import * as bcrypt from 'bcrypt' - -import { Channel } from 'src/chat/model/channel.entity'; -import { User } from 'src/users/user.entity'; -import { Message } from './model/message.entity'; +import { Injectable } from '@nestjs/common' +import { InjectRepository } from '@nestjs/typeorm' +import { Channel } from 'src/chat/model/channel.entity' +import { type User } from 'src/users/user.entity' +import { Repository } from 'typeorm' +import { Message } from './model/message.entity' import { CreateChannelDto } from './model/create-channel.dto' @Injectable() export class ChatService { - constructor( + constructor ( @InjectRepository(Channel) private readonly ChannelRepository: Repository, @InjectRepository(Message) - private readonly MessageRepository: Repository, - ) { } - - async createChannel(channelDatas: CreateChannelDto, creator: User): Promise { - channelDatas.password = await bcrypt.hash(channelDatas.password, 10); - const newChannel = this.ChannelRepository.create(channelDatas); - await this.addCreatorToChannel(newChannel, creator); - this.ChannelRepository.save(newChannel); - newChannel.password = undefined; - return newChannel; + private readonly MessageRepository: Repository + ) {} + + async createChannel (Channel: Channel, creator: User): Promise { + const newChannel = await this.addCreatorToChannel(Channel, creator) + return await this.ChannelRepository.save(newChannel) } - async getChannelsForUser(userId: number): Promise { - return this.ChannelRepository.find({}); //where userId is in User[] of channel? + async getChannelsForUser (userId: number): Promise { + return await this.ChannelRepository.find({}) // where userId is in User[] of channel? } - async addCreatorToChannel(Channel: Channel, creator: User): Promise { - Channel.users.push(creator); - return Channel; + async addCreatorToChannel (Channel: Channel, creator: User): Promise { + Channel.users.push(creator) + return Channel } - async createMessage(message: Message): Promise { - return this.MessageRepository.save(this.MessageRepository.create(message)); + async createMessage (message: Message): Promise { + return await this.MessageRepository.save( + this.MessageRepository.create(message) + ) } - async deleteBySocketId(socketId: string) { - return this.ChannelRepository.delete({}); // for disconnect + async deleteBySocketId (socketId: string) { + return await this.ChannelRepository.delete({}) // for disconnect } - async getChannel(id: number): Promise { - return this.ChannelRepository.findOneBy({ id }); + async getChannel (id: number): Promise { + return await this.ChannelRepository.findOneBy({ id }) } - async findMessagesInChannelForUser( + async findMessagesInChannelForUser ( channel: Channel, - user: User, - ): Promise { - return this.MessageRepository.findOne({ - where: { - channel: { id: channel.id } - }, - relations: { channel: true }, - }) + user: User + ): Promise { + return await this.MessageRepository.createQueryBuilder('message') + .where('message.channel = :chan', { chan: channel }) + .andWhere('message.author NOT IN (:...blocked)', { + blocked: user.blocked + }) + .getMany() } } diff --git a/back/volume/src/chat/model/channel.entity.ts b/back/volume/src/chat/model/channel.entity.ts index a1224fb..77c1639 100644 --- a/back/volume/src/chat/model/channel.entity.ts +++ b/back/volume/src/chat/model/channel.entity.ts @@ -1,4 +1,4 @@ -import { User } from 'src/users/user.entity'; +import { User } from 'src/users/user.entity' import { BeforeInsert, Column, @@ -6,44 +6,46 @@ import { JoinTable, ManyToMany, OneToMany, - PrimaryGeneratedColumn, -} from 'typeorm'; -import { Message } from './message.entity'; -import * as bcrypt from 'bcrypt'; + PrimaryGeneratedColumn +} from 'typeorm' +import { Message } from './message.entity' +import * as bcrypt from 'bcrypt' @Entity() export class Channel { @PrimaryGeneratedColumn() - id: number; + id: number @Column() - name: string; + name: string @ManyToMany(() => User) @JoinTable() - owners: User[]; + owners: User[] @ManyToMany(() => User) @JoinTable() - users: User[]; + users: User[] @OneToMany(() => Message, (message: Message) => message.channel) - messages: Message[]; + messages: Message[] - @OneToMany(() => User, (user: User) => user.id) //refuse connection - banned: User[]; + @OneToMany(() => User, (user: User) => user.id) // refuse connection + banned: User[] - @OneToMany(() => User, (user: User) => user.id) //refuse post - muted: User[]; + @OneToMany(() => User, (user: User) => user.id) // refuse post + muted: User[] @Column({ select: false }) - password: string; + password: string @BeforeInsert() - async hashPassword() { + async hashPassword () { this.password = await bcrypt.hash( this.password, - Number(process.env.HASH_SALT), - ); + Number(process.env.HASH_SALT) + ) } } + +export default Channel diff --git a/back/volume/src/chat/model/create-channel.dto.ts b/back/volume/src/chat/model/create-channel.dto.ts index ab7d4ec..cc3fbff 100644 --- a/back/volume/src/chat/model/create-channel.dto.ts +++ b/back/volume/src/chat/model/create-channel.dto.ts @@ -1,13 +1,13 @@ -import { IsPositive, IsAlpha, IsString, IsOptional } from 'class-validator'; +import { IsPositive, IsAlpha, IsString, IsOptional } from 'class-validator' export class CreateChannelDto { @IsString() @IsAlpha() - name: string; + name: string @IsPositive() - owner: number; + owner: number @IsOptional() - password: string; + password: string } diff --git a/back/volume/src/chat/model/message.entity.ts b/back/volume/src/chat/model/message.entity.ts index 416b6b1..da579c1 100644 --- a/back/volume/src/chat/model/message.entity.ts +++ b/back/volume/src/chat/model/message.entity.ts @@ -1,4 +1,4 @@ -import { User } from 'src/users/user.entity'; +import { User } from 'src/users/user.entity' import { Column, CreateDateColumn, @@ -6,26 +6,28 @@ import { JoinColumn, JoinTable, ManyToOne, - PrimaryGeneratedColumn, -} from 'typeorm'; -import { Channel } from './channel.entity'; + PrimaryGeneratedColumn +} from 'typeorm' +import { Channel } from './channel.entity' @Entity() export class Message { @PrimaryGeneratedColumn() - id: number; + id: number @Column() - text: string; + text: string @ManyToOne(() => User, (author: User) => author.messages) @JoinColumn() - author: User; + author: User @ManyToOne(() => Channel, (channel: Channel) => channel.messages) @JoinTable() - channel: Channel; + channel: Channel @CreateDateColumn() - created_at: Date; + created_at: Date } + +export default Message diff --git a/back/volume/src/chat/model/update-channel.dto.ts b/back/volume/src/chat/model/update-channel.dto.ts index 5b8d0d3..b9f324a 100644 --- a/back/volume/src/chat/model/update-channel.dto.ts +++ b/back/volume/src/chat/model/update-channel.dto.ts @@ -1,22 +1,22 @@ -import { PartialType } from '@nestjs/mapped-types'; -import { CreateChannelDto } from './create-channel.dto'; -import { Message } from './message.entity'; -import { User } from 'src/users/user.entity'; -import { IsString } from 'class-validator'; +import { PartialType } from '@nestjs/mapped-types' +import { CreateChannelDto } from './create-channel.dto' +import { type Message } from './message.entity' +import { type User } from 'src/users/user.entity' +import { IsString } from 'class-validator' export class UpdateChannelDto extends PartialType(CreateChannelDto) { - id: number; + id: number - users: [User]; + users: [User] - messages: [Message]; + messages: [Message] - owners: [number]; //user id + owners: [number] // user id - banned: [number]; //user id + banned: [number] // user id - muted: [number]; //user id + muted: [number] // user id @IsString() - password: string; + password: string } diff --git a/back/volume/src/db/db.module.ts b/back/volume/src/db/db.module.ts index 866021a..16e860a 100644 --- a/back/volume/src/db/db.module.ts +++ b/back/volume/src/db/db.module.ts @@ -19,8 +19,7 @@ import * as Joi from 'joi' autoLoadEntities: true, synchronize: true }) - }), + }) ] }) - -export class DbModule { } +export class DbModule {} diff --git a/back/volume/src/types.d.ts b/back/volume/src/types.d.ts index 1944541..afe5375 100644 --- a/back/volume/src/types.d.ts +++ b/back/volume/src/types.d.ts @@ -1,5 +1,5 @@ declare module 'passport-42' { - export type Profile = any; - export type VerifyCallback = any; - export class Strategy {}; + export type Profile = any + export type VerifyCallback = any + export class Strategy {} } diff --git a/back/volume/src/users/user.dto.ts b/back/volume/src/users/user.dto.ts index 9ed7ff8..f5cd8a7 100644 --- a/back/volume/src/users/user.dto.ts +++ b/back/volume/src/users/user.dto.ts @@ -1,5 +1,6 @@ import { IsString, + IsNumber, IsNotEmpty, IsEmail, Length, @@ -7,6 +8,9 @@ import { IsOptional } from 'class-validator' +import { ApiProperty } from '@nestjs/swagger' +import { Express } from 'express' + export class CreateUserDto { @IsPositive() @IsNotEmpty() @@ -15,10 +19,6 @@ export class CreateUserDto { @IsString() @IsNotEmpty() readonly username: string - - @IsString() - @IsNotEmpty() - readonly avatar: string } export class UpdateUserDto { @@ -30,10 +30,11 @@ export class UpdateUserDto { @IsNotEmpty() readonly username: string - @IsString() - @IsNotEmpty() - readonly avatar: string - @IsOptional() readonly status: string } + +export class AvatarUploadDto { + @ApiProperty({ type: 'string', format: 'binary' }) + file: Express.Multer.File +} diff --git a/back/volume/src/users/user.entity.ts b/back/volume/src/users/user.entity.ts index 94b7c4f..58429e6 100644 --- a/back/volume/src/users/user.entity.ts +++ b/back/volume/src/users/user.entity.ts @@ -1,39 +1,44 @@ import { - Column, Entity, - ManyToMany, - OneToMany, PrimaryGeneratedColumn, -} from 'typeorm'; -import { Message } from 'src/chat/model/message.entity'; -import { Channel } from 'src/chat/model/channel.entity'; + Column, + OneToOne, + OneToMany, + ManyToMany, + JoinColumn +} from 'typeorm' + +import Message from 'src/chat/model/message.entity' +import Channel from 'src/chat/model/channel.entity' @Entity() export class User { - @PrimaryGeneratedColumn() - id: number; - @Column({ unique: true }) - id_42: number; + id_42: number @Column({ unique: true }) - username: string; - - @Column({ default: '' }) - avatar: string; + username: string @Column({ default: 'online' }) - status: string; + status: string + + @Column({ name: 'avatar' }) + public avatar?: string @OneToMany(() => Message, (message: Message) => message.author) - messages: Message[]; + messages: Message[] @ManyToMany(() => Channel, (channel: Channel) => channel.users) - rooms: Channel[]; + rooms: Channel[] + + @ManyToMany(() => User) + blocked: User[] - @OneToMany(() => User, (user) => user.id) //filter messages - blocked: User[]; + @ManyToMany(() => User) + friends: User[] - //@Column({ default: { wr: -1, place: -1 } }) - //rank: { wr: number; place: number }; + // @Column({ default: { wr: -1, place: -1 } }) + // rank: { wr: number; place: number }; } + +export default User diff --git a/back/volume/src/users/users.controller.ts b/back/volume/src/users/users.controller.ts index 05095e8..3fc7743 100644 --- a/back/volume/src/users/users.controller.ts +++ b/back/volume/src/users/users.controller.ts @@ -4,11 +4,31 @@ import { Post, Body, Param, - ParseIntPipe + ParseIntPipe, + UploadedFile, + UseGuards, + UseInterceptors, + Req, + Res, + StreamableFile, + BadRequestException } from '@nestjs/common' + +import { FileInterceptor } from '@nestjs/platform-express' +import { diskStorage } from 'multer' + import { type User } from './user.entity' import { UsersService } from './users.service' -import { CreateUserDto, UpdateUserDto } from './user.dto' +import { CreateUserDto, UpdateUserDto, AvatarUploadDto } from './user.dto' + +import RequestWithUser from 'src/auth/requestWithUser.interface' +import { FtOauthGuard } from 'src/auth/42-auth.guard' +import { ApiBody, ApiConsumes } from '@nestjs/swagger' + +import { Response } from 'express' +import { createReadStream } from 'fs' +import { join } from 'path' + @Controller('users') export class UsersController { constructor (private readonly usersService: UsersService) {} @@ -27,4 +47,46 @@ export class UsersController { update (@Param('id', ParseIntPipe) id: number, @Body() user: UpdateUserDto) { this.usersService.update(id, user) } + + @Post(':id/avatar') + @UseInterceptors( + FileInterceptor('avatar', { + storage: diskStorage({ + destination: 'avatars/' + }), + fileFilter: (request: Request, file: Express.Multer.File, callback) => { + if (!file.mimetype.includes('image')) { + callback(new BadRequestException('Provide a valid image'), false) + return + } + callback(null, true) + } + }) + ) + @ApiConsumes('multipart/form-data') + @ApiBody({ + description: 'A new avatar for the user', + type: AvatarUploadDto + }) + async addAvatar ( + @Param('id', ParseIntPipe) id: number, + @UploadedFile() file: Express.Multer.File + ) { + await this.usersService.addAvatar(id, file.filename) + } + + @Get(':id/avatar') + async getAvatar ( + @Param('id', ParseIntPipe) id: number, + @Res({ passthrough: true }) response: Response + ) { + const user = await this.usersService.findOne(id) + const filename = user.avatar + const stream = createReadStream(join(process.cwd(), 'avatars/' + filename)) + response.set({ + 'Content-Diposition': `inline; filename="${filename}"`, + 'Content-Type': 'image/jpg' + }) + return new StreamableFile(stream) + } } diff --git a/back/volume/src/users/users.service.ts b/back/volume/src/users/users.service.ts index b3728e5..5b5eed5 100644 --- a/back/volume/src/users/users.service.ts +++ b/back/volume/src/users/users.service.ts @@ -1,52 +1,60 @@ -import { - Injectable, - NotFoundException -} from '@nestjs/common' +import { Injectable, NotFoundException } from '@nestjs/common' import { InjectRepository } from '@nestjs/typeorm' import { Repository } from 'typeorm' import { User } from './user.entity' import { type CreateUserDto, type UpdateUserDto } from './user.dto' +import { type Channel } from 'src/chat/model/channel.entity' @Injectable() export class UsersService { - constructor( + constructor ( @InjectRepository(User) private readonly usersRepository: Repository - ) { } + ) {} - async getAllUsers(): Promise { + async getAllUsers (): Promise { return await this.usersRepository.find({}) } - async getOneUser(username: string): Promise { - const user = await this.usersRepository.findOneBy({ username: username }) - if (user) return user - throw new NotFoundException(`User with username: ${username} not found`) + async getOneUser (username: string): Promise { + return await this.usersRepository.findOneBy({ username }) } - async getOneUser42(id_42: number): Promise { - const user = await this.usersRepository.findOneBy({ id_42: id_42 }) - if (user) return user; - throw new NotFoundException(`User with id_42: ${id_42} not found`) + async getOneUser42 (id_42: number): Promise { + return await this.usersRepository.findOneBy({ id_42 }) } - async create(userData: CreateUserDto) { + async create (userData: CreateUserDto) { try { - const newUser= this.usersRepository.create(userData) + const newUser = this.usersRepository.create(userData) return await this.usersRepository.save(newUser) } catch (err) { throw new Error(`Error creating ${err} user ${err.message}`) } } - async findOne(id: number) { - const user = await this.usersRepository.findOneBy({ id: id }) - if (user) return user; + async findOne (id: number) { + const user = await this.usersRepository.findOneBy({ id }) + if (user) return user throw new NotFoundException(`User #${id} not found`) } - async update(id: number, changes: UpdateUserDto) { + async findOnlineInChannel (channel: Channel): Promise { + return await this.usersRepository + .createQueryBuilder('user') + .where('user.channel = :chan', { chan: channel }) + .andWhere('user.status := status)', { status: 'online' }) + .getMany() + } + + async update (id: number, changes: UpdateUserDto) { const updatedUser = await this.findOne(id) this.usersRepository.merge(updatedUser, changes) return await this.usersRepository.save(updatedUser) } + + async addAvatar (userId: number, filename: string) { + await this.usersRepository.update(userId, { + avatar: filename + }) + } }