Browse Source

chat gateway, service and objects

master
nicolas-arnaud 2 years ago
parent
commit
b7e3c794c7
  1. 8
      .env_sample
  2. 3
      back/entrypoint.sh
  3. 774
      back/volume/package-lock.json
  4. 8
      back/volume/package.json
  5. 2
      back/volume/src/api/api.module.ts
  6. 16
      back/volume/src/app.module.ts
  7. 2
      back/volume/src/auth/auth.controller.ts
  8. 90
      back/volume/src/chat/chat.gateway.ts
  9. 25
      back/volume/src/chat/chat.module.ts
  10. 61
      back/volume/src/chat/chat.service.ts
  11. 33
      back/volume/src/chat/entities/channel.entity.ts
  12. 28
      back/volume/src/chat/entities/message.entity.ts
  13. 49
      back/volume/src/chat/model/channel.entity.ts
  14. 13
      back/volume/src/chat/model/create-channel.dto.ts
  15. 31
      back/volume/src/chat/model/message.entity.ts
  16. 22
      back/volume/src/chat/model/update-channel.dto.ts
  17. 12
      back/volume/src/db/db.module.ts
  18. 34
      back/volume/src/users/user.entity.ts
  19. 44
      back/volume/src/users/users.service.ts
  20. 13
      docker-compose.yml
  21. 6
      package-lock.json

8
.env_sample

@ -4,10 +4,14 @@ POSTGRES_USER: postgres_usr
POSTGRES_PASSWORD: postgres_pw
POSTGRES_DB: transcendence
PGADMIN_DEFAULT_EMAIL=admin@pg.com
PGADMIN_DEFAULT_PASSWORD=admin
BACK_PORT=3001
HASH_SALT=10
JWT_SECRET=
JWT_EXPIRATION_TIME_SECONDS=900
JWT_SECRET=test
JWT_EXPIRATION_TIME=900
FT_OAUTH_CLIENT_ID=
FT_OAUTH_CLIENT_SECRET=

3
back/entrypoint.sh

@ -7,9 +7,10 @@ POSTGRES_PASSWORD= $POSTGRES_PASSWORD
POSTGRES_DB= $POSTGRES_DB
BACK_PORT=$BACK_PORT
HASH_SALT=$HASH_SALT
JWT_SECRET=$JWT_SECRET
JWT_EXPIRATION_TIME_SECONDS=$JWT_EXPIRATION_TIME_SECONDS
JWT_EXPIRATION_TIME=$JWT_EXPIRATION_TIME
FT_OAUTH_CLIENT_ID=$FT_OAUTH_CLIENT_ID
FT_OAUTH_CLIENT_SECRET=$FT_OAUTH_CLIENT_SECRET

774
back/volume/package-lock.json

File diff suppressed because it is too large

8
back/volume/package.json

@ -25,6 +25,7 @@
"@nestjs/common": "^9.0.0",
"@nestjs/config": "^2.3.1",
"@nestjs/core": "^9.0.0",
"@nestjs/mapped-types": "^1.2.2",
"@nestjs/passport": "^9.0.3",
"@nestjs/platform-express": "^9.0.0",
"@nestjs/platform-ws": "^9.2.0",
@ -32,14 +33,16 @@
"@nestjs/testing": "^9.0.0",
"@nestjs/typeorm": "^9.0.1",
"@nestjs/websockets": "^9.2.0",
"@types/bcrypt": "^5.0.0",
"@types/cookie-parser": "^1.4.3",
"@types/express": "^4.17.13",
"@types/express-session": "^1.17.6",
"@types/passport": "^1.0.12",
"@types/node": "^16.0.0",
"@types/passport": "^1.0.12",
"@types/ws": "^8.5.3",
"class-validator": "^0.14.0",
"bcrypt": "^5.1.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"cookie-parser": "^1.4.6",
"express-session": "^1.17.3",
"joi": "^17.8.3",
@ -50,6 +53,7 @@
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rxjs": "^7.2.0",
"socket.io": "^4.6.1",
"typeorm": "^0.3.12",
"ws": "^8.11.0"
},

2
back/volume/src/api/api.module.ts

