├── .prettierrc ├── src ├── auth │ ├── strategies │ │ ├── interfaces │ │ │ └── jwt-payload.interface.ts │ │ ├── local.strategy.ts │ │ └── jwt.strategy.ts │ ├── guards │ │ ├── jwt-auth.guard.ts │ │ └── local-auth.guard.ts │ ├── auth.module.ts │ ├── auth.controller.ts │ └── auth.service.ts ├── chat │ ├── dto │ │ ├── ban-user.dto.ts │ │ ├── leave-room.dto.ts │ │ ├── join-room.dto.ts │ │ ├── kick-user.dto.ts │ │ └── add-message.dto.ts │ ├── chat.module.ts │ ├── adapters │ │ └── auth.adapter.ts │ └── chat.gateway.ts ├── user │ ├── dto │ │ ├── update-user.dto.ts │ │ ├── login-user.dto.ts │ │ └── create-user.dto.ts │ ├── user.module.ts │ ├── entities │ │ └── user.entity.ts │ └── user.service.ts ├── room │ ├── interfaces │ │ └── request-with-user.interface.ts │ ├── dto │ │ ├── update-room.dto.ts │ │ └── create-room.dto.ts │ ├── room.module.ts │ ├── entities │ │ ├── message.entity.ts │ │ └── room.entity.ts │ ├── guards │ │ └── ownership.guard.ts │ ├── room.controller.ts │ └── room.service.ts ├── app.module.ts └── main.ts ├── tsconfig.build.json ├── nest-cli.json ├── docker-compose.yml ├── test ├── jest-e2e.json └── app.e2e-spec.ts ├── .gitignore ├── tsconfig.json ├── .eslintrc.js ├── LICENSE ├── README.md └── package.json /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /src/auth/strategies/interfaces/jwt-payload.interface.ts: -------------------------------------------------------------------------------- 1 | export interface JwtPayload { 2 | id: string; 3 | } 4 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /src/chat/dto/ban-user.dto.ts: -------------------------------------------------------------------------------- 1 | import { KickUserDto } from './kick-user.dto'; 2 | 3 | export class BanUserDto extends KickUserDto {} 4 | -------------------------------------------------------------------------------- /src/chat/dto/leave-room.dto.ts: -------------------------------------------------------------------------------- 1 | import { JoinRoomDto } from './join-room.dto'; 2 | 3 | export class LeaveRoomDto extends JoinRoomDto {} 4 | -------------------------------------------------------------------------------- /src/chat/dto/join-room.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsString } from 'class-validator'; 2 | 3 | export class JoinRoomDto { 4 | @IsString() 5 | roomId: string; 6 | } 7 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src", 4 | "compilerOptions": { 5 | "deleteOutDir": true, 6 | "plugins": ["@nestjs/swagger/plugin"] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/auth/guards/jwt-auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { AuthGuard } from '@nestjs/passport'; 3 | 4 | @Injectable() 5 | export class JwtAuthGuard extends AuthGuard('jwt') {} 6 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | db: 4 | image: postgres 5 | restart: always 6 | ports: 7 | - "5432:5432" 8 | environment: 9 | POSTGRES_PASSWORD: pass123 10 | -------------------------------------------------------------------------------- /src/user/dto/update-user.dto.ts: -------------------------------------------------------------------------------- 1 | import { PartialType } from '@nestjs/swagger'; 2 | 3 | import { CreateUserDto } from './create-user.dto'; 4 | 5 | export class UpdateUserDto extends PartialType(CreateUserDto) {} 6 | -------------------------------------------------------------------------------- /src/auth/guards/local-auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { AuthGuard } from '@nestjs/passport'; 3 | 4 | @Injectable() 5 | export class LocalAuthGuard extends AuthGuard('local') {} 6 | -------------------------------------------------------------------------------- /src/user/dto/login-user.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsString } from 'class-validator'; 2 | 3 | export class LoginUserDto { 4 | @IsString() 5 | username: string; 6 | 7 | @IsString() 8 | password: string; 9 | } 10 | -------------------------------------------------------------------------------- /src/room/interfaces/request-with-user.interface.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'express'; 2 | 3 | import { User } from 'src/user/entities/user.entity'; 4 | 5 | export interface RequestWithUser extends Request { 6 | user: User; 7 | } 8 | -------------------------------------------------------------------------------- /test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/chat/dto/kick-user.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsString, IsUUID } from 'class-validator'; 2 | 3 | export class KickUserDto { 4 | @IsUUID() 5 | roomId: string; 6 | 7 | @IsUUID() 8 | userId: string; 9 | 10 | @IsString() 11 | reason: string; 12 | } 13 | -------------------------------------------------------------------------------- /src/chat/dto/add-message.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsOptional, IsString, IsUUID } from 'class-validator'; 2 | 3 | export class AddMessageDto { 4 | @IsString() 5 | text: string; 6 | 7 | @IsOptional() 8 | @IsUUID() 9 | roomId?: string; 10 | 11 | @IsOptional() 12 | @IsUUID() 13 | userId?: string; 14 | } 15 | -------------------------------------------------------------------------------- /src/user/dto/create-user.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsBoolean, IsString } from 'class-validator'; 2 | 3 | export class CreateUserDto { 4 | @IsString() 5 | readonly username: string; 6 | 7 | @IsString() 8 | readonly password: string; 9 | 10 | @IsString() 11 | readonly avatar: string; 12 | 13 | @IsBoolean() 14 | readonly is_admin: boolean; 15 | } 16 | -------------------------------------------------------------------------------- /src/room/dto/update-room.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsOptional, IsString } from 'class-validator'; 2 | 3 | export class UpdateRoomDto { 4 | @IsString() 5 | @IsOptional() 6 | readonly name?: string; 7 | 8 | @IsString() 9 | @IsOptional() 10 | readonly description?: string; 11 | 12 | @IsString() 13 | @IsOptional() 14 | readonly avatar?: string; 15 | } 16 | -------------------------------------------------------------------------------- /src/room/dto/create-room.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsOptional, IsString } from 'class-validator'; 2 | 3 | export class CreateRoomDto { 4 | @IsString() 5 | readonly name: string; 6 | 7 | @IsString() 8 | readonly description: string; 9 | 10 | @IsString() 11 | readonly avatar: string; 12 | 13 | @IsOptional() 14 | @IsString() 15 | ownerId?: string; 16 | } 17 | -------------------------------------------------------------------------------- /src/user/user.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | 4 | import { UserService } from './user.service'; 5 | 6 | import { User } from './entities/user.entity'; 7 | 8 | @Module({ 9 | imports: [TypeOrmModule.forFeature([User])], 10 | providers: [UserService], 11 | exports: [UserService], 12 | }) 13 | export class UserModule {} 14 | -------------------------------------------------------------------------------- /src/chat/chat.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { ChatGateway } from './chat.gateway'; 4 | 5 | import { UserModule } from 'src/user/user.module'; 6 | import { AuthModule } from 'src/auth/auth.module'; 7 | import { RoomModule } from 'src/room/room.module'; 8 | 9 | @Module({ 10 | imports: [UserModule, AuthModule, RoomModule], 11 | providers: [ChatGateway], 12 | }) 13 | export class ChatModule {} 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | pnpm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # OS 15 | .DS_Store 16 | 17 | # Tests 18 | /coverage 19 | /.nyc_output 20 | 21 | # IDEs and editors 22 | /.idea 23 | .project 24 | .classpath 25 | .c9/ 26 | *.launch 27 | .settings/ 28 | *.sublime-workspace 29 | 30 | # IDE - VSCode 31 | .vscode/* 32 | !.vscode/settings.json 33 | !.vscode/tasks.json 34 | !.vscode/launch.json 35 | !.vscode/extensions.json 36 | 37 | .env 38 | -------------------------------------------------------------------------------- /src/auth/strategies/local.strategy.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { PassportStrategy } from '@nestjs/passport'; 3 | 4 | import { Strategy } from 'passport-local'; 5 | 6 | import { AuthService } from '../auth.service'; 7 | 8 | @Injectable() 9 | export class LocalStrategy extends PassportStrategy(Strategy) { 10 | constructor(private readonly authService: AuthService) { 11 | super(); 12 | } 13 | 14 | async validate(username: string, password: string) { 15 | const user = await this.authService.validateUser({ username, password }); 16 | 17 | return user; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/room/room.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | 4 | import { UserModule } from 'src/user/user.module'; 5 | 6 | import { Room } from './entities/room.entity'; 7 | import { Message } from './entities/message.entity'; 8 | 9 | import { RoomController } from './room.controller'; 10 | 11 | import { RoomService } from './room.service'; 12 | 13 | @Module({ 14 | imports: [TypeOrmModule.forFeature([Room, Message]), UserModule], 15 | controllers: [RoomController], 16 | providers: [RoomService], 17 | exports: [RoomService], 18 | }) 19 | export class RoomModule {} 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "strictNullChecks": false, 16 | "noImplicitAny": false, 17 | "strictBindCallApply": false, 18 | "forceConsistentCasingInFileNames": false, 19 | "noFallthroughCasesInSwitch": false 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/room/entities/message.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Entity, 3 | PrimaryGeneratedColumn, 4 | Column, 5 | CreateDateColumn, 6 | ManyToOne, 7 | JoinTable, 8 | } from 'typeorm'; 9 | 10 | import { User } from 'src/user/entities/user.entity'; 11 | import { Room } from './room.entity'; 12 | 13 | @Entity() 14 | export class Message { 15 | @PrimaryGeneratedColumn('uuid') 16 | id: string; 17 | 18 | @Column({ length: 250 }) 19 | text: string; 20 | 21 | @CreateDateColumn() 22 | created_at: Date; 23 | 24 | @JoinTable() 25 | @ManyToOne(() => Room, (room: Room) => room.messages) 26 | room: Room; 27 | 28 | @JoinTable() 29 | @ManyToOne(() => User, (user: User) => user.messages) 30 | user: User; 31 | } 32 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: [ 9 | 'plugin:@typescript-eslint/recommended', 10 | 'plugin:prettier/recommended', 11 | ], 12 | root: true, 13 | env: { 14 | node: true, 15 | jest: true, 16 | }, 17 | ignorePatterns: ['.eslintrc.js'], 18 | rules: { 19 | '@typescript-eslint/interface-name-prefix': 'off', 20 | '@typescript-eslint/explicit-function-return-type': 'off', 21 | '@typescript-eslint/explicit-module-boundary-types': 'off', 22 | '@typescript-eslint/no-explicit-any': 'off', 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/auth/strategies/jwt.strategy.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { PassportStrategy } from '@nestjs/passport'; 3 | import { ExtractJwt, Strategy } from 'passport-jwt'; 4 | 5 | import { UserService } from 'src/user/user.service'; 6 | 7 | import { JwtPayload } from './interfaces/jwt-payload.interface'; 8 | 9 | @Injectable() 10 | export class JwtStrategy extends PassportStrategy(Strategy) { 11 | constructor(private readonly userService: UserService) { 12 | super({ 13 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 14 | ignoreExpiration: false, 15 | secretOrKey: process.env.JWT_ACCESS_SECRET, 16 | }); 17 | } 18 | 19 | async validate(payload: JwtPayload) { 20 | const user = await this.userService.findOne(payload.id); 21 | 22 | return user; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { JwtModule } from '@nestjs/jwt'; 3 | import { PassportModule } from '@nestjs/passport'; 4 | 5 | import { UserModule } from 'src/user/user.module'; 6 | 7 | import { AuthController } from './auth.controller'; 8 | 9 | import { AuthService } from './auth.service'; 10 | 11 | import { JwtStrategy } from './strategies/jwt.strategy'; 12 | import { LocalStrategy } from './strategies/local.strategy'; 13 | 14 | @Module({ 15 | imports: [ 16 | UserModule, 17 | PassportModule, 18 | JwtModule.register({ 19 | secret: process.env.JWT_ACCESS_SECRET, 20 | signOptions: { expiresIn: process.env.JWT_ACCESS_EXPIRE }, 21 | }), 22 | ], 23 | controllers: [AuthController], 24 | providers: [AuthService, LocalStrategy, JwtStrategy], 25 | exports: [AuthService], 26 | }) 27 | export class AuthModule {} 28 | -------------------------------------------------------------------------------- /src/room/entities/room.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Entity, 3 | PrimaryGeneratedColumn, 4 | Column, 5 | OneToMany, 6 | ManyToMany, 7 | } from 'typeorm'; 8 | 9 | import { User } from 'src/user/entities/user.entity'; 10 | import { Message } from './message.entity'; 11 | 12 | @Entity() 13 | export class Room { 14 | @PrimaryGeneratedColumn('uuid') 15 | id: string; 16 | 17 | @Column({ length: 20 }) 18 | name: string; 19 | 20 | @Column({ length: 60 }) 21 | description: string; 22 | 23 | @Column() 24 | avatar: string; 25 | 26 | @Column('uuid') 27 | ownerId: string; 28 | 29 | @OneToMany(() => User, (user: User) => user.room) 30 | users: Array; 31 | 32 | @ManyToMany(() => User, (user: User) => user.bannedRooms) 33 | bannedUsers: Array; 34 | 35 | @OneToMany(() => Message, (message: Message) => message.room) 36 | messages: Array; 37 | } 38 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { TypeOrmModule } from '@nestjs/typeorm'; 2 | import { Module } from '@nestjs/common'; 3 | 4 | import { ChatModule } from './chat/chat.module'; 5 | import { UserModule } from './user/user.module'; 6 | import { AuthModule } from './auth/auth.module'; 7 | import { ConfigModule } from '@nestjs/config'; 8 | import { RoomModule } from './room/room.module'; 9 | 10 | @Module({ 11 | imports: [ 12 | ConfigModule.forRoot(), 13 | TypeOrmModule.forRoot({ 14 | type: 'postgres', 15 | host: process.env.DB_HOST, 16 | port: +process.env.DB_PORT, 17 | username: process.env.DB_USERNAME, 18 | password: process.env.DB_PASS, 19 | database: process.env.DB_NAME, 20 | autoLoadEntities: true, 21 | synchronize: true, 22 | }), 23 | ChatModule, 24 | UserModule, 25 | AuthModule, 26 | RoomModule, 27 | ], 28 | providers: [], 29 | }) 30 | export class AppModule {} 31 | -------------------------------------------------------------------------------- /src/user/entities/user.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Entity, 3 | PrimaryGeneratedColumn, 4 | Column, 5 | ManyToOne, 6 | JoinTable, 7 | OneToMany, 8 | ManyToMany, 9 | } from 'typeorm'; 10 | 11 | import { Room } from 'src/room/entities/room.entity'; 12 | import { Message } from 'src/room/entities/message.entity'; 13 | 14 | @Entity() 15 | export class User { 16 | @PrimaryGeneratedColumn('uuid') 17 | id: string; 18 | 19 | @Column({ length: 20 }) 20 | username: string; 21 | 22 | @Column({ length: 60 }) 23 | password: string; 24 | 25 | @Column() 26 | avatar: string; 27 | 28 | @Column() 29 | is_admin: boolean; 30 | 31 | @JoinTable() 32 | @ManyToOne(() => Room, (room: Room) => room.users) 33 | room: Room; 34 | 35 | @JoinTable() 36 | @ManyToMany(() => Room, (room: Room) => room.bannedUsers, { eager: true }) 37 | bannedRooms: Array; 38 | 39 | @OneToMany(() => Message, (message: Message) => message.user) 40 | messages: Array; 41 | } 42 | -------------------------------------------------------------------------------- /src/room/guards/ownership.guard.ts: -------------------------------------------------------------------------------- 1 | import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; 2 | 3 | import { Observable } from 'rxjs'; 4 | 5 | import { RequestWithUser } from '../interfaces/request-with-user.interface'; 6 | 7 | import { RoomService } from '../room.service'; 8 | 9 | @Injectable() 10 | export class OwnershipGuard implements CanActivate { 11 | constructor(private readonly roomSerivce: RoomService) {} 12 | 13 | canActivate( 14 | context: ExecutionContext, 15 | ): boolean | Promise | Observable { 16 | return new Promise(async (resolve) => { 17 | try { 18 | const req = context.switchToHttp().getRequest(); 19 | const roomId = req.params.id; 20 | 21 | const room = await this.roomSerivce.findOne(roomId); 22 | 23 | if (room.ownerId === req.user.id) { 24 | resolve(true); 25 | } 26 | 27 | resolve(false); 28 | } catch (e) {} 29 | }); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { ValidationPipe } from '@nestjs/common'; 2 | import { NestFactory } from '@nestjs/core'; 3 | import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; 4 | 5 | import * as cookieParser from 'cookie-parser'; 6 | 7 | import { AppModule } from './app.module'; 8 | 9 | import { AuthIoAdapter } from './chat/adapters/auth.adapter'; 10 | 11 | async function bootstrap() { 12 | const app = await NestFactory.create(AppModule); 13 | 14 | app.use(cookieParser()); 15 | 16 | app.useGlobalPipes( 17 | new ValidationPipe({ 18 | whitelist: true, 19 | transform: true, 20 | }), 21 | ); 22 | 23 | app.useWebSocketAdapter(new AuthIoAdapter(app)); 24 | 25 | const options = new DocumentBuilder() 26 | .setTitle('Realtime Chat') 27 | .setDescription('Chat created using Nest.js + Websockets') 28 | .setVersion('1.0') 29 | .build(); 30 | const document = SwaggerModule.createDocument(app, options); 31 | SwaggerModule.setup('api', app, document); 32 | 33 | await app.listen(3000); 34 | } 35 | bootstrap(); 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Vladyslav Kondratiuk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/chat/adapters/auth.adapter.ts: -------------------------------------------------------------------------------- 1 | import { INestApplicationContext } from '@nestjs/common'; 2 | import { IoAdapter } from '@nestjs/platform-socket.io'; 3 | 4 | import { AuthService } from 'src/auth/auth.service'; 5 | import { UserService } from 'src/user/user.service'; 6 | 7 | export class AuthIoAdapter extends IoAdapter { 8 | private readonly authService: AuthService; 9 | private readonly userService: UserService; 10 | 11 | constructor(private app: INestApplicationContext) { 12 | super(app); 13 | this.authService = this.app.get(AuthService); 14 | this.userService = this.app.get(UserService); 15 | } 16 | 17 | createIOServer(port: number, options?: any): any { 18 | options.allowRequest = async (request, allowFunction) => { 19 | const token = request._query?.token; 20 | 21 | const isVerified = 22 | token && (await this.authService.verifyAccessToken(token)); 23 | const userExists = 24 | isVerified && (await this.userService.findOne(isVerified.id)); 25 | 26 | if (isVerified && userExists) { 27 | return allowFunction(null, true); 28 | } 29 | 30 | return allowFunction('Unauthorized', false); 31 | }; 32 | 33 | return super.createIOServer(port, options); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Nest Logo 3 |

