added basic auth, schemas and some ws event handling

This commit is contained in:
Suman991 2026-04-16 15:02:18 +05:30
parent c25480ded7
commit 538198840c
34 changed files with 1552 additions and 11 deletions

785
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -21,10 +21,21 @@
},
"dependencies": {
"@nestjs/common": "^11.0.1",
"@nestjs/config": "^4.0.4",
"@nestjs/core": "^11.0.1",
"@nestjs/jwt": "^11.0.2",
"@nestjs/mapped-types": "*",
"@nestjs/mongoose": "^11.0.4",
"@nestjs/platform-express": "^11.0.1",
"@nestjs/platform-socket.io": "^11.1.19",
"@nestjs/swagger": "^11.3.0",
"@nestjs/websockets": "^11.1.19",
"class-transformer": "^0.5.1",
"class-validator": "^0.15.1",
"mongoose": "^9.4.1",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1"
"rxjs": "^7.8.1",
"socket.io": "^4.8.3"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",

View File

@ -1,9 +1,32 @@
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UsersModule } from './users/users.module';
import { ConversationsModule } from './conversations/conversations.module';
import { MessagesModule } from './messages/messages.module';
import { ChatModule } from './chat/chat.module';
import { AuthModule } from './auth/auth.module';
import { MongooseModule } from '@nestjs/mongoose';
import { ConfigModule, ConfigService } from '@nestjs/config';
@Module({
imports: [],
imports: [
ConfigModule.forRoot({ isGlobal: true }), //access env anywhere
// connect db
MongooseModule.forRootAsync({
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
uri: configService.get<string>('DB_URI'),
}),
}),
UsersModule,
ConversationsModule,
MessagesModule,
ChatModule,
AuthModule,
],
controllers: [AppController],
providers: [AppService],
})

View File

@ -0,0 +1,38 @@
import { UsersService } from './../users/users.service';
import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';
import { AuthService } from './auth.service';
import { ApiOperation } from '@nestjs/swagger';
import { CreateUserDto } from 'src/users/dto/create-user.dto';
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService,
) {}
@Post('/token')
@ApiOperation({ summary: 'sign in', description: 'Authenticates a user Returns a JWT access token.' })
create(@Body() createUserDto: CreateUserDto ) {
return this.authService.create(createUserDto);
}
// @Get()
// findAll() {
// return this.authService.findAll();
// }
// @Get(':id')
// findOne(@Param('id') id: string) {
// return this.authService.findOne(+id);
// }
// @Patch(':id')
// update(@Param('id') id: string, @Body() updateAuthDto: UpdateAuthDto) {
// return this.authService.update(+id, updateAuthDto);
// }
// @Delete(':id')
// remove(@Param('id') id: string) {
// return this.authService.remove(+id);
// }
}

23
src/auth/auth.module.ts Normal file
View File

@ -0,0 +1,23 @@
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { UsersService } from 'src/users/users.service';
import { JwtModule, JwtService } from '@nestjs/jwt';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { UsersModule } from 'src/users/users.module';
@Module({
imports:[UsersModule,
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
secret: config.get('JWT_SECRET'),
signOptions: { expiresIn: config.get('JWT_EXPIRES_IN', '7') },
}),
}),
],
controllers: [AuthController],
providers: [AuthService],
})
export class AuthModule {}

45
src/auth/auth.service.ts Normal file
View File

@ -0,0 +1,45 @@
import { CreateUserDto } from './../users/dto/create-user.dto';
import { User } from './../users/schemas/user.schema';
import { UsersService } from './../users/users.service';
import { Injectable } from '@nestjs/common';
import { UpdateAuthDto } from './dto/update-auth.dto';
import { JwtService } from '@nestjs/jwt';
@Injectable()
export class AuthService {
constructor(
private readonly usersService: UsersService,
private readonly jwtService: JwtService,
) {}
async create(createUserDto: CreateUserDto) {
// check if user exist
let user=await this.usersService.findOne(createUserDto.name)
if(!user){
user = await this.usersService.create(createUserDto);
}
const token = this.jwtService.sign({
// sub: user.name.toLowerCase().replace(/\s+/g, '-'), // as name is unique
sub:user._id, // mongodb id
username: user.name,
});
return { token, username: user.name };
}
findAll() {
return `This action returns all auth`;
}
findOne(id: number) {
return `This action returns a #${id} auth`;
}
update(id: number, updateAuthDto: UpdateAuthDto) {
return `This action updates a #${id} auth`;
}
remove(id: number) {
return `This action removes a #${id} auth`;
}
}