@ -1,9 +1,7 @@
import { Module } from '@nestjs/common'
import { HttpModule } from '@nestjs/axios'
import { ApiController } from './api.controller'
@Module({
imports: [HttpModule],
controllers: [ApiController],
})

16
back/volume/src/app.module.ts

@ -1,4 +1,6 @@
import { Module } from '@nestjs/common'
import { ConfigModule } from '@nestjs/config'
import * as Joi from 'joi'
import { ApiModule } from './api/api.module'
import { AuthModule } from './auth/auth.module'
@ -9,12 +11,24 @@ import { UsersModule } from './users/users.module'
@Module({
imports: [
ConfigModule.forRoot({
validationSchema: Joi.object({
POSTGRES_HOST: Joi.string().required(),
POSTGRES_PORT: Joi.number().required(),
POSTGRES_USER: Joi.string().required(),
POSTGRES_PASSWORD: Joi.string().required(),
POSTGRES_DB: Joi.string().required(),
BACK_PORT: Joi.number(),
JWT_SECRET: Joi.string().required(),
JWT_EXPIRATION_TIME: Joi.string().required()
})
}),
ApiModule,
AuthModule,
ChatModule,
DbModule,
PongModule,
UsersModule
UsersModule,
],
})
export class AppModule { }

2
back/volume/src/auth/auth.controller.ts

@ -10,7 +10,7 @@ export class AuthController {
@Get('42/return')
@UseGuards(FtOauthGuard)
@Redirect('http://localhost:5000/')
@Redirect('http://localhost:80/')
ftAuthCallback (
@Res({ passthrough: true }) response: Response,
@Req() request: Request

90
back/volume/src/chat/chat.gateway.ts

@ -0,0 +1,90 @@
import {
OnGatewayConnection,
OnGatewayDisconnect,
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';
import { CreateChannelDto } from './model/create-channel.dto'
@WebSocketGateway({
cors: {
origin: [
'http://localhost:5000',
'http://localhost:80',
'http://localhost:8080',
],
},
})
export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect {
@WebSocketServer()
server: Server;
constructor(
private userService: UsersService,
private chatservice: ChatService,
) { }
async handleConnection(socket: Socket) {
try {
const user: User = await this.userService.findOne(socket.data.user.id);
if (!user) {
socket.emit('Error', new UnauthorizedException());
socket.disconnect();
return;
} else {
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);
}
} catch {
socket.emit('Error', new UnauthorizedException());
socket.disconnect();
return;
}
}
handleDisconnect(socket: Socket) {
socket.disconnect();
}
async onCreateChannel(socket: Socket, channel: CreateChannelDto): Promise<Channel> {
return 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(
channel,
socket.data.user,
);
this.server.to(socket.id).emit('messages', messages);
}
@SubscribeMessage('leaveChannel')
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({
...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
}
}

25
back/volume/src/chat/chat.module.ts

@ -1,15 +1,20 @@
import { Module, forwardRef } from '@nestjs/common'
import { TypeOrmModule } from '@nestjs/typeorm'
import { Message } from './entities/message.entity'
import { Channel } from './entities/channel.entity'
import { UsersModule } from 'src/users/users.module'
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: [
TypeOrmModule.forFeature([Channel, Message]),
forwardRef(() => UsersModule)
]
// providers: [ChatService]
AuthModule,
UsersModule,
TypeOrmModule.forFeature([Channel]),
TypeOrmModule.forFeature([Message]),
],
providers: [ChatGateway, ChatService],
})
export class ChatModule {}

61
back/volume/src/chat/chat.service.ts

@ -0,0 +1,61 @@
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 { CreateChannelDto } from './model/create-channel.dto'
@Injectable()
export class ChatService {
constructor(
@InjectRepository(Channel)
private readonly ChannelRepository: Repository<Channel>,
@InjectRepository(Message)
private readonly MessageRepository: Repository<Message>,
) { }
async createChannel(channelDatas: CreateChannelDto, creator: User): Promise<Channel> {
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;
}
async getChannelsForUser(userId: number): Promise<Channel[]> {
return this.ChannelRepository.find({}); //where userId is in User[] of channel?
}
async addCreatorToChannel(Channel: Channel, creator: User): Promise<Channel> {
Channel.users.push(creator);
return Channel;
}
async createMessage(message: Message): Promise<Message> {
return this.MessageRepository.save(this.MessageRepository.create(message));
}
async deleteBySocketId(socketId: string) {
return this.ChannelRepository.delete({}); // for disconnect
}
async getChannel(id: number): Promise<Channel | null> {
return this.ChannelRepository.findOneBy({ id });
}
async findMessagesInChannelForUser(
channel: Channel,
user: User,
): Promise<Message> {
return this.MessageRepository.findOne({
where: {
channel: { id: channel.id }
},
relations: { channel: true },
})
}
}