4 | 5 | [circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 6 | [circleci-url]: https://circleci.com/gh/nestjs/nest 7 | 8 |

A progressive Node.js framework for building efficient and scalable server-side applications.

9 | 10 | # Websockets chat 11 | 12 | I decided to get familiar with websockets in [Nest.js](https://github.com/nestjs/nest), realtime chat is the first thing that came to my mind 13 | 14 | ## Features 15 | 16 | - PassportJS/JWT auth 17 | - Rooms 18 | - Kick/Ban user 19 | 20 | ## Installation 21 | 22 | ```bash 23 | $ yarn install 24 | ``` 25 | 26 | ## Example of .env file 27 | ```bash 28 | JWT_ACCESS_SECRET=ACCESS_SECRET 29 | JWT_REFRESH_SECRET=REFRESH_SECRET 30 | JWT_ACCESS_EXPIRE=60m 31 | JWT_REFRESH_EXPIRE=30d 32 | DB_NAME=postgres 33 | DB_HOST=localhost 34 | DB_PORT=5432 35 | DB_USERNAME=postgres 36 | DB_PASS=pass123 37 | ``` 38 | 39 | ## Running the app 40 | 41 | ```bash 42 | # start docker containers 43 | $ docker-compose up 44 | 45 | # development 46 | $ yarn run start 47 | ``` 48 | 49 | -------------------------------------------------------------------------------- /src/room/room.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | Delete, 5 | Get, 6 | Param, 7 | Patch, 8 | Post, 9 | Req, 10 | UseGuards, 11 | } from '@nestjs/common'; 12 | 13 | import { RequestWithUser } from './interfaces/request-with-user.interface'; 14 | import { CreateRoomDto } from './dto/create-room.dto'; 15 | import { UpdateRoomDto } from './dto/update-room.dto'; 16 | 17 | import { RoomService } from './room.service'; 18 | 19 | import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard'; 20 | import { OwnershipGuard } from './guards/ownership.guard'; 21 | 22 | @Controller('room') 23 | export class RoomController { 24 | constructor(private readonly roomService: RoomService) {} 25 | 26 | @UseGuards(JwtAuthGuard) 27 | @Get(':id') 28 | async findOne(@Param('id') id: string) { 29 | return this.roomService.findOne(id); 30 | } 31 | 32 | @UseGuards(JwtAuthGuard) 33 | @Get() 34 | async find() { 35 | return this.roomService.findAll(); 36 | } 37 | 38 | @UseGuards(JwtAuthGuard) 39 | @Post() 40 | async create( 41 | @Req() req: RequestWithUser, 42 | @Body() createRoomDto: CreateRoomDto, 43 | ) { 44 | createRoomDto.ownerId = req.user.id; 45 | 46 | return this.roomService.create(createRoomDto); 47 | } 48 | 49 | @UseGuards(JwtAuthGuard, OwnershipGuard) 50 | @Patch(':id') 51 | async update( 52 | @Param('id') id: string, 53 | @Req() req: RequestWithUser, 54 | @Body() updateRoomDto: UpdateRoomDto, 55 | ) { 56 | return this.roomService.update(id, updateRoomDto); 57 | } 58 | 59 | @UseGuards(JwtAuthGuard, OwnershipGuard) 60 | @Delete(':id') 61 | async remove(@Param('id') id: string) { 62 | return this.roomService.remove(id); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/auth/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | HttpException, 5 | HttpStatus, 6 | Post, 7 | Req, 8 | Res, 9 | UseGuards, 10 | } from '@nestjs/common'; 11 | 12 | import { Request, Response } from 'express'; 13 | 14 | import { AuthService } from './auth.service'; 15 | 16 | import { LocalAuthGuard } from './guards/local-auth.guard'; 17 | 18 | import { CreateUserDto } from 'src/user/dto/create-user.dto'; 19 | import { LoginUserDto } from 'src/user/dto/login-user.dto'; 20 | 21 | @Controller('auth') 22 | export class AuthController { 23 | constructor(private readonly authService: AuthService) {} 24 | 25 | @Post('/signUp') 26 | async singUp( 27 | @Body() userDto: CreateUserDto, 28 | @Res({ passthrough: true }) res: Response, 29 | ) { 30 | const tokens = await this.authService.singUp(userDto); 31 | 32 | if (!tokens) { 33 | throw new HttpException( 34 | 'User under this username already exists', 35 | HttpStatus.BAD_REQUEST, 36 | ); 37 | } 38 | 39 | res.cookie('refreshToken', tokens.refreshToken, { 40 | httpOnly: true, 41 | maxAge: 30 * 24 * 60 * 60 * 1000, 42 | }); 43 | 44 | return tokens; 45 | } 46 | 47 | @Post('/signIn') 48 | @UseGuards(LocalAuthGuard) 49 | async singIn( 50 | @Body() userDto: LoginUserDto, 51 | @Res({ passthrough: true }) res: Response, 52 | ) { 53 | const tokens = await this.authService.signIn(userDto); 54 | 55 | res.cookie('refreshToken', tokens.refreshToken, { 56 | httpOnly: true, 57 | maxAge: 30 * 24 * 60 * 60 * 1000, 58 | }); 59 | 60 | return tokens; 61 | } 62 | 63 | @Post('/update') 64 | async updateTokens(@Req() req: Request) { 65 | const { refreshToken } = req.cookies; 66 | 67 | const accessToken = await this.authService.updateAccessToken(refreshToken); 68 | 69 | if (!accessToken) { 70 | throw new HttpException('Unauthorized', HttpStatus.UNAUTHORIZED); 71 | } 72 | 73 | return accessToken; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/user/user.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ForbiddenException, 3 | Injectable, 4 | NotFoundException, 5 | } from '@nestjs/common'; 6 | import { InjectRepository } from '@nestjs/typeorm'; 7 | 8 | import { Repository } from 'typeorm'; 9 | 10 | import { User } from './entities/user.entity'; 11 | import { Room } from 'src/room/entities/room.entity'; 12 | import { CreateUserDto } from './dto/create-user.dto'; 13 | import { UpdateUserDto } from './dto/update-user.dto'; 14 | 15 | @Injectable() 16 | export class UserService { 17 | constructor( 18 | @InjectRepository(User) private readonly userRepository: Repository, 19 | ) {} 20 | 21 | async findAll() { 22 | const users = await this.userRepository.find(); 23 | 24 | return users; 25 | } 26 | 27 | async findOne(id: string) { 28 | const user = await this.userRepository.findOne(id, { 29 | relations: ['room'], 30 | }); 31 | 32 | if (!user) { 33 | throw new NotFoundException(`There is no user under id ${id}`); 34 | } 35 | 36 | return user; 37 | } 38 | 39 | async findOneByUsername(username: string) { 40 | const user = await this.userRepository.findOne({ username }); 41 | 42 | return user; 43 | } 44 | 45 | async create(createUserDto: CreateUserDto) { 46 | const user = await this.userRepository.create({ 47 | ...createUserDto, 48 | }); 49 | 50 | return this.userRepository.save(user); 51 | } 52 | 53 | async update(id: string, updateUserDto: UpdateUserDto) { 54 | const user = await this.userRepository.preload({ 55 | id, 56 | ...updateUserDto, 57 | }); 58 | 59 | if (!user) { 60 | throw new NotFoundException(`There is no user under id ${id}`); 61 | } 62 | 63 | return this.userRepository.save(user); 64 | } 65 | 66 | async updateUserRoom(id: string, room: Room) { 67 | const user = await this.userRepository.preload({ 68 | id, 69 | room, 70 | }); 71 | 72 | if (!user) { 73 | throw new NotFoundException(`There is no user under id ${id}`); 74 | } 75 | 76 | const isBanned = user.bannedRooms?.find( 77 | (bannedRoom) => bannedRoom.id === room?.id, 78 | ); 79 | 80 | if (isBanned) { 81 | throw new ForbiddenException(`You have been banned from this room`); 82 | } 83 | 84 | return this.userRepository.save(user); 85 | } 86 | 87 | async remove(id: string) { 88 | const user = await this.findOne(id); 89 | 90 | return this.userRepository.remove(user); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "websockets-chat", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "prebuild": "rimraf dist", 10 | "build": "nest build", 11 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 12 | "start": "nest start", 13 | "start:dev": "nest start --watch", 14 | "start:debug": "nest start --debug --watch", 15 | "start:prod": "node dist/main", 16 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 17 | "test": "jest", 18 | "test:watch": "jest --watch", 19 | "test:cov": "jest --coverage", 20 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 21 | "test:e2e": "jest --config ./test/jest-e2e.json" 22 | }, 23 | "dependencies": { 24 | "@nestjs/common": "^8.0.0", 25 | "@nestjs/config": "^1.1.5", 26 | "@nestjs/core": "^8.0.0", 27 | "@nestjs/jwt": "^8.0.0", 28 | "@nestjs/mapped-types": "^1.0.1", 29 | "@nestjs/passport": "^8.0.1", 30 | "@nestjs/platform-express": "^8.2.4", 31 | "@nestjs/platform-socket.io": "^8.2.4", 32 | "@nestjs/swagger": "^5.1.5", 33 | "@nestjs/typeorm": "^8.0.2", 34 | "@nestjs/websockets": "^8.2.4", 35 | "bcryptjs": "^2.4.3", 36 | "class-transformer": "^0.5.1", 37 | "class-validator": "^0.13.2", 38 | "cookie-parser": "^1.4.6", 39 | "dotenv": "^10.0.0", 40 | "passport": "^0.5.2", 41 | "passport-jwt": "^4.0.0", 42 | "passport-local": "^1.0.0", 43 | "pg": "^8.7.1", 44 | "reflect-metadata": "^0.1.13", 45 | "rimraf": "^3.0.2", 46 | "rxjs": "^7.2.0", 47 | "socket.io": "^4.4.0", 48 | "swagger-ui-express": "^4.3.0", 49 | "typeorm": "^0.2.41", 50 | "uuid": "^8.3.2" 51 | }, 52 | "devDependencies": { 53 | "@nestjs/cli": "^8.0.0", 54 | "@nestjs/schematics": "^8.0.0", 55 | "@nestjs/testing": "^8.0.0", 56 | "@types/bcryptjs": "^2.4.2", 57 | "@types/cookie-parser": "^1.4.2", 58 | "@types/express": "^4.17.13", 59 | "@types/jest": "27.0.2", 60 | "@types/node": "^16.0.0", 61 | "@types/passport": "^1.0.7", 62 | "@types/passport-jwt": "^3.0.6", 63 | "@types/passport-local": "^1.0.34", 64 | "@types/supertest": "^2.0.11", 65 | "@typescript-eslint/eslint-plugin": "^5.0.0", 66 | "@typescript-eslint/parser": "^5.0.0", 67 | "eslint": "^8.0.1", 68 | "eslint-config-prettier": "^8.3.0", 69 | "eslint-plugin-prettier": "^4.0.0", 70 | "jest": "^27.2.5", 71 | "prettier": "^2.3.2", 72 | "source-map-support": "^0.5.20", 73 | "supertest": "^6.1.3", 74 | "ts-jest": "^27.0.3", 75 | "ts-loader": "^9.2.3", 76 | "ts-node": "^10.0.0", 77 | "tsconfig-paths": "^3.10.1", 78 | "typescript": "^4.3.5" 79 | }, 80 | "jest": { 81 | "moduleFileExtensions": [ 82 | "js", 83 | "json", 84 | "ts" 85 | ], 86 | "modulePaths": [ 87 | "" 88 | ], 89 | "testRegex": ".*\\.spec\\.ts$", 90 | "transform": { 91 | "^.+\\.(t|j)s$": "ts-jest" 92 | }, 93 | "collectCoverageFrom": [ 94 | "**/*.(t|j)s" 95 | ], 96 | "coverageDirectory": "../coverage", 97 | "testEnvironment": "node" 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Injectable, 3 | UnauthorizedException, 4 | NotFoundException, 5 | } from '@nestjs/common'; 6 | import { JwtService } from '@nestjs/jwt'; 7 | 8 | import * as bcrypt from 'bcryptjs'; 9 | 10 | import { UserService } from 'src/user/user.service'; 11 | 12 | import { User } from 'src/user/entities/user.entity'; 13 | import { CreateUserDto } from 'src/user/dto/create-user.dto'; 14 | import { LoginUserDto } from 'src/user/dto/login-user.dto'; 15 | 16 | @Injectable() 17 | export class AuthService { 18 | constructor( 19 | private readonly userService: UserService, 20 | private readonly jwtService: JwtService, 21 | ) {} 22 | 23 | async singUp(userDto: CreateUserDto) { 24 | const candidate = await this.userService.findOneByUsername( 25 | userDto.username, 26 | ); 27 | 28 | if (candidate) return null; 29 | 30 | const hashedPassword = await bcrypt.hash(userDto.password, 7); 31 | const user = await this.userService.create({ 32 | ...userDto, 33 | password: hashedPassword, 34 | }); 35 | 36 | const tokens = await this.generateTokens(user.id); 37 | 38 | return tokens; 39 | } 40 | 41 | async signIn(userDto: LoginUserDto) { 42 | const user = await this.userService.findOneByUsername(userDto.username); 43 | 44 | const tokens = await this.generateTokens(user.id); 45 | 46 | return tokens; 47 | } 48 | 49 | async validateUser(userDto: LoginUserDto): Promise { 50 | const user = await this.userService.findOneByUsername(userDto.username); 51 | 52 | if (!user) { 53 | throw new NotFoundException(`There is no user under this username`); 54 | } 55 | 56 | const passwordEquals = await bcrypt.compare( 57 | userDto.password, 58 | user.password, 59 | ); 60 | 61 | if (passwordEquals) return user; 62 | 63 | throw new UnauthorizedException({ message: 'Incorrect password' }); 64 | } 65 | 66 | verifyAccessToken(accessToken: string) { 67 | try { 68 | const payload = this.jwtService.verify(accessToken, { 69 | secret: process.env.JWT_ACCESS_SECRET, 70 | }); 71 | 72 | return payload; 73 | } catch (err) { 74 | return null; 75 | } 76 | } 77 | 78 | verifyRefreshToken(refreshToken: string) { 79 | const payload = this.jwtService.verify(refreshToken, { 80 | secret: process.env.JWT_REFRESH_SECRET, 81 | }); 82 | 83 | return payload; 84 | } 85 | 86 | async updateAccessToken(refreshToken: string) { 87 | try { 88 | const userId = this.verifyRefreshToken(refreshToken); 89 | 90 | const tokens = await this.generateTokens(userId); 91 | 92 | return tokens.accessToken; 93 | } catch (e) { 94 | return null; 95 | } 96 | } 97 | 98 | private async generateTokens(id: string) { 99 | const payload = { id }; 100 | 101 | const accessToken = this.jwtService.sign(payload, { 102 | secret: process.env.JWT_ACCESS_SECRET, 103 | expiresIn: process.env.JWT_ACCESS_EXPIRE, 104 | }); 105 | const refreshToken = this.jwtService.sign(payload, { 106 | secret: process.env.JWT_REFRESH_SECRET, 107 | expiresIn: process.env.JWT_REFRESH_EXPIRE, 108 | }); 109 | const tokens = { accessToken, refreshToken }; 110 | 111 | return tokens; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/room/room.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, NotFoundException } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | 4 | import { Repository } from 'typeorm'; 5 | 6 | import { UserService } from 'src/user/user.service'; 7 | 8 | import { Room } from './entities/room.entity'; 9 | import { Message } from './entities/message.entity'; 10 | 11 | import { AddMessageDto } from 'src/chat/dto/add-message.dto'; 12 | import { CreateRoomDto } from 'src/room/dto/create-room.dto'; 13 | import { UpdateRoomDto } from 'src/room/dto/update-room.dto'; 14 | import { BanUserDto } from 'src/chat/dto/ban-user.dto'; 15 | 16 | @Injectable() 17 | export class RoomService { 18 | constructor( 19 | @InjectRepository(Room) private readonly roomRepository: Repository, 20 | @InjectRepository(Message) 21 | private readonly messageRepository: Repository, 22 | private readonly userService: UserService, 23 | ) {} 24 | 25 | async findAll() { 26 | const rooms = await this.roomRepository.find({ relations: ['messages'] }); 27 | 28 | return rooms; 29 | } 30 | 31 | async findOne(id: string) { 32 | const room = await this.roomRepository.findOne(id); 33 | 34 | if (!room) { 35 | throw new NotFoundException(`There is no room under id ${id}`); 36 | } 37 | 38 | return room; 39 | } 40 | 41 | async findOneWithRelations(id: string) { 42 | const room = await this.roomRepository.findOne(id, { 43 | relations: ['messages', 'users', 'bannedUsers'], 44 | }); 45 | 46 | if (!room) { 47 | throw new NotFoundException(`There is no room under id ${id}`); 48 | } 49 | 50 | return room; 51 | } 52 | 53 | async findOneByName(name: string) { 54 | const room = await this.roomRepository.findOne({ name }); 55 | 56 | return room; 57 | } 58 | 59 | async create(createRoomDto: CreateRoomDto) { 60 | const room = await this.roomRepository.create({ 61 | ...createRoomDto, 62 | }); 63 | 64 | return this.roomRepository.save(room); 65 | } 66 | 67 | async addMessage(addMessageDto: AddMessageDto) { 68 | const { roomId, userId, text } = addMessageDto; 69 | 70 | const room = await this.findOne(roomId); 71 | const user = await this.userService.findOne(userId); 72 | 73 | const message = await this.messageRepository.create({ 74 | text, 75 | room, 76 | user, 77 | }); 78 | 79 | return this.messageRepository.save(message); 80 | } 81 | 82 | async update(id: string, updateRoomDto: UpdateRoomDto) { 83 | const room = await this.roomRepository.preload({ 84 | id, 85 | ...updateRoomDto, 86 | }); 87 | 88 | if (!room) { 89 | throw new NotFoundException(`There is no room under id ${id}`); 90 | } 91 | 92 | return this.roomRepository.save(room); 93 | } 94 | 95 | async banUserFromRoom(banUserDto: BanUserDto) { 96 | const { userId, roomId } = banUserDto; 97 | 98 | const user = await this.userService.findOne(userId); 99 | const room = await this.findOne(roomId); 100 | 101 | await this.userService.updateUserRoom(userId, null); 102 | 103 | const bannedUsers = { ...room.bannedUsers, ...user }; 104 | const updatedRoom = await this.roomRepository.preload({ 105 | id: roomId, 106 | bannedUsers, 107 | }); 108 | 109 | return this.roomRepository.save(updatedRoom); 110 | } 111 | 112 | async remove(id: string) { 113 | const room = await this.findOne(id); 114 | 115 | return this.roomRepository.remove(room); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/chat/chat.gateway.ts: -------------------------------------------------------------------------------- 1 | import { ForbiddenException, UsePipes, ValidationPipe } from '@nestjs/common'; 2 | import { 3 | OnGatewayConnection, 4 | OnGatewayDisconnect, 5 | SubscribeMessage, 6 | WebSocketGateway, 7 | WebSocketServer, 8 | } from '@nestjs/websockets'; 9 | 10 | import { Socket } from 'socket.io'; 11 | 12 | import { UserService } from 'src/user/user.service'; 13 | import { AuthService } from 'src/auth/auth.service'; 14 | import { RoomService } from 'src/room/room.service'; 15 | 16 | import { AddMessageDto } from './dto/add-message.dto'; 17 | import { JoinRoomDto } from './dto/join-room.dto'; 18 | import { LeaveRoomDto } from './dto/leave-room.dto'; 19 | import { KickUserDto } from './dto/kick-user.dto'; 20 | import { BanUserDto } from './dto/ban-user.dto'; 21 | 22 | @UsePipes(new ValidationPipe()) 23 | @WebSocketGateway() 24 | export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { 25 | @WebSocketServer() 26 | server; 27 | 28 | connectedUsers: Map = new Map(); 29 | 30 | constructor( 31 | private readonly userService: UserService, 32 | private readonly authService: AuthService, 33 | private readonly roomService: RoomService, 34 | ) {} 35 | 36 | async handleConnection(client: Socket): Promise { 37 | const token = client.handshake.query.token.toString(); 38 | const payload = this.authService.verifyAccessToken(token); 39 | 40 | const user = payload && (await this.userService.findOne(payload.id)); 41 | const room = user?.room; 42 | 43 | if (!user) { 44 | client.disconnect(true); 45 | 46 | return; 47 | } 48 | 49 | this.connectedUsers.set(client.id, user.id); 50 | 51 | if (room) { 52 | return this.onRoomJoin(client, { roomId: room.id }); 53 | } 54 | } 55 | 56 | async handleDisconnect(client: Socket) { 57 | this.connectedUsers.delete(client.id); 58 | } 59 | 60 | @SubscribeMessage('message') 61 | async onMessage(client: Socket, addMessageDto: AddMessageDto) { 62 | const userId = this.connectedUsers.get(client.id); 63 | const user = await this.userService.findOne(userId); 64 | 65 | if (!user.room) { 66 | return; 67 | } 68 | 69 | addMessageDto.userId = userId; 70 | addMessageDto.roomId = user.room.id; 71 | 72 | await this.roomService.addMessage(addMessageDto); 73 | 74 | client.to(user.room.id).emit('message', addMessageDto.text); 75 | } 76 | 77 | @SubscribeMessage('join') 78 | async onRoomJoin(client: Socket, joinRoomDto: JoinRoomDto) { 79 | const { roomId } = joinRoomDto; 80 | const limit = 10; 81 | 82 | const room = await this.roomService.findOneWithRelations(roomId); 83 | 84 | if (!room) return; 85 | 86 | const userId = this.connectedUsers.get(client.id); 87 | const messages = room.messages.slice(limit * -1); 88 | 89 | await this.userService.updateUserRoom(userId, room); 90 | 91 | client.join(roomId); 92 | 93 | client.emit('message', messages); 94 | } 95 | 96 | @SubscribeMessage('leave') 97 | async onRoomLeave(client: Socket, leaveRoomDto: LeaveRoomDto) { 98 | const { roomId } = leaveRoomDto; 99 | const userId = this.connectedUsers.get(client.id); 100 | 101 | await this.userService.updateUserRoom(userId, null); 102 | 103 | client.leave(roomId); 104 | } 105 | 106 | @SubscribeMessage('user-kick') 107 | async onUserKick(client: Socket, kickUserDto: KickUserDto) { 108 | const { roomId, reason } = kickUserDto; 109 | 110 | const userId = this.connectedUsers.get(client.id); 111 | const room = await this.roomService.findOneWithRelations(roomId); 112 | 113 | if (userId !== room.ownerId) { 114 | throw new ForbiddenException(`You are not the owner of the room!`); 115 | } 116 | 117 | await this.userService.updateUserRoom(kickUserDto.userId, null); 118 | 119 | const kickedClient = this.getClientByUserId(kickUserDto.userId); 120 | 121 | if (!kickedClient) return; 122 | 123 | client.to(kickedClient.id).emit('kicked', reason); 124 | kickedClient.leave(roomId); 125 | } 126 | 127 | @SubscribeMessage('user-ban') 128 | async onUserBan(client: Socket, banUserDto: BanUserDto) { 129 | const { roomId, reason } = banUserDto; 130 | 131 | const userId = this.connectedUsers.get(client.id); 132 | const room = await this.roomService.findOneWithRelations(roomId); 133 | 134 | if (userId !== room.ownerId) { 135 | throw new ForbiddenException(`You are not the owner of the room!`); 136 | } 137 | 138 | if (userId === banUserDto.userId) { 139 | throw new ForbiddenException(`You can't ban yourself`); 140 | } 141 | 142 | await this.roomService.banUserFromRoom(banUserDto); 143 | 144 | const bannedClient = this.getClientByUserId(banUserDto.userId); 145 | 146 | if (!bannedClient) return; 147 | 148 | client.to(bannedClient.id).emit('banned', reason); 149 | bannedClient.leave(roomId); 150 | } 151 | 152 | private getClientByUserId(userId: string): Socket | null { 153 | for (const [key, value] of this.connectedUsers.entries()) { 154 | if (value === userId) { 155 | const kickedClient = this.server.sockets.sockets.get(key); 156 | 157 | return kickedClient; 158 | } 159 | } 160 | 161 | return null; 162 | } 163 | } 164 | --------------------------------------------------------------------------------