View File

@ -0,0 +1 @@
export class CreateAuthDto {}

View File

@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/mapped-types';
import { CreateAuthDto } from './create-auth.dto';
export class UpdateAuthDto extends PartialType(CreateAuthDto) {}

184
src/chat/chat.gateway.ts Normal file
View File

@ -0,0 +1,184 @@
import {
WebSocketGateway,
SubscribeMessage,
MessageBody,
OnGatewayInit,
OnGatewayConnection,
OnGatewayDisconnect,
WebSocketServer,
WsException,
ConnectedSocket,
} from '@nestjs/websockets';
import { ChatService } from './chat.service';
import { Server, Socket } from 'socket.io';
import { Logger, UseGuards } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { JoinLeaveRoomDto } from './dto/join-leave-room.dto';
import { WsJwtGuard } from './guards/ws-jwt.guard';
import { RoomMessageDto } from './dto/room-message.dto';
@WebSocketGateway({
namespace: '/chat',
cors: { origin: '*' },
})
export class ChatGateway
implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect
{
constructor(
private readonly chatService: ChatService,
private readonly jwtService: JwtService,
) {}
@WebSocketServer()
server!: Server;
private readonly logger = new Logger(ChatGateway.name);
private users = new Map<string, { userId: string; username: string }>(); // {socketId:{userId:'12rt3',username:'suman'}}
afterInit(server: Server) {
this.logger.log('/chat namespace initialized');
// server.setMaxListeners(20);
}
handleConnection(client: Socket) {
try {
// st-1: Extract token
const token =
client.handshake.auth?.token || client.handshake.headers?.authorization; // fallback
if (!token) {
throw new WsException('No token provided');
}
// strip 'Bearer' prefix if present
const rawToken = token.replace(/^Bearer\s+/i, '');
// st-2: verify jwt
const payload = this.jwtService.verify(rawToken);
// st-3:attach user to client
client.data.user = {
id: payload.sub, // mongo id
username: payload.username,
};
// in memeory
this.users.set(client.id, {
userId: payload.sub,
username: payload.username,
});
this.logger.log(`[+] Authenticated: ${payload.username} (${client.id})`);
// tell the new client their own info
client.emit('connected', {
YourId: client.id,
username: payload.username,
message: `welcome, ${payload.username}`,
});
// tell everyone else someone joined
client.broadcast.emit('serverNotice', `[+] ${payload.username} joined`);
} catch (err) {
// st-4: Disconnect unauthorized clients
this.logger.warn(`[!] Rejected connection: ${err}`);
client.emit('authError', {
mesage: err || 'Unauthorized',
});
client.disconnect();
}
}
handleDisconnect(client: Socket) {
const user = this.users.get(client.id);
if (user) {
this.users.delete(client.id);
this.logger.log(`[-] Disconnected: ${user.username} (${client.id})`);
this.server.emit('serverNotice', `[-] ${user.username} left`);
}
}
@UseGuards(WsJwtGuard)
@SubscribeMessage('joinRoom')
handleJoinRoom(
@MessageBody() body: JoinLeaveRoomDto, //roomId
@ConnectedSocket() client: Socket,
) {
// extract user
const username=client.data.user.username;
// join user in the room(front-end will provide roomId(standard appraoch))
client.join(body.roomId)
// tell the joiner he successfully joined
client.emit("roomJoined",{
roomId:body.roomId,
message:`You joined room: ${body.roomId}`,
})
// also tell others in the room except the new joiner
client.to(body.roomId).emit("roomNotice",{
roomId: body.roomId,
message: `[+] ${username} joined the room`,
})
// return acknowledgement
return { success: true, roomId: body.roomId };
}
@UseGuards(WsJwtGuard)
@SubscribeMessage('leaveRoom')
handleLeaveRoom(
@MessageBody() body: JoinLeaveRoomDto, //roomId
@ConnectedSocket() client: Socket,
) {
// extract user
const username=client.data.user.username;
// remove the user from the room
client.leave(body.roomId)
// confirm to the leaver
client.emit('roomLeft', { roomId: body.roomId });
// Notify remaining memebers only
client.to(body.roomId).emit('roomNotice', {
roomId: body.roomId,
message: `[-] ${username} left the room`,
});
return { success: true, roomId: body.roomId };
}
@UseGuards(WsJwtGuard)
@SubscribeMessage('roomMessage')
handleRoomMessage(
@MessageBody() body:RoomMessageDto, //text:string
@ConnectedSocket() sender:Socket
){
// extract user
const username=sender.data.user.username
// Check if user is in room
const isMember = sender.rooms.has(body.roomId);
if (!isMember) {
sender.emit('roomError', {
message: 'You are not allowed',
});
return;
}
this.logger.log(`[ROOM:${body.roomId}] ${username}: ${body.text}`);
// send everone in the room(including the sender)
this.server.to(body.roomId).emit('roomMessage', {
roomId: body.roomId,
senderId: sender.id,
username,
text: body.text,
});
}
}