33
back/volume/src/chat/entities/channel.entity.ts

@ -1,33 +0,0 @@
import {
Entity,
PrimaryGeneratedColumn,
JoinTable,
OneToMany,
ManyToMany
} from 'typeorm'
import { Message } from './message.entity'
import { User } from 'src/users/user.entity'
@Entity('channel')
export class Channel {
@PrimaryGeneratedColumn()
id: number
@OneToMany(() => Message, (message) => message.channel)
message: Message[]
@ManyToMany(() => User)
@JoinTable({
name: 'users_id',
joinColumn: {
name: 'channel',
referencedColumnName: 'id'
},
inverseJoinColumn: {
name: 'user',
referencedColumnName: 'id'
}
})
user: User[]
}

28
back/volume/src/chat/entities/message.entity.ts

@ -1,28 +0,0 @@
import {
BaseEntity,
Entity,
PrimaryGeneratedColumn,
Column,
JoinColumn,
ManyToOne
} from 'typeorm'
import { Channel } from './channel.entity'
import { User } from 'src/users/user.entity'
@Entity('message')
export class Message extends BaseEntity {
@PrimaryGeneratedColumn()
public id: number
@Column()
public content: string
@ManyToOne(() => User)
@JoinColumn({ name: 'author_id' })
public author: User
@ManyToOne(() => Channel)
@JoinColumn({ name: 'channel_id' })
public channel: Channel
}

49
back/volume/src/chat/model/channel.entity.ts

@ -0,0 +1,49 @@
import { User } from 'src/users/user.entity';
import {
BeforeInsert,
Column,
Entity,
JoinTable,
ManyToMany,
OneToMany,
PrimaryGeneratedColumn,
} from 'typeorm';
import { Message } from './message.entity';
import * as bcrypt from 'bcrypt';
@Entity()
export class Channel {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@ManyToMany(() => User)
@JoinTable()
owners: User[];
@ManyToMany(() => User)
@JoinTable()
users: User[];
@OneToMany(() => Message, (message: Message) => message.channel)
messages: Message[];
@OneToMany(() => User, (user: User) => user.id) //refuse connection
banned: User[];
@OneToMany(() => User, (user: User) => user.id) //refuse post
muted: User[];
@Column({ select: false })
password: string;
@BeforeInsert()
async hashPassword() {
this.password = await bcrypt.hash(
this.password,
Number(process.env.HASH_SALT),
);
}
}

13
back/volume/src/chat/model/create-channel.dto.ts

@ -0,0 +1,13 @@
import { IsPositive, IsAlpha, IsString, IsOptional } from 'class-validator';
export class CreateChannelDto {
@IsString()
@IsAlpha()
name: string;
@IsPositive()
owner: number;
@IsOptional()
password: string;
}

31
back/volume/src/chat/model/message.entity.ts

@ -0,0 +1,31 @@
import { User } from 'src/users/user.entity';
import {
Column,
CreateDateColumn,
Entity,
JoinColumn,
JoinTable,
ManyToOne,
PrimaryGeneratedColumn,
} from 'typeorm';
import { Channel } from './channel.entity';
@Entity()
export class Message {
@PrimaryGeneratedColumn()
id: number;
@Column()
text: string;
@ManyToOne(() => User, (author: User) => author.messages)
@JoinColumn()
author: User;
@ManyToOne(() => Channel, (channel: Channel) => channel.messages)
@JoinTable()
channel: Channel;
@CreateDateColumn()
created_at: Date;
}

22
back/volume/src/chat/model/update-channel.dto.ts

@ -0,0 +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';
export class UpdateChannelDto extends PartialType(CreateChannelDto) {
id: number;
users: [User];
messages: [Message];
owners: [number]; //user id
banned: [number]; //user id
muted: [number]; //user id
@IsString()
password: string;
}