20
src/chat/chat.module.ts Normal file
View File

@ -0,0 +1,20 @@
import { Module } from '@nestjs/common';
import { ChatService } from './chat.service';
import { ChatGateway } from './chat.gateway';
import { JwtModule } from '@nestjs/jwt';
import { ConfigModule, ConfigService } from '@nestjs/config';
@Module({
imports: [
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
secret: config.get('JWT_SECRET'),
signOptions: { expiresIn: config.get('JWT_EXPIRES_IN', '7') },
}),
}),
],
providers: [ChatGateway, ChatService],
})
export class ChatModule {}

6
src/chat/chat.service.ts Normal file
View File

@ -0,0 +1,6 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class ChatService {
}

View File

@ -0,0 +1,9 @@
import { IsNotEmpty, IsString, MaxLength, MinLength } from 'class-validator';
export class ChatMessageDto {
@IsString({ message: 'text must be a string' })
@IsNotEmpty({ message: 'text cannot be empty' })
@MinLength(1, { message: 'Message is too short' })
@MaxLength(500, { message: 'Message cannot exceed 500 characters' })
text!: string;
}

View File

@ -0,0 +1,12 @@
import { IsNotEmpty, IsString, Matches, MaxLength } from "class-validator";
export class JoinLeaveRoomDto {
@IsString()
@IsNotEmpty({ message: 'roomId cannot be empty' })
@MaxLength(50, { message: 'Room name too long (max 50 chars)' })
@Matches(/^[a-z0-9-_]+$/, {
message:
'roomId can only contain lowercase letters, numbers, hyphens and underscores',
})
roomId!: string;
}

View File

@ -0,0 +1,13 @@
import { IsNotEmpty, IsString, Matches, MaxLength, MinLength } from "class-validator";
export class RoomMessageDto {
@IsString()
@IsNotEmpty()
@Matches(/^[a-z0-9-_]+$/, { message: 'Invalid roomId format' })
roomId!: string;
@IsString()
@IsNotEmpty({ message: 'Message cannot be empty' })
@MinLength(1)
@MaxLength(500, { message: 'Message cannot exceed 500 characters' })
text!: string;
}

View File

@ -0,0 +1,20 @@
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { WsException } from '@nestjs/websockets';
import { Socket } from 'socket.io';
@Injectable()
export class WsJwtGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const client: Socket = context.switchToWs().getClient<Socket>();
// client.data.user was set in handleConnection() after JWT verification
const user = client.data?.user;
if (!user) {
throw new WsException('Unauthorized — no authenticated user on socket');
}
// optionally check role
return true;
}
}

View File

@ -0,0 +1,34 @@
import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';
import { ConversationsService } from './conversations.service';
import { CreateConversationDto } from './dto/create-conversation.dto';
import { UpdateConversationDto } from './dto/update-conversation.dto';
@Controller('conversations')
export class ConversationsController {
constructor(private readonly conversationsService: ConversationsService) {}
@Post()
create(@Body() createConversationDto: CreateConversationDto) {
return this.conversationsService.create(createConversationDto);
}
@Get()
findAll() {
return this.conversationsService.findAll();
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.conversationsService.findOne(+id);
}
@Patch(':id')
update(@Param('id') id: string, @Body() updateConversationDto: UpdateConversationDto) {
return this.conversationsService.update(+id, updateConversationDto);
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.conversationsService.remove(+id);
}
}

View File

@ -0,0 +1,18 @@
import { Module } from '@nestjs/common';
import { ConversationsService } from './conversations.service';
import { ConversationsController } from './conversations.controller';
import { MongooseModule } from '@nestjs/mongoose';
import { Conversation, ConversationSchema } from './schemas/conversation.schema';
@Module({
imports:[
MongooseModule.forFeature([
{name:Conversation.name,schema:ConversationSchema},
])
],
controllers: [ConversationsController],
providers: [ConversationsService],
exports:[ConversationsService]
})
export class ConversationsModule {}

View File

@ -0,0 +1,34 @@
import { Injectable } from '@nestjs/common';
import { CreateConversationDto } from './dto/create-conversation.dto';
import { UpdateConversationDto } from './dto/update-conversation.dto';
import { Conversation, ConversationDocument } from './schemas/conversation.schema';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
@Injectable()
export class ConversationsService {
constructor(
@InjectModel(Conversation.name) private userModel: Model<ConversationDocument>,
) {}
create(createConversationDto: CreateConversationDto) {
return 'This action adds a new conversation';
}
findAll() {
return `This action returns all conversations`;
}
findOne(id: number) {
return `This action returns a #${id} conversation`;
}
update(id: number, updateConversationDto: UpdateConversationDto) {
return `This action updates a #${id} conversation`;
}
remove(id: number) {
return `This action removes a #${id} conversation`;
}
}

View File

@ -0,0 +1 @@
export class CreateConversationDto {}

View File

@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/mapped-types';
import { CreateConversationDto } from './create-conversation.dto';
export class UpdateConversationDto extends PartialType(CreateConversationDto) {}

View File

@ -0,0 +1,27 @@
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
export type ConversationDocument = Conversation & Document;
export enum ConversationType{
P2P='p2p',
GROUP='group'
}
@Schema({ timestamps: true })
export class Conversation {
@Prop({
type:String,
enum:ConversationType,
required:true
})
type!:ConversationType
@Prop({type:[String], required:true})
participants!:string[]
@Prop({required:false,default:null})
name!:string // for p2p no name
createdAt!:Date
}
export const ConversationSchema = SchemaFactory.createForClass(Conversation);

View File

@ -1,8 +1,39 @@
import { ConfigService } from '@nestjs/config';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { ValidationPipe } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(process.env.PORT ?? 3000);
app.enableCors({ origin: '*' });
app.useGlobalPipes(
new ValidationPipe({
whitelist:true, // strips unknown properties from @Body()
forbidNonWhitelisted:true, // throws 400 if unknown props are sent
transform:true // converts query string numbers/booleans from strings → correct types
})
)
const config = new DocumentBuilder()
.setTitle('Chat app')
.setDescription('The chat API description')
.setVersion('1.0')
// .addBearerAuth(
// {
// type: 'http',
// scheme: 'bearer',
// bearerFormat: 'JWT',
// in: 'header',
// },
// 'jwt',
// )
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api', app, document);
const configService=app.get(ConfigService)
const port=configService.get<number>('PORT')|| 3000;
await app.listen(port);
}
bootstrap();

View File

@ -0,0 +1 @@
export class CreateMessageDto {}

View File

@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/mapped-types';
import { CreateMessageDto } from './create-message.dto';
export class UpdateMessageDto extends PartialType(CreateMessageDto) {}

View File

@ -0,0 +1,34 @@
import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';
import { MessagesService } from './messages.service';
import { CreateMessageDto } from './dto/create-message.dto';
import { UpdateMessageDto } from './dto/update-message.dto';
@Controller('messages')
export class MessagesController {
constructor(private readonly messagesService: MessagesService) {}
@Post()
create(@Body() createMessageDto: CreateMessageDto) {
return this.messagesService.create(createMessageDto);
}
@Get()
findAll() {
return this.messagesService.findAll();
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.messagesService.findOne(+id);
}
@Patch(':id')
update(@Param('id') id: string, @Body() updateMessageDto: UpdateMessageDto) {
return this.messagesService.update(+id, updateMessageDto);
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.messagesService.remove(+id);
}
}