12
back/volume/src/db/db.module.ts

@ -5,16 +5,6 @@ import * as Joi from 'joi'
@Module({
imports: [
ConfigModule.forRoot({
validationSchema: Joi.object({
POSTGRES_HOST: Joi.string().required(),
POSTGRES_PORT: Joi.number().required(),
POSTGRES_USER: Joi.string().required(),
POSTGRES_PASSWORD: Joi.string().required(),
POSTGRES_DB: Joi.string().required(),
BACK_PORT: Joi.number(),
})
}),
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
@ -26,7 +16,7 @@ import * as Joi from 'joi'
password: configService.get<string>('POSTGRES_PASSWORD'),
database: configService.get<string>('POSTGRES_DB'),
jwt_secret: configService.get<string>('JWT_SECRET'),
entities: [__dirname + '/../**/*.entity.ts'],
autoLoadEntities: true,
synchronize: true
})
}),

34
back/volume/src/users/user.entity.ts

@ -1,19 +1,39 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'
import {
Column,
Entity,
ManyToMany,
OneToMany,
PrimaryGeneratedColumn,
} 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
id: number;
@Column({ unique: true })
id_42: number
id_42: number;
@Column({ unique: true })
username: string
username: string;
@Column()
avatar: string
@Column({ default: '' })
avatar: string;
@Column({ default: 'online' })
status: string
status: string;
@OneToMany(() => Message, (message: Message) => message.author)
messages: Message[];
@ManyToMany(() => Channel, (channel: Channel) => channel.users)
rooms: Channel[];
@OneToMany(() => User, (user) => user.id) //filter messages
blocked: User[];
//@Column({ default: { wr: -1, place: -1 } })
//rank: { wr: number; place: number };
}

44
back/volume/src/users/users.service.ts

@ -1,5 +1,4 @@
import {
ConflictException,
Injectable,
NotFoundException
} from '@nestjs/common'
@ -10,45 +9,44 @@ import { type CreateUserDto, type UpdateUserDto } from './user.dto'
@Injectable()
export class UsersService {
constructor (
constructor(
@InjectRepository(User) private readonly usersRepository: Repository<User>
) {}
) { }
async getAllUsers (): Promise<User[]> {
async getAllUsers(): Promise<User[]> {
return await this.usersRepository.find({})
}
async getOneUser (username: string): Promise<User | null> {
return await this.usersRepository.findOneBy({ username })
async getOneUser(username: string): Promise<User | null> {
const user = await this.usersRepository.findOneBy({ username: username })
if (user) return user
throw new NotFoundException(`User with username: ${username} not found`)
}
async getOneUser42 (id_42: number): Promise<User | null> {
return await this.usersRepository.findOneBy({ id_42 })
async getOneUser42(id_42: number): Promise<User | null> {
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 create (newUser: CreateUserDto) {
async create(userData: CreateUserDto) {
try {
const user = new User()
user.id_42 = newUser.id_42
user.avatar = newUser.avatar
user.username = newUser.username
return await this.usersRepository.save(user)
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 })
if (user == null) {
async findOne(id: number) {
const user = await this.usersRepository.findOneBy({ id: id })
if (user) return user;
throw new NotFoundException(`User #${id} not found`)
}
return user
}
async update (id: number, changes: UpdateUserDto) {
const user = await this.findOne(id)
await this.usersRepository.merge(user, changes)
return await this.usersRepository.save(user)
async update(id: number, changes: UpdateUserDto) {
const updatedUser = await this.findOne(id)
this.usersRepository.merge(updatedUser, changes)
return await this.usersRepository.save(updatedUser)
}
}

13
docker-compose.yml

@ -33,4 +33,15 @@ services:
networks: [transcendence]
restart: always
env_file: .env
pgadmin:
links:
- postgres:postgres
container_name: pgadmin
image: dpage/pgadmin4
ports:
- "8080:80"
volumes:
- /data/pgadmin:/root/.pgadmin
env_file:
- .env
networks: [transcendence]

6
package-lock.json

@ -1,6 +0,0 @@
{
"name": "transcendance",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}
Loading…
Cancel
Save