View File

@ -0,0 +1,17 @@
import { Module } from '@nestjs/common';
import { MessagesService } from './messages.service';
import { MessagesController } from './messages.controller';
import { MongooseModule } from '@nestjs/mongoose';
import { Message, MessageSchema } from './schemas/message.schema';
@Module({
imports:[
MongooseModule.forFeature([
{name:Message.name,schema:MessageSchema},
])
],
controllers: [MessagesController],
providers: [MessagesService],
exports:[MessagesService]
})
export class MessagesModule {}

View File

@ -0,0 +1,30 @@
import { Injectable } from '@nestjs/common';
import { CreateMessageDto } from './dto/create-message.dto';
import { UpdateMessageDto } from './dto/update-message.dto';
import { InjectModel } from '@nestjs/mongoose';
import { Message, MessageDocument } from './schemas/message.schema';
import { Model } from 'mongoose';
@Injectable()
export class MessagesService {
constructor( @InjectModel(Message.name) private MessageModel: Model<MessageDocument>,){}
create(createMessageDto: CreateMessageDto) {
return 'This action adds a new message';
}
findAll() {
return `This action returns all messages`;
}
findOne(id: number) {
return `This action returns a #${id} message`;
}
update(id: number, updateMessageDto: UpdateMessageDto) {
return `This action updates a #${id} message`;
}
remove(id: number) {
return `This action removes a #${id} message`;
}
}

View File

@ -0,0 +1,19 @@
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
export type MessageDocument = Message & Document;
@Schema({timestamps:true})
export class Message {
@Prop({ required:true})
conversationId!:string
@Prop({ required:true})
senderId!:string
@Prop({ required:false})
text!:string
createdAt!:Date
}
export const MessageSchema = SchemaFactory.createForClass(Message);

View File

@ -0,0 +1,9 @@
import {IsNotEmpty, IsString } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class CreateUserDto {
@ApiProperty({ example: 'Jane Doe', description: 'Full name of the user' })
@IsNotEmpty()
@IsString()
name!: string;
}

View File

@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/mapped-types';
import { CreateUserDto } from './create-user.dto';
export class UpdateUserDto extends PartialType(CreateUserDto) {}

View File

@ -0,0 +1,11 @@
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import { Document } from 'mongoose';
export type UserDocument = User & Document;
@Schema({timestamps:true})
export class User {
@Prop({required:true, unique:true})
name!:string
}
export const UserSchema = SchemaFactory.createForClass(User);

View File

@ -0,0 +1,34 @@
import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
// @Post()
// create(@Body() createUserDto: CreateUserDto) {
// return this.usersService.create(createUserDto);
// }
@Get()
findAll() {
return this.usersService.findAll();
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.usersService.findOne(id);
}
// @Patch(':id')
// update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
// return this.usersService.update(+id, updateUserDto);
// }
// @Delete(':id')
// remove(@Param('id') id: string) {
// return this.usersService.remove(+id);
// }
}

18
src/users/users.module.ts Normal file
View File

@ -0,0 +1,18 @@
import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
import { MongooseModule } from '@nestjs/mongoose';
import { User, UserSchema } from './schemas/user.schema';
@Module({
imports:[
MongooseModule.forFeature([
{name:User.name,schema:UserSchema},
])
],
controllers: [UsersController],
providers: [UsersService],
exports:[UsersService]
})
export class UsersModule {}

View File

@ -0,0 +1,33 @@
import { Injectable } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { User, UserDocument } from './schemas/user.schema';
import { Model } from 'mongoose';
import { InjectModel } from '@nestjs/mongoose';
@Injectable()
export class UsersService {
constructor(@InjectModel(User.name) private userModel:Model<UserDocument>){
}
async create(createUserDto: CreateUserDto):Promise<UserDocument> {
return await this.userModel.create(createUserDto)
}
findAll() {
return `This action returns all users`;
}
async findOne(name: string):Promise<UserDocument | null> {
return await this.userModel.findOne({name}).exec()
}
// update(id: number, updateUserDto: UpdateUserDto) {
// return `This action updates a #${id} user`;
// }
// remove(id: number) {
// return `This action removes a #${id} user`;
// }
}