Comprehensive Documentation for a Chat Application
Introduction
- Purpose: Outline the purpose of the document.
- Scope: Define the scope of the chat application.
- Technologies Used: List Next.js, NestJS, TailwindCSS, REST API, WebSocket, MongoDB.
Project Structure
- Frontend: Overview of the Next.js project structure.
- Backend: Overview of the NestJS project structure.
- Database: Structure of MongoDB collections (User, Chat, Message).
Sections and Functionality
1. User Authentication
-
User Registration:
- Endpoint:
/auth/register
- Method:
POST
- Payload:
{ "username": "string", "password": "string" }
- Description: Registers a new user.
- Endpoint:
-
User Login:
- Endpoint:
/auth/login
- Method:
POST
- Payload:
{ "username": "string", "password": "string" }
- Description: Authenticates a user and returns a JWT token.
- Endpoint:
-
User Logout:
- Description: Logout mechanism (typically handled on the client side by destroying the JWT token).
2. User Management
-
Profile Management:
- Endpoint:
/users/me
- Method:
GET
- Description: Fetches the logged-in user's profile.
- Endpoint:
-
Update Profile:
- Endpoint:
/users/me
- Method:
PUT
- Payload:
{ "username": "string", "password": "string" }
- Description: Updates the logged-in user's profile information.
- Endpoint:
3. Chat Management
-
Create Chat:
- Endpoint:
/chats
- Method:
POST
- Payload:
{ "participants": ["userId1", "userId2"] }
- Description: Creates a new chat session between users.
- Endpoint:
-
Fetch Chats:
- Endpoint:
/chats
- Method:
GET
- Description: Fetches all chat sessions for the logged-in user.
- Endpoint:
-
Fetch Chat Details:
- Endpoint:
/chats/:chatId
- Method:
GET
- Description: Fetches messages in a specific chat session.
- Endpoint:
4. Messaging
-
Send Message:
- Endpoint:
/chats/:chatId/messages
- Method:
POST
- Payload:
{ "content": "string" }
- Description: Sends a new message in a chat session.
- Endpoint:
-
Receive Messages:
- Description: Real-time message receiving using WebSocket.
- WebSocket Event:
receiveMessage
5. Real-time Communication
-
WebSocket Setup:
- Description: Initializing WebSocket connection on the client-side and handling events.
-
WebSocket Events:
- sendMessage: Event to send a message.
- receiveMessage: Event to receive messages.
6. User Interface
-
Login Page:
- Description: UI for user login.
- Components: Form, Input Fields, Submit Button.
-
Registration Page:
- Description: UI for user registration.
- Components: Form, Input Fields, Submit Button.
-
Chat List Page:
- Description: UI for displaying the list of chat sessions.
- Components: List of Chats, Search Bar.
-
Chat Window:
- Description: UI for displaying chat messages and sending new messages.
- Components: Message List, Input Field, Send Button.
7. Notifications
-
Real-time Notifications:
- Description: Display real-time notifications for new messages.
8. File Sharing
-
Upload File:
- Endpoint:
/chats/:chatId/files
- Method:
POST
- Payload:
{ "file": "file object" }
- Description: Uploads a file to a chat session.
- Endpoint:
-
Download File:
- Endpoint:
/chats/:chatId/files/:fileId
- Method:
GET
- Description: Downloads a file from a chat session.
- Endpoint:
9. Settings
-
User Settings:
- Description: Page for user settings (e.g., notification preferences, account management).
10. Deployment
-
Frontend Deployment:
- Description: Steps to deploy the Next.js app using Vercel.
-
Backend Deployment:
- Description: Steps to deploy the NestJS app using Heroku, DigitalOcean, or AWS.
11. Security
-
JWT Authentication:
- Description: Implementing JWT for user authentication.
-
Data Encryption:
- Description: Encrypting sensitive data (e.g., passwords).
12. Testing
-
Unit Testing:
- Description: Writing unit tests for both frontend and backend.
-
Integration Testing:
- Description: Writing integration tests to test API endpoints.
13. Performance Optimization
-
Frontend Optimization:
- Description: Techniques for optimizing the Next.js application (e.g., code splitting, lazy loading).
-
Backend Optimization:
- Description: Techniques for optimizing the NestJS application (e.g., caching, database indexing).
14. Documentation
-
API Documentation:
- Description: Detailed documentation of all API endpoints using tools like Swagger.
-
User Guide:
- Description: Guide for end-users on how to use the chat application.
15. Future Enhancements
-
Voice and Video Calls:
- Description: Adding support for voice and video calls.
-
Group Chats:
- Description: Adding support for group chat functionality.
-
Status Indicators:
- Description: Adding online/offline status indicators for users.
This comprehensive breakdown covers the essential sections and functionalities needed to create a chat application similar to WhatsApp or Telegram using Next.js, NestJS, and TailwindCSS.
Creating a chat application like WhatsApp or Telegram involves a comprehensive set of features and technologies. Below is a detailed breakdown to help you get started with building a chat app using Next.js for the frontend, NestJS for the backend, TailwindCSS for styling, and REST APIs for communication.
1. Project Setup
Frontend (Next.js)
- Initialize Next.js:
npx create-next-app@latest chat-app
cd chat-app
- Install TailwindCSS:
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
-
Configure TailwindCSS: Update
tailwind.config.js
andglobals.css
.
// tailwind.config.js
module.exports = {
content: [
"./pages/**/*.{js,ts,jsx,tsx}",
"./components/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
};
/* globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
Backend (NestJS)
- Initialize NestJS:
npm i -g @nestjs/cli
nest new chat-backend
cd chat-backend
- Install Required Modules:
npm install @nestjs/mongoose mongoose @nestjs/passport passport passport-local bcryptjs
npm install --save-dev @types/passport-local
2. Database Schema and Models
- Define User and Chat Models: Use Mongoose for schema definition.
User Schema
import { Schema } from 'mongoose';
export const UserSchema = new Schema({
username: { type: String, required: true, unique: true },
password: { type: String, required: true },
});
Chat Schema
import { Schema } from 'mongoose';
export const ChatSchema = new Schema({
participants: [{ type: Schema.Types.ObjectId, ref: 'User' }],
messages: [
{
sender: { type: Schema.Types.ObjectId, ref: 'User' },
content: { type: String, required: true },
timestamp: { type: Date, default: Date.now },
},
],
});
3. Authentication
- Local Strategy for Authentication: Use Passport.js for authentication.
Local Strategy
import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthService } from './auth.service';
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private authService: AuthService) {
super();
}
async validate(username: string, password: string): Promise<any> {
const user = await this.authService.validateUser(username, password);
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}
4. REST API Endpoints
- User Registration and Login: Implement endpoints for user registration and login.
Auth Controller
import { Controller, Request, Post, UseGuards } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LocalAuthGuard } from './local-auth.guard';
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@Post('register')
async register(@Request() req) {
return this.authService.register(req.body);
}
@UseGuards(LocalAuthGuard)
@Post('login')
async login(@Request() req) {
return this.authService.login(req.user);
}
}
5. Chat Functionality
- Create and Fetch Chats: Implement endpoints for creating and fetching chats and messages.
Chat Controller
import { Controller, Post, Get, Param, Body } from '@nestjs/common';
import { ChatService } from './chat.service';
@Controller('chats')
export class ChatController {
constructor(private chatService: ChatService) {}
@Post()
async createChat(@Body() createChatDto: CreateChatDto) {
return this.chatService.createChat(createChatDto);
}
@Get(':chatId')
async getChat(@Param('chatId') chatId: string) {
return this.chatService.getChat(chatId);
}
@Post(':chatId/messages')
async sendMessage(@Param('chatId') chatId: string, @Body() sendMessageDto: SendMessageDto) {
return this.chatService.sendMessage(chatId, sendMessageDto);
}
}
6. Real-time Communication
- WebSocket for Real-Time: Integrate WebSocket for real-time messaging.
Install Dependencies
npm install @nestjs/websockets @nestjs/platform-socket.io
WebSocket Gateway
import {
SubscribeMessage,
WebSocketGateway,
OnGatewayInit,
WebSocketServer,
OnGatewayConnection,
OnGatewayDisconnect,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
@WebSocketGateway()
export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect {
@WebSocketServer() server: Server;
handleConnection(client: Socket, ...args: any[]) {
console.log(`Client connected: ${client.id}`);
}
handleDisconnect(client: Socket) {
console.log(`Client disconnected: ${client.id}`);
}
@SubscribeMessage('sendMessage')
handleMessage(client: Socket, payload: any): void {
this.server.emit('receiveMessage', payload);
}
}
7. Frontend Integration
- Real-Time Messaging with Socket.io: Use Socket.io on the frontend for real-time updates.
Install Socket.io Client
npm install socket.io-client
Frontend Integration
import { useEffect, useState } from 'react';
import io from 'socket.io-client';
const socket = io('http://localhost:3000');
export default function Chat() {
const [messages, setMessages] = useState([]);
const [input, setInput] = useState('');
useEffect(() => {
socket.on('receiveMessage', (message) => {
setMessages((prevMessages) => [...prevMessages, message]);
});
}, []);
const sendMessage = () => {
socket.emit('sendMessage', input);
setInput('');
};
return (
<div className="chat-container">
<div className="messages">
{messages.map((msg, index) => (
<div key={index}>{msg}</div>
))}
</div>
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => (e.key === 'Enter' ? sendMessage() : null)}
/>
<button onClick={sendMessage}>Send</button>
</div>
);
}
8. Styling with TailwindCSS
- TailwindCSS for UI: Style the chat interface using TailwindCSS.
Example Styles
// Example chat component with TailwindCSS classes
export default function Chat() {
// ... (useState and useEffect hooks)
return (
<div className="flex flex-col h-screen">
<div className="flex-1 overflow-y-auto p-4">
{messages.map((msg, index) => (
<div key={index} className="bg-gray-200 p-2 my-2 rounded">
{msg}
</div>
))}
</div>
<div className="p-4 border-t border-gray-300 flex">
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
className="flex-1 p-2 border rounded"
onKeyDown={(e) => (e.key === 'Enter' ? sendMessage() : null)}
/>
<button
onClick={sendMessage}
className="ml-2 bg-blue-500 text-white p-2 rounded"
>
Send
</button>
</div>
</div>
);
}
9. Deployment and Hosting
- Frontend Deployment: Use Vercel for deploying the Next.js app.
- Backend Deployment: Use services like Heroku, DigitalOcean, or AWS for deploying the NestJS app.
By following this breakdown, you can build a comprehensive chat application with real-time messaging capabilities, user authentication, and a responsive UI. Adjust and expand upon these basics to include additional features like user profiles, file sharing, notifications, etc., as needed.
User Authentication: User Registration
Backend Code (NestJS)
-
Auth Module Setup:
- Generate the Auth Module:
nest generate module auth nest generate service auth nest generate controller auth
-
User Schema:
-
Create User Schema (
src/schemas/user.schema.ts
):
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { Document } from 'mongoose'; export type UserDocument = User & Document; @Schema() export class User { @Prop({ required: true, unique: true }) username: string; @Prop({ required: true }) password: string; } export const UserSchema = SchemaFactory.createForClass(User);
-
Create User Schema (
-
User DTO (Data Transfer Object):
-
Create DTO for User Registration (
src/auth/dto/register-user.dto.ts
):
export class RegisterUserDto { username: string; password: string; }
-
Create DTO for User Registration (
-
Auth Service:
-
Update the Auth Service (
src/auth/auth.service.ts
):
import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Model } from 'mongoose'; import { User, UserDocument } from '../schemas/user.schema'; import { RegisterUserDto } from './dto/register-user.dto'; import * as bcrypt from 'bcrypt'; @Injectable() export class AuthService { constructor(@InjectModel(User.name) private userModel: Model<UserDocument>) {} async register(registerUserDto: RegisterUserDto): Promise<User> { const { username, password } = registerUserDto; // Check if the user already exists const existingUser = await this.userModel.findOne({ username }).exec(); if (existingUser) { throw new Error('User already exists'); } // Hash the password const salt = await bcrypt.genSalt(); const hashedPassword = await bcrypt.hash(password, salt); // Create a new user const newUser = new this.userModel({ username, password: hashedPassword }); return newUser.save(); } }
-
Update the Auth Service (
-
Auth Controller:
-
Update the Auth Controller (
src/auth/auth.controller.ts
):
import { Controller, Post, Body } from '@nestjs/common'; import { AuthService } from './auth.service'; import { RegisterUserDto } from './dto/register-user.dto'; import { User } from '../schemas/user.schema'; @Controller('auth') export class AuthController { constructor(private readonly authService: AuthService) {} @Post('register') async register(@Body() registerUserDto: RegisterUserDto): Promise<User> { return this.authService.register(registerUserDto); } }
-
Update the Auth Controller (
-
Mongoose Setup:
-
Configure Mongoose in App Module (
src/app.module.ts
):
import { Module } from '@nestjs/common'; import { MongooseModule } from '@nestjs/mongoose'; import { AuthModule } from './auth/auth.module'; import { User, UserSchema } from './schemas/user.schema'; @Module({ imports: [ MongooseModule.forRoot('mongodb://localhost/chat-app'), MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]), AuthModule, ], }) export class AppModule {}
-
Configure Mongoose in App Module (
Frontend Code (Next.js)
-
API Call for Registration:
-
Create API Function (
src/services/api.js
):
import axios from 'axios'; const API_URL = 'http://localhost:3000/auth'; export const register = async (username, password) => { try { const response = await axios.post(`${API_URL}/register`, { username, password }); return response.data; } catch (error) { throw error.response.data; } };
-
Create API Function (
-
Registration Form:
-
Create Registration Form Component (
src/components/RegisterForm.js
):
import { useState } from 'react'; import { register } from '../services/api'; export default function RegisterForm() { const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [message, setMessage] = useState(''); const handleSubmit = async (e) => { e.preventDefault(); try { const data = await register(username, password); setMessage('User registered successfully'); } catch (error) { setMessage(`Error: ${error.message}`); } }; return ( <div className="max-w-md mx-auto mt-10"> <h2 className="text-2xl font-bold mb-5">Register</h2> <form onSubmit={handleSubmit}> <div className="mb-4"> <label className="block text-gray-700">Username</label> <input type="text" value={username} onChange={(e) => setUsername(e.target.value)} className="w-full px-3 py-2 border rounded" /> </div> <div className="mb-4"> <label className="block text-gray-700">Password</label> <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} className="w-full px-3 py-2 border rounded" /> </div> <button type="submit" className="bg-blue-500 text-white px-4 py-2 rounded"> Register </button> </form> {message && <p className="mt-4">{message}</p>} </div> ); }
-
Create Registration Form Component (
Conclusion
This setup includes the backend implementation for user registration with NestJS and a simple frontend registration form with Next.js. It uses MongoDB for data storage and bcrypt for password hashing. Adjust and expand this as needed for your complete chat application.
User Authentication: User Login and Logout
Backend Code (NestJS)
Auth Module Setup (continued from the previous setup)
-
Login DTO (Data Transfer Object):
-
Create DTO for User Login (
src/auth/dto/login-user.dto.ts
):
export class LoginUserDto { username: string; password: string; }
-
Create DTO for User Login (
-
JWT Module Setup:
- Install JWT Package:
npm install @nestjs/jwt passport-jwt npm install --save-dev @types/passport-jwt
-
Configure JWT in Auth Module (
src/auth/auth.module.ts
):
import { Module } from '@nestjs/common'; import { JwtModule } from '@nestjs/jwt'; import { PassportModule } from '@nestjs/passport'; import { AuthService } from './auth.service'; import { AuthController } from './auth.controller'; import { User, UserSchema } from '../schemas/user.schema'; import { MongooseModule } from '@nestjs/mongoose'; import { JwtStrategy } from './jwt.strategy'; @Module({ imports: [ MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]), PassportModule, JwtModule.register({ secret: 'YOUR_SECRET_KEY', // Replace with a secure key signOptions: { expiresIn: '1h' }, }), ], providers: [AuthService, JwtStrategy], controllers: [AuthController], }) export class AuthModule {}
-
Auth Service (continued):
-
Update Auth Service to Handle Login and JWT Generation (
src/auth/auth.service.ts
):
import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Model } from 'mongoose'; import { JwtService } from '@nestjs/jwt'; import { User, UserDocument } from '../schemas/user.schema'; import { RegisterUserDto } from './dto/register-user.dto'; import { LoginUserDto } from './dto/login-user.dto'; import * as bcrypt from 'bcrypt'; @Injectable() export class AuthService { constructor( @InjectModel(User.name) private userModel: Model<UserDocument>, private jwtService: JwtService, ) {} async register(registerUserDto: RegisterUserDto): Promise<User> { const { username, password } = registerUserDto; const existingUser = await this.userModel.findOne({ username }).exec(); if (existingUser) { throw new Error('User already exists'); } const salt = await bcrypt.genSalt(); const hashedPassword = await bcrypt.hash(password, salt); const newUser = new this.userModel({ username, password: hashedPassword }); return newUser.save(); } async validateUser(username: string, password: string): Promise<User> { const user = await this.userModel.findOne({ username }).exec(); if (!user) { return null; } const isPasswordValid = await bcrypt.compare(password, user.password); if (!isPasswordValid) { return null; } return user; } async login(loginUserDto: LoginUserDto): Promise<{ access_token: string }> { const { username, password } = loginUserDto; const user = await this.validateUser(username, password); if (!user) { throw new Error('Invalid credentials'); } const payload = { username: user.username, sub: user._id }; return { access_token: this.jwtService.sign(payload), }; } }
-
Update Auth Service to Handle Login and JWT Generation (
-
Auth Controller (continued):
-
Update Auth Controller for Login (
src/auth/auth.controller.ts
):
import { Controller, Post, Body } from '@nestjs/common'; import { AuthService } from './auth.service'; import { RegisterUserDto } from './dto/register-user.dto'; import { LoginUserDto } from './dto/login-user.dto'; import { User } from '../schemas/user.schema'; @Controller('auth') export class AuthController { constructor(private readonly authService: AuthService) {} @Post('register') async register(@Body() registerUserDto: RegisterUserDto): Promise<User> { return this.authService.register(registerUserDto); } @Post('login') async login(@Body() loginUserDto: LoginUserDto): Promise<{ access_token: string }> { return this.authService.login(loginUserDto); } }
-
Update Auth Controller for Login (
-
JWT Strategy:
-
Create JWT Strategy for Protecting Routes (
src/auth/jwt.strategy.ts
):
import { Strategy, ExtractJwt } from 'passport-jwt'; import { PassportStrategy } from '@nestjs/passport'; import { Injectable } from '@nestjs/common'; import { JwtPayload } from './jwt-payload.interface'; import { AuthService } from './auth.service'; @Injectable() export class JwtStrategy extends PassportStrategy(Strategy) { constructor(private authService: AuthService) { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), ignoreExpiration: false, secretOrKey: 'YOUR_SECRET_KEY', // Replace with a secure key }); } async validate(payload: JwtPayload) { return { userId: payload.sub, username: payload.username }; } }
-
Create JWT Strategy for Protecting Routes (
-
JWT Payload Interface:
-
Create JWT Payload Interface (
src/auth/jwt-payload.interface.ts
):
export interface JwtPayload { username: string; sub: string; }
-
Create JWT Payload Interface (
Frontend Code (Next.js)
-
API Call for Login:
-
Update API Function (
src/services/api.js
):
import axios from 'axios'; const API_URL = 'http://localhost:3000/auth'; export const register = async (username, password) => { try { const response = await axios.post(`${API_URL}/register`, { username, password }); return response.data; } catch (error) { throw error.response.data; } }; export const login = async (username, password) => { try { const response = await axios.post(`${API_URL}/login`, { username, password }); return response.data; } catch (error) { throw error.response.data; } };
-
Update API Function (
-
Login Form:
-
Create Login Form Component (
src/components/LoginForm.js
):
import { useState } from 'react'; import { login } from '../services/api'; export default function LoginForm() { const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [message, setMessage] = useState(''); const handleSubmit = async (e) => { e.preventDefault(); try { const data = await login(username, password); localStorage.setItem('token', data.access_token); setMessage('User logged in successfully'); } catch (error) { setMessage(`Error: ${error.message}`); } }; return ( <div className="max-w-md mx-auto mt-10"> <h2 className="text-2xl font-bold mb-5">Login</h2> <form onSubmit={handleSubmit}> <div className="mb-4"> <label className="block text-gray-700">Username</label> <input type="text" value={username} onChange={(e) => setUsername(e.target.value)} className="w-full px-3 py-2 border rounded" /> </div> <div className="mb-4"> <label className="block text-gray-700">Password</label> <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} className="w-full px-3 py-2 border rounded" /> </div> <button type="submit" className="bg-blue-500 text-white px-4 py-2 rounded"> Login </button> </form> {message && <p className="mt-4">{message}</p>} </div> ); }
-
Create Login Form Component (
-
Logout Function:
- Handle Logout on the Client Side:
export const logout = () => { localStorage.removeItem('token'); window.location.href = '/login'; // Redirect to login page };
-
Example Logout Button:
import { logout } from '../services/api'; export default function LogoutButton() { return ( <button onClick={logout} className="bg-red-500 text-white px-4 py-2 rounded"> Logout </button> ); }
Conclusion
This setup includes the backend implementation for user login with NestJS and a simple frontend login form with Next.js. The logout mechanism is handled on the client side by removing the JWT token from local storage. This setup provides the basic authentication flow for a chat application.
User Management: Profile Management
Backend Code (NestJS)
User Profile Endpoint
-
Create User Profile DTO:
-
Create DTO for User Profile (
src/auth/dto/user-profile.dto.ts
):
export class UserProfileDto { username: string; }
-
Create DTO for User Profile (
-
User Service:
-
Update User Service to Handle Profile Fetching (
src/auth/auth.service.ts
):
import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Model } from 'mongoose'; import { JwtService } from '@nestjs/jwt'; import { User, UserDocument } from '../schemas/user.schema'; import { RegisterUserDto } from './dto/register-user.dto'; import { LoginUserDto } from './dto/login-user.dto'; import { UserProfileDto } from './dto/user-profile.dto'; import * as bcrypt from 'bcrypt'; @Injectable() export class AuthService { constructor( @InjectModel(User.name) private userModel: Model<UserDocument>, private jwtService: JwtService, ) {} // ... other methods async getUserProfile(userId: string): Promise<UserProfileDto> { const user = await this.userModel.findById(userId).exec(); if (!user) { throw new Error('User not found'); } return { username: user.username }; } }
-
Update User Service to Handle Profile Fetching (
-
Auth Controller:
-
Update Auth Controller to Include Profile Fetching (
src/auth/auth.controller.ts
):
import { Controller, Post, Get, Request, Body, UseGuards } from '@nestjs/common'; import { AuthService } from './auth.service'; import { RegisterUserDto } from './dto/register-user.dto'; import { LoginUserDto } from './dto/login-user.dto'; import { User } from '../schemas/user.schema'; import { JwtAuthGuard } from './jwt-auth.guard'; @Controller('auth') export class AuthController { constructor(private readonly authService: AuthService) {} @Post('register') async register(@Body() registerUserDto: RegisterUserDto): Promise<User> { return this.authService.register(registerUserDto); } @Post('login') async login(@Body() loginUserDto: LoginUserDto): Promise<{ access_token: string }> { return this.authService.login(loginUserDto); } @UseGuards(JwtAuthGuard) @Get('me') async getProfile(@Request() req): Promise<{ username: string }> { return this.authService.getUserProfile(req.user.userId); } }
-
Update Auth Controller to Include Profile Fetching (
-
JWT Auth Guard:
-
Create JWT Auth Guard (
src/auth/jwt-auth.guard.ts
):
import { Injectable } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; @Injectable() export class JwtAuthGuard extends AuthGuard('jwt') {}
-
Create JWT Auth Guard (
-
Update JWT Strategy:
-
Ensure Payload Includes User ID (
src/auth/jwt.strategy.ts
):
import { Strategy, ExtractJwt } from 'passport-jwt'; import { PassportStrategy } from '@nestjs/passport'; import { Injectable } from '@nestjs/common'; import { JwtPayload } from './jwt-payload.interface'; import { AuthService } from './auth.service'; @Injectable() export class JwtStrategy extends PassportStrategy(Strategy) { constructor(private authService: AuthService) { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), ignoreExpiration: false, secretOrKey: 'YOUR_SECRET_KEY', // Replace with a secure key }); } async validate(payload: JwtPayload) { return { userId: payload.sub, username: payload.username }; } }
-
Ensure Payload Includes User ID (
Frontend Code (Next.js)
-
API Call for Fetching User Profile:
-
Create API Function for Fetching User Profile (
src/services/api.js
):
import axios from 'axios'; const API_URL = 'http://localhost:3000/auth'; export const register = async (username, password) => { try { const response = await axios.post(`${API_URL}/register`, { username, password }); return response.data; } catch (error) { throw error.response.data; } }; export const login = async (username, password) => { try { const response = await axios.post(`${API_URL}/login`, { username, password }); return response.data; } catch (error) { throw error.response.data; } }; export const getProfile = async (token) => { try { const response = await axios.get(`${API_URL}/me`, { headers: { Authorization: `Bearer ${token}`, }, }); return response.data; } catch (error) { throw error.response.data; } }; export const logout = () => { localStorage.removeItem('token'); window.location.href = '/login'; // Redirect to login page };
-
Create API Function for Fetching User Profile (
-
Profile Page:
-
Create Profile Page Component (
src/pages/profile.js
):
import { useEffect, useState } from 'react'; import { getProfile } from '../services/api'; export default function Profile() { const [profile, setProfile] = useState(null); const [error, setError] = useState(''); useEffect(() => { const fetchProfile = async () => { try { const token = localStorage.getItem('token'); if (token) { const data = await getProfile(token); setProfile(data); } else { setError('No token found'); } } catch (err) { setError(err.message); } }; fetchProfile(); }, []); if (error) { return <div className="text-red-500">{error}</div>; } return ( <div className="max-w-md mx-auto mt-10"> <h2 className="text-2xl font-bold mb-5">Profile</h2> {profile ? ( <div> <p><strong>Username:</strong> {profile.username}</p> </div> ) : ( <p>Loading...</p> )} </div> ); }
-
Create Profile Page Component (
Conclusion
This setup includes the backend implementation for fetching the logged-in user's profile with NestJS and a simple frontend profile page with Next.js. The profile endpoint is protected with JWT authentication, ensuring only authenticated users can access their profile information.
User Management: Update Profile
Backend Code (NestJS)
-
Update User Profile DTO:
-
Create DTO for Updating User Profile (
src/auth/dto/update-user.dto.ts
):
export class UpdateUserDto { username?: string; password?: string; }
-
Create DTO for Updating User Profile (
-
User Service:
-
Update User Service to Handle Profile Updating (
src/auth/auth.service.ts
):
import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Model } from 'mongoose'; import { JwtService } from '@nestjs/jwt'; import { User, UserDocument } from '../schemas/user.schema'; import { RegisterUserDto } from './dto/register-user.dto'; import { LoginUserDto } from './dto/login-user.dto'; import { UpdateUserDto } from './dto/update-user.dto'; import * as bcrypt from 'bcrypt'; @Injectable() export class AuthService { constructor( @InjectModel(User.name) private userModel: Model<UserDocument>, private jwtService: JwtService, ) {} // ... other methods async updateUserProfile(userId: string, updateUserDto: UpdateUserDto): Promise<User> { const user = await this.userModel.findById(userId).exec(); if (!user) { throw new Error('User not found'); } if (updateUserDto.username) { user.username = updateUserDto.username; } if (updateUserDto.password) { const salt = await bcrypt.genSalt(); user.password = await bcrypt.hash(updateUserDto.password, salt); } return user.save(); } }
-
Update User Service to Handle Profile Updating (
-
Auth Controller:
-
Update Auth Controller to Include Profile Updating (
src/auth/auth.controller.ts
):
import { Controller, Post, Get, Put, Request, Body, UseGuards } from '@nestjs/common'; import { AuthService } from './auth.service'; import { RegisterUserDto } from './dto/register-user.dto'; import { LoginUserDto } from './dto/login-user.dto'; import { UpdateUserDto } from './dto/update-user.dto'; import { User } from '../schemas/user.schema'; import { JwtAuthGuard } from './jwt-auth.guard'; @Controller('auth') export class AuthController { constructor(private readonly authService: AuthService) {} @Post('register') async register(@Body() registerUserDto: RegisterUserDto): Promise<User> { return this.authService.register(registerUserDto); } @Post('login') async login(@Body() loginUserDto: LoginUserDto): Promise<{ access_token: string }> { return this.authService.login(loginUserDto); } @UseGuards(JwtAuthGuard) @Get('me') async getProfile(@Request() req): Promise<{ username: string }> { return this.authService.getUserProfile(req.user.userId); } @UseGuards(JwtAuthGuard) @Put('me') async updateProfile(@Request() req, @Body() updateUserDto: UpdateUserDto): Promise<User> { return this.authService.updateUserProfile(req.user.userId, updateUserDto); } }
-
Update Auth Controller to Include Profile Updating (
JWT Auth Guard and Strategy (from previous steps).
Frontend Code (Next.js)
-
API Call for Updating User Profile:
-
Create API Function for Updating User Profile (
src/services/api.js
):
import axios from 'axios'; const API_URL = 'http://localhost:3000/auth'; export const register = async (username, password) => { try { const response = await axios.post(`${API_URL}/register`, { username, password }); return response.data; } catch (error) { throw error.response.data; } }; export const login = async (username, password) => { try { const response = await axios.post(`${API_URL}/login`, { username, password }); return response.data; } catch (error) { throw error.response.data; } }; export const getProfile = async (token) => { try { const response = await axios.get(`${API_URL}/me`, { headers: { Authorization: `Bearer ${token}`, }, }); return response.data; } catch (error) { throw error.response.data; } }; export const updateProfile = async (token, userData) => { try { const response = await axios.put(`${API_URL}/me`, userData, { headers: { Authorization: `Bearer ${token}`, }, }); return response.data; } catch (error) { throw error.response.data; } }; export const logout = () => { localStorage.removeItem('token'); window.location.href = '/login'; // Redirect to login page };
-
Create API Function for Updating User Profile (
-
Update Profile Form:
-
Create Update Profile Form Component (
src/components/UpdateProfileForm.js
):
import { useState, useEffect } from 'react'; import { getProfile, updateProfile } from '../services/api'; export default function UpdateProfileForm() { const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [message, setMessage] = useState(''); useEffect(() => { const fetchProfile = async () => { try { const token = localStorage.getItem('token'); if (token) { const data = await getProfile(token); setUsername(data.username); } } catch (err) { setMessage(`Error: ${err.message}`); } }; fetchProfile(); }, []); const handleSubmit = async (e) => { e.preventDefault(); try { const token = localStorage.getItem('token'); const userData = { username, password: password || undefined }; const data = await updateProfile(token, userData); setMessage('Profile updated successfully'); } catch (error) { setMessage(`Error: ${error.message}`); } }; return ( <div className="max-w-md mx-auto mt-10"> <h2 className="text-2xl font-bold mb-5">Update Profile</h2> <form onSubmit={handleSubmit}> <div className="mb-4"> <label className="block text-gray-700">Username</label> <input type="text" value={username} onChange={(e) => setUsername(e.target.value)} className="w-full px-3 py-2 border rounded" /> </div> <div className="mb-4"> <label className="block text-gray-700">Password</label> <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} className="w-full px-3 py-2 border rounded" placeholder="Leave blank to keep the same" /> </div> <button type="submit" className="bg-blue-500 text-white px-4 py-2 rounded"> Update </button> </form> {message && <p className="mt-4">{message}</p>} </div> ); }
-
Create Update Profile Form Component (
Conclusion
This setup includes the backend implementation for updating the logged-in user's profile with NestJS and a simple frontend update profile form with Next.js. The profile update endpoint is protected with JWT authentication, ensuring only authenticated users can update their profile information.
Chat Management: Create Chat
Backend Code (NestJS)
-
Chat Module Setup
- Generate the Chat Module:
nest generate module chat nest generate service chat nest generate controller chat
-
Chat Schema:
-
Create Chat Schema (
src/schemas/chat.schema.ts
):
import { Schema, Prop, SchemaFactory } from '@nestjs/mongoose'; import { Document, Types } from 'mongoose'; export type ChatDocument = Chat & Document; @Schema() export class Chat { @Prop({ type: [{ type: Types.ObjectId, ref: 'User' }], required: true }) participants: Types.ObjectId[]; @Prop({ type: [{ type: Object }] }) messages: { sender: Types.ObjectId; content: string; timestamp: Date }[]; } export const ChatSchema = SchemaFactory.createForClass(Chat);
-
Create Chat Schema (
-
Create Chat DTO:
-
Create DTO for Creating Chat (
src/chat/dto/create-chat.dto.ts
):
export class CreateChatDto { participants: string[]; }
-
Create DTO for Creating Chat (
-
Chat Service:
-
Update Chat Service to Handle Chat Creation (
src/chat/chat.service.ts
):
import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Model } from 'mongoose'; import { Chat, ChatDocument } from '../schemas/chat.schema'; import { CreateChatDto } from './dto/create-chat.dto'; @Injectable() export class ChatService { constructor(@InjectModel(Chat.name) private chatModel: Model<ChatDocument>) {} async createChat(createChatDto: CreateChatDto): Promise<Chat> { const newChat = new this.chatModel({ participants: createChatDto.participants, messages: [], }); return newChat.save(); } }
-
Update Chat Service to Handle Chat Creation (
-
Chat Controller:
-
Update Chat Controller to Include Chat Creation (
src/chat/chat.controller.ts
):
import { Controller, Post, Body, UseGuards } from '@nestjs/common'; import { ChatService } from './chat.service'; import { CreateChatDto } from './dto/create-chat.dto'; import { JwtAuthGuard } from '../auth/jwt-auth.guard'; import { Chat } from '../schemas/chat.schema'; @Controller('chats') export class ChatController { constructor(private readonly chatService: ChatService) {} @UseGuards(JwtAuthGuard) @Post() async createChat(@Body() createChatDto: CreateChatDto): Promise<Chat> { return this.chatService.createChat(createChatDto); } }
-
Update Chat Controller to Include Chat Creation (
JWT Auth Guard (from previous steps).
Frontend Code (Next.js)
-
API Call for Creating Chat:
-
Create API Function for Creating Chat (
src/services/api.js
):
import axios from 'axios'; const API_URL = 'http://localhost:3000'; export const register = async (username, password) => { try { const response = await axios.post(`${API_URL}/auth/register`, { username, password }); return response.data; } catch (error) { throw error.response.data; } }; export const login = async (username, password) => { try { const response = await axios.post(`${API_URL}/auth/login`, { username, password }); return response.data; } catch (error) { throw error.response.data; } }; export const getProfile = async (token) => { try { const response = await axios.get(`${API_URL}/auth/me`, { headers: { Authorization: `Bearer ${token}`, }, }); return response.data; } catch (error) { throw error.response.data; } }; export const updateProfile = async (token, userData) => { try { const response = await axios.put(`${API_URL}/auth/me`, userData, { headers: { Authorization: `Bearer ${token}`, }, }); return response.data; } catch (error) { throw error.response.data; } }; export const createChat = async (token, participants) => { try { const response = await axios.post(`${API_URL}/chats`, { participants }, { headers: { Authorization: `Bearer ${token}`, }, }); return response.data; } catch (error) { throw error.response.data; } }; export const logout = () => { localStorage.removeItem('token'); window.location.href = '/login'; // Redirect to login page };
-
Create API Function for Creating Chat (
-
Create Chat Form:
-
Create Chat Form Component (
src/components/CreateChatForm.js
):
import { useState } from 'react'; import { createChat } from '../services/api'; export default function CreateChatForm() { const [participants, setParticipants] = useState(''); const [message, setMessage] = useState(''); const handleSubmit = async (e) => { e.preventDefault(); try { const token = localStorage.getItem('token'); const participantIds = participants.split(',').map(id => id.trim()); const data = await createChat(token, participantIds); setMessage('Chat created successfully'); } catch (error) { setMessage(`Error: ${error.message}`); } }; return ( <div className="max-w-md mx-auto mt-10"> <h2 className="text-2xl font-bold mb-5">Create Chat</h2> <form onSubmit={handleSubmit}> <div className="mb-4"> <label className="block text-gray-700">Participants</label> <input type="text" value={participants} onChange={(e) => setParticipants(e.target.value)} className="w-full px-3 py-2 border rounded" placeholder="Comma-separated user IDs" /> </div> <button type="submit" className="bg-blue-500 text-white px-4 py-2 rounded"> Create Chat </button> </form> {message && <p className="mt-4">{message}</p>} </div> ); }
-
Create Chat Form Component (
Conclusion
This setup includes the backend implementation for creating a new chat session with NestJS and a simple frontend form for creating a chat with Next.js. The chat creation endpoint is protected with JWT authentication, ensuring only authenticated users can create new chat sessions.
Chat Management: Fetch Chats
Backend Code (NestJS)
-
Chat Service:
-
Update Chat Service to Handle Fetching Chats (
src/chat/chat.service.ts
):
import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Model } from 'mongoose'; import { Chat, ChatDocument } from '../schemas/chat.schema'; import { User, UserDocument } from '../schemas/user.schema'; @Injectable() export class ChatService { constructor( @InjectModel(Chat.name) private chatModel: Model<ChatDocument>, @InjectModel(User.name) private userModel: Model<UserDocument>, ) {} async createChat(participants: string[]): Promise<Chat> { const newChat = new this.chatModel({ participants: participants, messages: [], }); return newChat.save(); } async getChats(userId: string): Promise<Chat[]> { return this.chatModel.find({ participants: userId }).populate('participants', 'username').exec(); } }
-
Update Chat Service to Handle Fetching Chats (
-
Chat Controller:
-
Update Chat Controller to Include Fetching Chats (
src/chat/chat.controller.ts
):
import { Controller, Post, Get, Body, UseGuards, Request } from '@nestjs/common'; import { ChatService } from './chat.service'; import { CreateChatDto } from './dto/create-chat.dto'; import { JwtAuthGuard } from '../auth/jwt-auth.guard'; import { Chat } from '../schemas/chat.schema'; @Controller('chats') export class ChatController { constructor(private readonly chatService: ChatService) {} @UseGuards(JwtAuthGuard) @Post() async createChat(@Body() createChatDto: CreateChatDto): Promise<Chat> { return this.chatService.createChat(createChatDto.participants); } @UseGuards(JwtAuthGuard) @Get() async getChats(@Request() req): Promise<Chat[]> { return this.chatService.getChats(req.user.userId); } }
-
Update Chat Controller to Include Fetching Chats (
-
Ensure JWT Auth Guard is Applied:
- Ensure that
JwtAuthGuard
is properly set up and imported as shown in previous examples.
- Ensure that
Frontend Code (Next.js)
-
API Call for Fetching Chats:
-
Create API Function for Fetching Chats (
src/services/api.js
):
import axios from 'axios'; const API_URL = 'http://localhost:3000'; export const register = async (username, password) => { try { const response = await axios.post(`${API_URL}/auth/register`, { username, password }); return response.data; } catch (error) { throw error.response.data; } }; export const login = async (username, password) => { try { const response = await axios.post(`${API_URL}/auth/login`, { username, password }); return response.data; } catch (error) { throw error.response.data; } }; export const getProfile = async (token) => { try { const response = await axios.get(`${API_URL}/auth/me`, { headers: { Authorization: `Bearer ${token}`, }, }); return response.data; } catch (error) { throw error.response.data; } }; export const updateProfile = async (token, userData) => { try { const response = await axios.put(`${API_URL}/auth/me`, userData, { headers: { Authorization: `Bearer ${token}`, }, }); return response.data; } catch (error) { throw error.response.data; } }; export const createChat = async (token, participants) => { try { const response = await axios.post(`${API_URL}/chats`, { participants }, { headers: { Authorization: `Bearer ${token}`, }, }); return response.data; } catch (error) { throw error.response.data; } }; export const getChats = async (token) => { try { const response = await axios.get(`${API_URL}/chats`, { headers: { Authorization: `Bearer ${token}`, }, }); return response.data; } catch (error) { throw error.response.data; } }; export const logout = () => { localStorage.removeItem('token'); window.location.href = '/login'; // Redirect to login page };
-
Create API Function for Fetching Chats (
-
Chat List Page:
-
Create Chat List Component (
src/components/ChatList.js
):
import { useEffect, useState } from 'react'; import { getChats } from '../services/api'; export default function ChatList() { const [chats, setChats] = useState([]); const [error, setError] = useState(''); useEffect(() => { const fetchChats = async () => { try { const token = localStorage.getItem('token'); if (token) { const data = await getChats(token); setChats(data); } else { setError('No token found'); } } catch (err) { setError(err.message); } }; fetchChats(); }, []); if (error) { return <div className="text-red-500">{error}</div>; } return ( <div className="max-w-md mx-auto mt-10"> <h2 className="text-2xl font-bold mb-5">Chats</h2> {chats.length > 0 ? ( <ul> {chats.map((chat) => ( <li key={chat._id} className="mb-2 p-2 border rounded"> {chat.participants.map((participant) => participant.username).join(', ')} </li> ))} </ul> ) : ( <p>No chats available.</p> )} </div> ); }
-
Create Chat List Component (
Conclusion
This setup includes the backend implementation for fetching all chat sessions for the logged-in user with NestJS and a simple frontend chat list component with Next.js. The chat fetching endpoint is protected with JWT authentication, ensuring only authenticated users can fetch their chat sessions.
Chat Management: Fetch Chat Details
Backend Code (NestJS)
-
Chat Service:
-
Update Chat Service to Handle Fetching Chat Details (
src/chat/chat.service.ts
):
import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Model } from 'mongoose'; import { Chat, ChatDocument } from '../schemas/chat.schema'; import { User, UserDocument } from '../schemas/user.schema'; @Injectable() export class ChatService { constructor( @InjectModel(Chat.name) private chatModel: Model<ChatDocument>, @InjectModel(User.name) private userModel: Model<UserDocument>, ) {} async createChat(participants: string[]): Promise<Chat> { const newChat = new this.chatModel({ participants: participants, messages: [], }); return newChat.save(); } async getChats(userId: string): Promise<Chat[]> { return this.chatModel.find({ participants: userId }).populate('participants', 'username').exec(); } async getChatDetails(chatId: string): Promise<Chat> { return this.chatModel.findById(chatId).populate('participants', 'username').populate('messages.sender', 'username').exec(); } }
-
Update Chat Service to Handle Fetching Chat Details (
-
Chat Controller:
-
Update Chat Controller to Include Fetching Chat Details (
src/chat/chat.controller.ts
):
import { Controller, Post, Get, Param, Body, UseGuards, Request } from '@nestjs/common'; import { ChatService } from './chat.service'; import { CreateChatDto } from './dto/create-chat.dto'; import { JwtAuthGuard } from '../auth/jwt-auth.guard'; import { Chat } from '../schemas/chat.schema'; @Controller('chats') export class ChatController { constructor(private readonly chatService: ChatService) {} @UseGuards(JwtAuthGuard) @Post() async createChat(@Body() createChatDto: CreateChatDto): Promise<Chat> { return this.chatService.createChat(createChatDto.participants); } @UseGuards(JwtAuthGuard) @Get() async getChats(@Request() req): Promise<Chat[]> { return this.chatService.getChats(req.user.userId); } @UseGuards(JwtAuthGuard) @Get(':chatId') async getChatDetails(@Param('chatId') chatId: string): Promise<Chat> { return this.chatService.getChatDetails(chatId); } }
-
Update Chat Controller to Include Fetching Chat Details (
-
Ensure JWT Auth Guard is Applied:
- Ensure that
JwtAuthGuard
is properly set up and imported as shown in previous examples.
- Ensure that
Frontend Code (Next.js)
-
API Call for Fetching Chat Details:
-
Create API Function for Fetching Chat Details (
src/services/api.js
):
import axios from 'axios'; const API_URL = 'http://localhost:3000'; export const register = async (username, password) => { try { const response = await axios.post(`${API_URL}/auth/register`, { username, password }); return response.data; } catch (error) { throw error.response.data; } }; export const login = async (username, password) => { try { const response = await axios.post(`${API_URL}/auth/login`, { username, password }); return response.data; } catch (error) { throw error.response.data; } }; export const getProfile = async (token) => { try { const response = await axios.get(`${API_URL}/auth/me`, { headers: { Authorization: `Bearer ${token}`, }, }); return response.data; } catch (error) { throw error.response.data; } }; export const updateProfile = async (token, userData) => { try { const response = await axios.put(`${API_URL}/auth/me`, userData, { headers: { Authorization: `Bearer ${token}`, }, }); return response.data; } catch (error) { throw error.response.data; } }; export const createChat = async (token, participants) => { try { const response = await axios.post(`${API_URL}/chats`, { participants }, { headers: { Authorization: `Bearer ${token}`, }, }); return response.data; } catch (error) { throw error.response.data; } }; export const getChats = async (token) => { try { const response = await axios.get(`${API_URL}/chats`, { headers: { Authorization: `Bearer ${token}`, }, }); return response.data; } catch (error) { throw error.response.data; } }; export const getChatDetails = async (token, chatId) => { try { const response = await axios.get(`${API_URL}/chats/${chatId}`, { headers: { Authorization: `Bearer ${token}`, }, }); return response.data; } catch (error) { throw error.response.data; } }; export const logout = () => { localStorage.removeItem('token'); window.location.href = '/login'; // Redirect to login page };
-
Create API Function for Fetching Chat Details (
-
Chat Details Page:
-
Create Chat Details Component (
src/components/ChatDetails.js
):
import { useEffect, useState } from 'react'; import { getChatDetails } from '../services/api'; export default function ChatDetails({ chatId }) { const [chat, setChat] = useState(null); const [error, setError] = useState(''); useEffect(() => { const fetchChatDetails = async () => { try { const token = localStorage.getItem('token'); if (token) { const data = await getChatDetails(token, chatId); setChat(data); } else { setError('No token found'); } } catch (err) { setError(err.message); } }; fetchChatDetails(); }, [chatId]); if (error) { return <div className="text-red-500">{error}</div>; } return ( <div className="max-w-md mx-auto mt-10"> <h2 className="text-2xl font-bold mb-5">Chat Details</h2> {chat ? ( <div> <h3 className="text-xl font-bold mb-3">Participants</h3> <ul> {chat.participants.map((participant) => ( <li key={participant._id}>{participant.username}</li> ))} </ul> <h3 className="text-xl font-bold mb-3 mt-5">Messages</h3> <ul> {chat.messages.map((message) => ( <li key={message._id} className="mb-2 p-2 border rounded"> <strong>{message.sender.username}</strong>: {message.content} <br /> <span className="text-gray-500 text-sm">{new Date(message.timestamp).toLocaleString()}</span> </li> ))} </ul> </div> ) : ( <p>Loading...</p> )} </div> ); }
-
Create Chat Details Component (
-
Usage in a Page:
-
Create a Page to Display Chat Details (
src/pages/chat/[chatId].js
):
import { useRouter } from 'next/router'; import ChatDetails from '../../components/ChatDetails'; export default function ChatPage() { const router = useRouter(); const { chatId } = router.query; if (!chatId) { return <div>Loading...</div>; } return <ChatDetails chatId={chatId} />; }
-
Create a Page to Display Chat Details (
Conclusion
This setup includes the backend implementation for fetching details of a specific chat session with NestJS and a simple frontend chat details component with Next.js. The chat details endpoint is protected with JWT authentication, ensuring only authenticated users can fetch the details of their chat sessions.
Messaging: Send Message
Backend Code (NestJS)
-
Message DTO:
-
Create DTO for Sending Message (
src/chat/dto/send-message.dto.ts
):
export class SendMessageDto { content: string; }
-
Create DTO for Sending Message (
-
Chat Schema Update:
-
Update Chat Schema to Include Messages (
src/schemas/chat.schema.ts
):
import { Schema, Prop, SchemaFactory } from '@nestjs/mongoose'; import { Document, Types } from 'mongoose'; export type ChatDocument = Chat & Document; @Schema() export class Chat { @Prop({ type: [{ type: Types.ObjectId, ref: 'User' }], required: true }) participants: Types.ObjectId[]; @Prop({ type: [{ sender: { type: Types.ObjectId, ref: 'User' }, content: String, timestamp: Date }], default: [] }) messages: { sender: Types.ObjectId; content: string; timestamp: Date }[]; } export const ChatSchema = SchemaFactory.createForClass(Chat);
-
Update Chat Schema to Include Messages (
-
Chat Service:
-
Update Chat Service to Handle Sending Messages (
src/chat/chat.service.ts
):
import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Model } from 'mongoose'; import { Chat, ChatDocument } from '../schemas/chat.schema'; import { User, UserDocument } from '../schemas/user.schema'; import { SendMessageDto } from './dto/send-message.dto'; @Injectable() export class ChatService { constructor( @InjectModel(Chat.name) private chatModel: Model<ChatDocument>, @InjectModel(User.name) private userModel: Model<UserDocument>, ) {} async createChat(participants: string[]): Promise<Chat> { const newChat = new this.chatModel({ participants: participants, messages: [], }); return newChat.save(); } async getChats(userId: string): Promise<Chat[]> { return this.chatModel.find({ participants: userId }).populate('participants', 'username').exec(); } async getChatDetails(chatId: string): Promise<Chat> { return this.chatModel.findById(chatId).populate('participants', 'username').populate('messages.sender', 'username').exec(); } async sendMessage(chatId: string, userId: string, sendMessageDto: SendMessageDto): Promise<Chat> { const chat = await this.chatModel.findById(chatId); if (!chat) { throw new NotFoundException('Chat not found'); } chat.messages.push({ sender: userId, content: sendMessageDto.content, timestamp: new Date(), }); return chat.save(); } }
-
Update Chat Service to Handle Sending Messages (
-
Chat Controller:
-
Update Chat Controller to Include Sending Messages (
src/chat/chat.controller.ts
):
import { Controller, Post, Get, Param, Body, UseGuards, Request } from '@nestjs/common'; import { ChatService } from './chat.service'; import { CreateChatDto } from './dto/create-chat.dto'; import { SendMessageDto } from './dto/send-message.dto'; import { JwtAuthGuard } from '../auth/jwt-auth.guard'; import { Chat } from '../schemas/chat.schema'; @Controller('chats') export class ChatController { constructor(private readonly chatService: ChatService) {} @UseGuards(JwtAuthGuard) @Post() async createChat(@Body() createChatDto: CreateChatDto): Promise<Chat> { return this.chatService.createChat(createChatDto.participants); } @UseGuards(JwtAuthGuard) @Get() async getChats(@Request() req): Promise<Chat[]> { return this.chatService.getChats(req.user.userId); } @UseGuards(JwtAuthGuard) @Get(':chatId') async getChatDetails(@Param('chatId') chatId: string): Promise<Chat> { return this.chatService.getChatDetails(chatId); } @UseGuards(JwtAuthGuard) @Post(':chatId/messages') async sendMessage(@Param('chatId') chatId: string, @Request() req, @Body() sendMessageDto: SendMessageDto): Promise<Chat> { return this.chatService.sendMessage(chatId, req.user.userId, sendMessageDto); } }
-
Update Chat Controller to Include Sending Messages (
-
Ensure JWT Auth Guard is Applied:
- Ensure that
JwtAuthGuard
is properly set up and imported as shown in previous examples.
- Ensure that
Frontend Code (Next.js)
-
API Call for Sending Messages:
-
Create API Function for Sending Messages (
src/services/api.js
):
import axios from 'axios'; const API_URL = 'http://localhost:3000'; export const register = async (username, password) => { try { const response = await axios.post(`${API_URL}/auth/register`, { username, password }); return response.data; } catch (error) { throw error.response.data; } }; export const login = async (username, password) => { try { const response = await axios.post(`${API_URL}/auth/login`, { username, password }); return response.data; } catch (error) { throw error.response.data; } }; export const getProfile = async (token) => { try { const response = await axios.get(`${API_URL}/auth/me`, { headers: { Authorization: `Bearer ${token}`, }, }); return response.data; } catch (error) { throw error.response.data; } }; export const updateProfile = async (token, userData) => { try { const response = await axios.put(`${API_URL}/auth/me`, userData, { headers: { Authorization: `Bearer ${token}`, }, }); return response.data; } catch (error) { throw error.response.data; } }; export const createChat = async (token, participants) => { try { const response = await axios.post(`${API_URL}/chats`, { participants }, { headers: { Authorization: `Bearer ${token}`, }, }); return response.data; } catch (error) { throw error.response.data; } }; export const getChats = async (token) => { try { const response = await axios.get(`${API_URL}/chats`, { headers: { Authorization: `Bearer ${token}`, }, }); return response.data; } catch (error) { throw error.response.data; } }; export const getChatDetails = async (token, chatId) => { try { const response = await axios.get(`${API_URL}/chats/${chatId}`, { headers: { Authorization: `Bearer ${token}`, }, }); return response.data; } catch (error) { throw error.response.data; } }; export const sendMessage = async (token, chatId, content) => { try { const response = await axios.post(`${API_URL}/chats/${chatId}/messages`, { content }, { headers: { Authorization: `Bearer ${token}`, }, }); return response.data; } catch (error) { throw error.response.data; } }; export const logout = () => { localStorage.removeItem('token'); window.location.href = '/login'; // Redirect to login page };
-
Create API Function for Sending Messages (
-
Chat Details Component:
-
Update Chat Details Component to Include Sending Messages (
src/components/ChatDetails.js
):
import { useEffect, useState } from 'react'; import { getChatDetails, sendMessage } from '../services/api'; export default function ChatDetails({ chatId }) { const [chat, setChat] = useState(null); const [messageContent, setMessageContent] = useState(''); const [error, setError] = useState(''); useEffect(() => { const fetchChatDetails = async () => { try { const token = localStorage.getItem('token'); if (token) { const data = await getChatDetails(token, chatId); setChat(data); } else { setError('No token found'); } } catch (err) { setError(err.message); } }; fetchChatDetails(); }, [chatId]); const handleSendMessage = async (e) => { e.preventDefault(); try { const token = localStorage.getItem('token'); if (token) { const data = await sendMessage(token, chatId, messageContent); setChat(data); setMessageContent(''); } else { setError('No token found'); } } catch (err) { setError(err.message); } }; if (error) { return <div className="text-red-500">{error}</div>; } return ( <div className="max-w-md mx-auto mt-10"> <h2 className="text-2xl font-bold mb-5">Chat Details</h2> {chat ? ( <div> <h3 className="text-xl
-
Update Chat Details Component to Include Sending Messages (
font-bold mb-3">Participants
-
{chat.participants.map((participant) => (
- {participant.username} ))}
Messages
-
{chat.messages.map((message) => (
- {message.sender.username}: {message.content} {new Date(message.timestamp).toLocaleString()} ))}
type="text"
value={messageContent}
onChange={(e) => setMessageContent(e.target.value)}
className="w-full px-3 py-2 border rounded mb-2"
placeholder="Type your message..."
/>
Send
) : (
Loading...
)}
);
}
```
-
Usage in a Page:
-
Create a Page to Display Chat Details (
src/pages/chat/[chatId].js
):
import { useRouter } from 'next/router'; import ChatDetails from '../../components/ChatDetails'; export default function ChatPage() { const router = useRouter(); const { chatId } = router.query; if (!chatId) { return <div>Loading...</div>; } return <ChatDetails chatId={chatId} />; }
-
Create a Page to Display Chat Details (
Conclusion
This setup includes the backend implementation for sending messages in a specific chat session with NestJS and a simple frontend chat details component with Next.js. The chat details component now includes functionality for sending messages. The chat details and message sending endpoints are protected with JWT authentication, ensuring only authenticated users can fetch and send messages in their chat sessions.
Messaging: Real-Time Message Receiving Using WebSocket
Backend Code (NestJS)
-
WebSocket Gateway Setup:
- Install WebSocket Dependencies:
npm install @nestjs/websockets @nestjs/platform-socket.io
-
WebSocket Gateway:
-
Create WebSocket Gateway (
src/chat/chat.gateway.ts
):
import { SubscribeMessage, WebSocketGateway, WebSocketServer, OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect, } from '@nestjs/websockets'; import { Server, Socket } from 'socket.io'; import { ChatService } from './chat.service'; import { SendMessageDto } from './dto/send-message.dto'; @WebSocketGateway() export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect { @WebSocketServer() server: Server; constructor(private readonly chatService: ChatService) {} afterInit(server: Server) { console.log('Init'); } handleConnection(client: Socket, ...args: any[]) { console.log(`Client connected: ${client.id}`); } handleDisconnect(client: Socket) { console.log(`Client disconnected: ${client.id}`); } @SubscribeMessage('sendMessage') async handleMessage(client: Socket, payload: { chatId: string; userId: string; content: string }) { const message: SendMessageDto = { content: payload.content }; const chat = await this.chatService.sendMessage(payload.chatId, payload.userId, message); this.server.to(payload.chatId).emit('receiveMessage', chat); } @SubscribeMessage('joinChat') handleJoinChat(client: Socket, chatId: string) { client.join(chatId); console.log(`Client ${client.id} joined chat ${chatId}`); } }
-
Create WebSocket Gateway (
-
Chat Service (Update for WebSocket):
-
Update Chat Service to Handle WebSocket Messages (
src/chat/chat.service.ts
):
import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Model } from 'mongoose'; import { Chat, ChatDocument } from '../schemas/chat.schema'; import { User, UserDocument } from '../schemas/user.schema'; import { SendMessageDto } from './dto/send-message.dto'; @Injectable() export class ChatService { constructor( @InjectModel(Chat.name) private chatModel: Model<ChatDocument>, @InjectModel(User.name) private userModel: Model<UserDocument>, ) {} async createChat(participants: string[]): Promise<Chat> { const newChat = new this.chatModel({ participants: participants, messages: [], }); return newChat.save(); } async getChats(userId: string): Promise<Chat[]> { return this.chatModel.find({ participants: userId }).populate('participants', 'username').exec(); } async getChatDetails(chatId: string): Promise<Chat> { return this.chatModel.findById(chatId).populate('participants', 'username').populate('messages.sender', 'username').exec(); } async sendMessage(chatId: string, userId: string, sendMessageDto: SendMessageDto): Promise<Chat> { const chat = await this.chatModel.findById(chatId); if (!chat) { throw new NotFoundException('Chat not found'); } chat.messages.push({ sender: userId, content: sendMessageDto.content, timestamp: new Date(), }); return chat.save(); } }
-
Update Chat Service to Handle WebSocket Messages (
-
Chat Module:
-
Update Chat Module to Include WebSocket Gateway (
src/chat/chat.module.ts
):
import { Module } from '@nestjs/common'; import { MongooseModule } from '@nestjs/mongoose'; import { ChatService } from './chat.service'; import { ChatController } from './chat.controller'; import { ChatGateway } from './chat.gateway'; import { Chat, ChatSchema } from '../schemas/chat.schema'; import { User, UserSchema } from '../schemas/user.schema'; @Module({ imports: [ MongooseModule.forFeature([{ name: Chat.name, schema: ChatSchema }]), MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]), ], providers: [ChatService, ChatGateway], controllers: [ChatController], }) export class ChatModule {}
-
Update Chat Module to Include WebSocket Gateway (
Frontend Code (Next.js)
-
WebSocket Client Setup:
- Install Socket.io Client:
npm install socket.io-client
-
WebSocket Client:
-
Create WebSocket Client Hook (
src/hooks/useChat.js
):
import { useEffect, useState } from 'react'; import io from 'socket.io-client'; const useChat = (chatId) => { const [messages, setMessages] = useState([]); const [message, setMessage] = useState(''); const [socket, setSocket] = useState(null); useEffect(() => { const newSocket = io('http://localhost:3000'); setSocket(newSocket); newSocket.emit('joinChat', chatId); newSocket.on('receiveMessage', (newChat) => { setMessages(newChat.messages); }); return () => newSocket.close(); }, [chatId]); const sendMessage = (userId, content) => { socket.emit('sendMessage', { chatId, userId, content }); setMessage(''); }; return { messages, message, setMessage, sendMessage, }; }; export default useChat;
-
Create WebSocket Client Hook (
-
Chat Details Component:
-
Update Chat Details Component to Include WebSocket (
src/components/ChatDetails.js
):
import { useRouter } from 'next/router'; import useChat from '../hooks/useChat'; export default function ChatDetails({ chatId, userId }) { const router = useRouter(); const { messages, message, setMessage, sendMessage } = useChat(chatId); const handleSendMessage = (e) => { e.preventDefault(); sendMessage(userId, message); }; return ( <div className="max-w-md mx-auto mt-10"> <h2 className="text-2xl font-bold mb-5">Chat Details</h2> <div> <h3 className="text-xl font-bold mb-3">Messages</h3> <ul> {messages.map((msg, index) => ( <li key={index} className="mb-2 p-2 border rounded"> <strong>{msg.sender.username}</strong>: {msg.content} <br /> <span className="text-gray-500 text-sm">{new Date(msg.timestamp).toLocaleString()}</span> </li> ))} </ul> <form onSubmit={handleSendMessage} className="mt-4"> <input type="text" value={message} onChange={(e) => setMessage(e.target.value)} className="w-full px-3 py-2 border rounded mb-2" placeholder="Type your message..." /> <button type="submit" className="bg-blue-500 text-white px-4 py-2 rounded">Send</button> </form> </div> </div> ); }
-
Update Chat Details Component to Include WebSocket (
-
Usage in a Page:
-
Update Page to Include User ID (
src/pages/chat/[chatId].js
):
import { useRouter } from 'next/router'; import ChatDetails from '../../components/ChatDetails'; export default function ChatPage() { const router = useRouter(); const { chatId } = router.query; const userId = 'USER_ID'; // Replace with the actual user ID from authentication if (!chatId) { return <div>Loading...</div>; } return <ChatDetails chatId={chatId} userId={userId} />; }
-
Update Page to Include User ID (
Conclusion
This setup includes the backend implementation for real-time message receiving using WebSocket with NestJS and a simple frontend implementation with Next.js using Socket.io. The ChatGateway
handles the WebSocket events, and the frontend uses a custom hook to manage WebSocket connections and message state. This allows for real-time messaging capabilities in your chat application.
Real-time Communication: WebSocket Setup
Backend Code (NestJS)
-
WebSocket Gateway Setup:
- Install WebSocket Dependencies:
npm install @nestjs/websockets @nestjs/platform-socket.io
-
WebSocket Gateway:
-
Create WebSocket Gateway (
src/chat/chat.gateway.ts
):
import { SubscribeMessage, WebSocketGateway, WebSocketServer, OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect, } from '@nestjs/websockets'; import { Server, Socket } from 'socket.io'; import { ChatService } from './chat.service'; import { SendMessageDto } from './dto/send-message.dto'; @WebSocketGateway() export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect { @WebSocketServer() server: Server; constructor(private readonly chatService: ChatService) {} afterInit(server: Server) { console.log('WebSocket server initialized'); } handleConnection(client: Socket) { console.log(`Client connected: ${client.id}`); } handleDisconnect(client: Socket) { console.log(`Client disconnected: ${client.id}`); } @SubscribeMessage('sendMessage') async handleMessage(client: Socket, payload: { chatId: string; userId: string; content: string }) { const message: SendMessageDto = { content: payload.content }; const chat = await this.chatService.sendMessage(payload.chatId, payload.userId, message); this.server.to(payload.chatId).emit('receiveMessage', chat); } @SubscribeMessage('joinChat') handleJoinChat(client: Socket, chatId: string) { client.join(chatId); console.log(`Client ${client.id} joined chat ${chatId}`); } }
-
Create WebSocket Gateway (
-
Chat Service Update:
-
Update Chat Service to Handle WebSocket Messages (
src/chat/chat.service.ts
):
import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Model } from 'mongoose'; import { Chat, ChatDocument } from '../schemas/chat.schema'; import { User, UserDocument } from '../schemas/user.schema'; import { SendMessageDto } from './dto/send-message.dto'; @Injectable() export class ChatService { constructor( @InjectModel(Chat.name) private chatModel: Model<ChatDocument>, @InjectModel(User.name) private userModel: Model<UserDocument>, ) {} async createChat(participants: string[]): Promise<Chat> { const newChat = new this.chatModel({ participants: participants, messages: [], }); return newChat.save(); } async getChats(userId: string): Promise<Chat[]> { return this.chatModel.find({ participants: userId }).populate('participants', 'username').exec(); } async getChatDetails(chatId: string): Promise<Chat> { return this.chatModel.findById(chatId).populate('participants', 'username').populate('messages.sender', 'username').exec(); } async sendMessage(chatId: string, userId: string, sendMessageDto: SendMessageDto): Promise<Chat> { const chat = await this.chatModel.findById(chatId); if (!chat) { throw new NotFoundException('Chat not found'); } chat.messages.push({ sender: userId, content: sendMessageDto.content, timestamp: new Date(), }); return chat.save(); } }
-
Update Chat Service to Handle WebSocket Messages (
-
Chat Module Update:
-
Update Chat Module to Include WebSocket Gateway (
src/chat/chat.module.ts
):
import { Module } from '@nestjs/common'; import { MongooseModule } from '@nestjs/mongoose'; import { ChatService } from './chat.service'; import { ChatController } from './chat.controller'; import { ChatGateway } from './chat.gateway'; import { Chat, ChatSchema } from '../schemas/chat.schema'; import { User, UserSchema } from '../schemas/user.schema'; @Module({ imports: [ MongooseModule.forFeature([{ name: Chat.name, schema: ChatSchema }]), MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]), ], providers: [ChatService, ChatGateway], controllers: [ChatController], }) export class ChatModule {}
-
Update Chat Module to Include WebSocket Gateway (
Frontend Code (Next.js)
-
WebSocket Client Setup:
- Install Socket.io Client:
npm install socket.io-client
-
WebSocket Client Hook:
-
Create WebSocket Client Hook (
src/hooks/useChat.js
):
import { useEffect, useState } from 'react'; import io from 'socket.io-client'; const useChat = (chatId) => { const [messages, setMessages] = useState([]); const [message, setMessage] = useState(''); const [socket, setSocket] = useState(null); useEffect(() => { const newSocket = io('http://localhost:3000'); setSocket(newSocket); newSocket.emit('joinChat', chatId); newSocket.on('receiveMessage', (newChat) => { setMessages(newChat.messages); }); return () => newSocket.close(); }, [chatId]); const sendMessage = (userId, content) => { socket.emit('sendMessage', { chatId, userId, content }); setMessage(''); }; return { messages, message, setMessage, sendMessage, }; }; export default useChat;
-
Create WebSocket Client Hook (
-
Chat Details Component:
-
Update Chat Details Component to Include WebSocket (
src/components/ChatDetails.js
):
import { useEffect, useState } from 'react'; import useChat from '../hooks/useChat'; export default function ChatDetails({ chatId, userId }) { const { messages, message, setMessage, sendMessage } = useChat(chatId); const handleSendMessage = (e) => { e.preventDefault(); sendMessage(userId, message); }; return ( <div className="max-w-md mx-auto mt-10"> <h2 className="text-2xl font-bold mb-5">Chat Details</h2> <div> <h3 className="text-xl font-bold mb-3">Messages</h3> <ul> {messages.map((msg, index) => ( <li key={index} className="mb-2 p-2 border rounded"> <strong>{msg.sender.username}</strong>: {msg.content} <br /> <span className="text-gray-500 text-sm">{new Date(msg.timestamp).toLocaleString()}</span> </li> ))} </ul> <form onSubmit={handleSendMessage} className="mt-4"> <input type="text" value={message} onChange={(e) => setMessage(e.target.value)} className="w-full px-3 py-2 border rounded mb-2" placeholder="Type your message..." /> <button type="submit" className="bg-blue-500 text-white px-4 py-2 rounded">Send</button> </form> </div> </div> ); }
-
Update Chat Details Component to Include WebSocket (
-
Usage in a Page:
-
Update Page to Include User ID (
src/pages/chat/[chatId].js
):
import { useRouter } from 'next/router'; import ChatDetails from '../../components/ChatDetails'; export default function ChatPage() { const router = useRouter(); const { chatId } = router.query; const userId = 'USER_ID'; // Replace with the actual user ID from authentication if (!chatId) { return <div>Loading...</div>; } return <ChatDetails chatId={chatId} userId={userId} />; }
-
Update Page to Include User ID (
Conclusion
This setup includes the backend implementation for real-time message receiving and sending using WebSocket with NestJS and a simple frontend implementation with Next.js using Socket.io. The ChatGateway
handles the WebSocket events, and the frontend uses a custom hook to manage WebSocket connections and message state. This allows for real-time messaging capabilities in your chat application, ensuring that messages are sent and received in real time.
User Interface: Login Page
Frontend Code (Next.js)
-
Create Login Page:
-
Create Login Page Component (
src/pages/login.js
):
import { useState } from 'react'; import { useRouter } from 'next/router'; import { login } from '../services/api'; export default function LoginPage() { const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [error, setError] = useState(''); const router = useRouter(); const handleSubmit = async (e) => { e.preventDefault(); try { const data = await login(username, password); localStorage.setItem('token', data.access_token); router.push('/chats'); } catch (err) { setError(err.message); } }; return ( <div className="flex items-center justify-center h-screen bg-gray-100"> <div className="w-full max-w-md p-8 space-y-8 bg-white rounded shadow-lg"> <h2 className="text-2xl font-bold text-center">Login</h2> <form className="space-y-6" onSubmit={handleSubmit}> <div> <label className="block text-gray-700">Username</label> <input type="text" value={username} onChange={(e) => setUsername(e.target.value)} className="w-full px-3 py-2 border rounded" required /> </div> <div> <label className="block text-gray-700">Password</label> <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} className="w-full px-3 py-2 border rounded" required /> </div> <div> <button type="submit" className="w-full px-4 py-2 text-white bg-blue-500 rounded hover:bg-blue-700" > Login </button> </div> {error && <p className="text-red-500">{error}</p>} </form> </div> </div> ); }
-
Create Login Page Component (
-
API Call for Login:
-
Update API Function for Login (
src/services/api.js
):
import axios from 'axios'; const API_URL = 'http://localhost:3000'; export const register = async (username, password) => { try { const response = await axios.post(`${API_URL}/auth/register`, { username, password }); return response.data; } catch (error) { throw error.response.data; } }; export const login = async (username, password) => { try { const response = await axios.post(`${API_URL}/auth/login`, { username, password }); return response.data; } catch (error) { throw error.response.data; } }; export const getProfile = async (token) => { try { const response = await axios.get(`${API_URL}/auth/me`, { headers: { Authorization: `Bearer ${token}`, }, }); return response.data; } catch (error) { throw error.response.data; } }; export const updateProfile = async (token, userData) => { try { const response = await axios.put(`${API_URL}/auth/me`, userData, { headers: { Authorization: `Bearer ${token}`, }, }); return response.data; } catch (error) { throw error.response.data; } }; export const createChat = async (token, participants) => { try { const response = await axios.post(`${API_URL}/chats`, { participants }, { headers: { Authorization: `Bearer ${token}`, }, }); return response.data; } catch (error) { throw error.response.data; } }; export const getChats = async (token) => { try { const response = await axios.get(`${API_URL}/chats`, { headers: { Authorization: `Bearer ${token}`, }, }); return response.data; } catch (error) { throw error.response.data; } }; export const getChatDetails = async (token, chatId) => { try { const response = await axios.get(`${API_URL}/chats/${chatId}`, { headers: { Authorization: `Bearer ${token}`, }, }); return response.data; } catch (error) { throw error.response.data; } }; export const sendMessage = async (token, chatId, content) => { try { const response = await axios.post(`${API_URL}/chats/${chatId}/messages`, { content }, { headers: { Authorization: `Bearer ${token}`, }, }); return response.data; } catch (error) { throw error.response.data; } }; export const logout = () => { localStorage.removeItem('token'); window.location.href = '/login'; // Redirect to login page };
-
Update API Function for Login (
Conclusion
This setup includes the frontend implementation of a login page using Next.js and Tailwind CSS. The login page consists of a form with input fields for the username and password, and a submit button to log in. The login API call is handled using an async function that communicates with the backend. Upon successful login, the user is redirected to the chat page. Any error during login is displayed to the user.
User Interface: Registration Page
Frontend Code (Next.js)
-
Create Registration Page:
-
Create Registration Page Component (
src/pages/register.js
):
import { useState } from 'react'; import { useRouter } from 'next/router'; import { register } from '../services/api'; export default function RegisterPage() { const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [error, setError] = useState(''); const [success, setSuccess] = useState(''); const router = useRouter(); const handleSubmit = async (e) => { e.preventDefault(); try { await register(username, password); setSuccess('Registration successful! You can now login.'); setUsername(''); setPassword(''); setError(''); setTimeout(() => { router.push('/login'); }, 2000); } catch (err) { setError(err.message); setSuccess(''); } }; return ( <div className="flex items-center justify-center h-screen bg-gray-100"> <div className="w-full max-w-md p-8 space-y-8 bg-white rounded shadow-lg"> <h2 className="text-2xl font-bold text-center">Register</h2> <form className="space-y-6" onSubmit={handleSubmit}> <div> <label className="block text-gray-700">Username</label> <input type="text" value={username} onChange={(e) => setUsername(e.target.value)} className="w-full px-3 py-2 border rounded" required /> </div> <div> <label className="block text-gray-700">Password</label> <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} className="w-full px-3 py-2 border rounded" required /> </div> <div> <button type="submit" className="w-full px-4 py-2 text-white bg-blue-500 rounded hover:bg-blue-700" > Register </button> </div> {error && <p className="text-red-500">{error}</p>} {success && <p className="text-green-500">{success}</p>} </form> </div> </div> ); }
-
Create Registration Page Component (
-
API Call for Registration:
-
Update API Function for Registration (
src/services/api.js
):
import axios from 'axios'; const API_URL = 'http://localhost:3000'; export const register = async (username, password) => { try { const response = await axios.post(`${API_URL}/auth/register`, { username, password }); return response.data; } catch (error) { throw error.response.data; } }; export const login = async (username, password) => { try { const response = await axios.post(`${API_URL}/auth/login`, { username, password }); return response.data; } catch (error) { throw error.response.data; } }; export const getProfile = async (token) => { try { const response = await axios.get(`${API_URL}/auth/me`, { headers: { Authorization: `Bearer ${token}`, }, }); return response.data; } catch (error) { throw error.response.data; } }; export const updateProfile = async (token, userData) => { try { const response = await axios.put(`${API_URL}/auth/me`, userData, { headers: { Authorization: `Bearer ${token}`, }, }); return response.data; } catch (error) { throw error.response.data; } }; export const createChat = async (token, participants) => { try { const response = await axios.post(`${API_URL}/chats`, { participants }, { headers: { Authorization: `Bearer ${token}`, }, }); return response.data; } catch (error) { throw error.response.data; } }; export const getChats = async (token) => { try { const response = await axios.get(`${API_URL}/chats`, { headers: { Authorization: `Bearer ${token}`, }, }); return response.data; } catch (error) { throw error.response.data; } }; export const getChatDetails = async (token, chatId) => { try { const response = await axios.get(`${API_URL}/chats/${chatId}`, { headers: { Authorization: `Bearer ${token}`, }, }); return response.data; } catch (error) { throw error.response.data; } }; export const sendMessage = async (token, chatId, content) => { try { const response = await axios.post(`${API_URL}/chats/${chatId}/messages`, { content }, { headers: { Authorization: `Bearer ${token}`, }, }); return response.data; } catch (error) { throw error.response.data; } }; export const logout = () => { localStorage.removeItem('token'); window.location.href = '/login'; // Redirect to login page };
-
Update API Function for Registration (
Conclusion
This setup includes the frontend implementation of a registration page using Next.js and Tailwind CSS. The registration page consists of a form with input fields for the username and password, and a submit button to register. The registration API call is handled using an async function that communicates with the backend. Upon successful registration, the user is notified and redirected to the login page. Any error during registration is displayed to the user.
User Interface: Chat List Page and Chat Window
Frontend Code (Next.js)
Chat List Page
-
Create Chat List Page:
-
Create Chat List Page Component (
src/pages/chats.js
):
import { useEffect, useState } from 'react'; import { useRouter } from 'next/router'; import { getChats } from '../services/api'; export default function ChatListPage() { const [chats, setChats] = useState([]); const [search, setSearch] = useState(''); const [error, setError] = useState(''); const router = useRouter(); useEffect(() => { const fetchChats = async () => { try { const token = localStorage.getItem('token'); if (token) { const data = await getChats(token); setChats(data); } else { setError('No token found'); } } catch (err) { setError(err.message); } }; fetchChats(); }, []); const filteredChats = chats.filter(chat => chat.participants.some(participant => participant.username.toLowerCase().includes(search.toLowerCase()) ) ); const handleChatClick = (chatId) => { router.push(`/chat/${chatId}`); }; if (error) { return <div className="text-red-500">{error}</div>; } return ( <div className="max-w-md mx-auto mt-10"> <h2 className="text-2xl font-bold mb-5">Chats</h2> <input type="text" value={search} onChange={(e) => setSearch(e.target.value)} className="w-full px-3 py-2 border rounded mb-5" placeholder="Search chats..." /> {filteredChats.length > 0 ? ( <ul> {filteredChats.map(chat => ( <li key={chat._id} className="mb-2 p-2 border rounded cursor-pointer" onClick={() => handleChatClick(chat._id)} > {chat.participants.map(participant => participant.username).join(', ')} </li> ))} </ul> ) : ( <p>No chats available.</p> )} </div> ); }
-
Create Chat List Page Component (
Chat Window
-
Create Chat Window Component:
-
Create Chat Window Component (
src/components/ChatWindow.js
):
import { useEffect, useState } from 'react'; import useChat from '../hooks/useChat'; export default function ChatWindow({ chatId, userId }) { const { messages, message, setMessage, sendMessage } = useChat(chatId); const handleSendMessage = (e) => { e.preventDefault(); sendMessage(userId, message); }; return ( <div className="max-w-md mx-auto mt-10"> <h2 className="text-2xl font-bold mb-5">Chat</h2> <div> <ul className="mb-5"> {messages.map((msg, index) => ( <li key={index} className="mb-2 p-2 border rounded"> <strong>{msg.sender.username}</strong>: {msg.content} <br /> <span className="text-gray-500 text-sm">{new Date(msg.timestamp).toLocaleString()}</span> </li> ))} </ul> <form onSubmit={handleSendMessage}> <input type="text" value={message} onChange={(e) => setMessage(e.target.value)} className="w-full px-3 py-2 border rounded mb-2" placeholder="Type your message..." /> <button type="submit" className="bg-blue-500 text-white px-4 py-2 rounded"> Send </button> </form> </div> </div> ); }
-
Create Chat Window Component (
-
WebSocket Client Hook (from previous steps):
-
Ensure you have the
useChat
hook (src/hooks/useChat.js
):
import { useEffect, useState } from 'react'; import io from 'socket.io-client'; const useChat = (chatId) => { const [messages, setMessages] = useState([]); const [message, setMessage] = useState(''); const [socket, setSocket] = useState(null); useEffect(() => { const newSocket = io('http://localhost:3000'); setSocket(newSocket); newSocket.emit('joinChat', chatId); newSocket.on('receiveMessage', (newChat) => { setMessages(newChat.messages); }); return () => newSocket.close(); }, [chatId]); const sendMessage = (userId, content) => { socket.emit('sendMessage', { chatId, userId, content }); setMessage(''); }; return { messages, message, setMessage, sendMessage, }; }; export default useChat;
-
Ensure you have the
-
Usage in a Page:
-
Create Page for Chat Window (
src/pages/chat/[chatId].js
):
import { useRouter } from 'next/router'; import ChatWindow from '../../components/ChatWindow'; export default function ChatPage() { const router = useRouter(); const { chatId } = router.query; const userId = 'USER_ID'; // Replace with the actual user ID from authentication if (!chatId) { return <div>Loading...</div>; } return <ChatWindow chatId={chatId} userId={userId} />; }
-
Create Page for Chat Window (
Conclusion
This setup includes the frontend implementation for the chat list page and chat window using Next.js and Tailwind CSS. The chat list page displays the list of chat sessions with a search bar, allowing users to filter chats. The chat window displays chat messages and includes an input field and send button for sending new messages. The useChat
hook manages the WebSocket connection and real-time message handling.
Notifications: Real-time Notifications for New Messages
Frontend Code (Next.js)
-
WebSocket Client Hook Enhancement:
-
Update
useChat
Hook to Handle Notifications (src/hooks/useChat.js
):
import { useEffect, useState } from 'react'; import io from 'socket.io-client'; const useChat = (chatId, onNewMessage) => { const [messages, setMessages] = useState([]); const [message, setMessage] = useState(''); const [socket, setSocket] = useState(null); useEffect(() => { const newSocket = io('http://localhost:3000'); setSocket(newSocket); newSocket.emit('joinChat', chatId); newSocket.on('receiveMessage', (newChat) => { setMessages(newChat.messages); if (onNewMessage) { const newMessage = newChat.messages[newChat.messages.length - 1]; onNewMessage(newMessage); } }); return () => newSocket.close(); }, [chatId]); const sendMessage = (userId, content) => { socket.emit('sendMessage', { chatId, userId, content }); setMessage(''); }; return { messages, message, setMessage, sendMessage, }; }; export default useChat;
-
Update
-
Notification Component:
-
Create Notification Component (
src/components/Notification.js
):
export default function Notification({ message }) { if (!message) return null; return ( <div className="fixed bottom-0 right-0 mb-4 mr-4 p-4 bg-blue-500 text-white rounded shadow-lg"> <strong>{message.sender.username}</strong>: {message.content} </div> ); }
-
Create Notification Component (
-
Chat Details Component with Notifications:
-
Update Chat Details Component to Show Notifications (
src/components/ChatDetails.js
):
import { useState } from 'react'; import useChat from '../hooks/useChat'; import Notification from './Notification'; export default function ChatDetails({ chatId, userId }) { const [notification, setNotification] = useState(null); const { messages, message, setMessage, sendMessage } = useChat(chatId, setNotification); const handleSendMessage = (e) => { e.preventDefault(); sendMessage(userId, message); }; return ( <div className="max-w-md mx-auto mt-10"> <h2 className="text-2xl font-bold mb-5">Chat</h2> <div> <ul className="mb-5"> {messages.map((msg, index) => ( <li key={index} className="mb-2 p-2 border rounded"> <strong>{msg.sender.username}</strong>: {msg.content} <br /> <span className="text-gray-500 text-sm">{new Date(msg.timestamp).toLocaleString()}</span> </li> ))} </ul> <form onSubmit={handleSendMessage}> <input type="text" value={message} onChange={(e) => setMessage(e.target.value)} className="w-full px-3 py-2 border rounded mb-2" placeholder="Type your message..." /> <button type="submit" className="bg-blue-500 text-white px-4 py-2 rounded"> Send </button> </form> </div> <Notification message={notification} /> </div> ); }
-
Update Chat Details Component to Show Notifications (
-
Usage in a Page:
-
Ensure Page Includes User ID (
src/pages/chat/[chatId].js
):
import { useRouter } from 'next/router'; import ChatDetails from '../../components/ChatDetails'; export default function ChatPage() { const router = useRouter(); const { chatId } = router.query; const userId = 'USER_ID'; // Replace with the actual user ID from authentication if (!chatId) { return <div>Loading...</div>; } return <ChatDetails chatId={chatId} userId={userId} />; }
-
Ensure Page Includes User ID (
Conclusion
This setup includes the frontend implementation for real-time notifications for new messages using Next.js. The useChat
hook is enhanced to notify the component of new messages. A Notification
component displays the notification at the bottom right corner of the screen. The ChatDetails
component is updated to show notifications whenever a new message is received. This ensures that users are promptly notified of new messages in real-time.
File Sharing: Upload and Download Files
Backend Code (NestJS)
-
Install Required Dependencies:
- Install Multer for File Uploads:
npm install @nestjs/platform-express multer
-
File Schema:
-
Create File Schema (
src/schemas/file.schema.ts
):
import { Schema, Prop, SchemaFactory } from '@nestjs/mongoose'; import { Document, Types } from 'mongoose'; export type FileDocument = File & Document; @Schema() export class File { @Prop({ required: true }) filename: string; @Prop({ required: true }) path: string; @Prop({ required: true }) mimetype: string; @Prop({ type: Types.ObjectId, ref: 'Chat' }) chat: Types.ObjectId; } export const FileSchema = SchemaFactory.createForClass(File);
-
Create File Schema (
-
Chat Schema Update:
-
Update Chat Schema to Include Files (
src/schemas/chat.schema.ts
):
import { Schema, Prop, SchemaFactory } from '@nestjs/mongoose'; import { Document, Types } from 'mongoose'; export type ChatDocument = Chat & Document; @Schema() export class Chat { @Prop({ type: [{ type: Types.ObjectId, ref: 'User' }], required: true }) participants: Types.ObjectId[]; @Prop({ type: [{ sender: { type: Types.ObjectId, ref: 'User' }, content: String, timestamp: Date }], default: [] }) messages: { sender: Types.ObjectId; content: string; timestamp: Date }[]; @Prop({ type: [{ type: Types.ObjectId, ref: 'File' }], default: [] }) files: Types.ObjectId[]; } export const ChatSchema = SchemaFactory.createForClass(Chat);
-
Update Chat Schema to Include Files (
-
File Service:
-
Create File Service to Handle File Upload and Download (
src/file/file.service.ts
):
import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Model } from 'mongoose'; import { Chat, ChatDocument } from '../schemas/chat.schema'; import { File, FileDocument } from '../schemas/file.schema'; import { Express } from 'express'; import { join } from 'path'; @Injectable() export class FileService { constructor( @InjectModel(File.name) private fileModel: Model<FileDocument>, @InjectModel(Chat.name) private chatModel: Model<ChatDocument>, ) {} async uploadFile(chatId: string, file: Express.Multer.File): Promise<File> { const chat = await this.chatModel.findById(chatId); if (!chat) { throw new NotFoundException('Chat not found'); } const newFile = new this.fileModel({ filename: file.filename, path: file.path, mimetype: file.mimetype, chat: chatId, }); chat.files.push(newFile._id); await chat.save(); return newFile.save(); } async downloadFile(fileId: string): Promise<File> { const file = await this.fileModel.findById(fileId); if (!file) { throw new NotFoundException('File not found'); } return file; } getFilePath(file: File): string { return join(__dirname, '..', '..', file.path); } }
-
Create File Service to Handle File Upload and Download (
-
File Controller:
-
Create File Controller to Handle File Upload and Download (
src/file/file.controller.ts
):
import { Controller, Post, Get, Param, UploadedFile, UseInterceptors, Res, NotFoundException } from '@nestjs/common'; import { FileInterceptor } from '@nestjs/platform-express'; import { FileService } from './file.service'; import { diskStorage } from 'multer'; import { v4 as uuidv4 } from 'uuid'; import { Express, Response } from 'express'; @Controller('chats/:chatId/files') export class FileController { constructor(private readonly fileService: FileService) {} @Post() @UseInterceptors( FileInterceptor('file', { storage: diskStorage({ destination: './uploads', filename: (req, file, cb) => { const filename = `${uuidv4()}-${file.originalname}`; cb(null, filename); }, }), }), ) async uploadFile( @Param('chatId') chatId: string, @UploadedFile() file: Express.Multer.File, ) { return this.fileService.uploadFile(chatId, file); } @Get(':fileId') async downloadFile( @Param('fileId') fileId: string, @Res() res: Response, ) { const file = await this.fileService.downloadFile(fileId); if (!file) { throw new NotFoundException('File not found'); } res.sendFile(this.fileService.getFilePath(file)); } }
-
Create File Controller to Handle File Upload and Download (
-
File Module:
-
Create File Module (
src/file/file.module.ts
):
import { Module } from '@nestjs/common'; import { MongooseModule } from '@nestjs/mongoose'; import { FileService } from './file.service'; import { FileController } from './file.controller'; import { File, FileSchema } from '../schemas/file.schema'; import { Chat, ChatSchema } from '../schemas/chat.schema'; @Module({ imports: [ MongooseModule.forFeature([{ name: File.name, schema: FileSchema }]), MongooseModule.forFeature([{ name: Chat.name, schema: ChatSchema }]), ], providers: [FileService], controllers: [FileController], }) export class FileModule {}
-
Create File Module (
-
App Module Update:
-
Update App Module to Include File Module (
src/app.module.ts
):
import { Module } from '@nestjs/common'; import { MongooseModule } from '@nestjs/mongoose'; import { AuthModule } from './auth/auth.module'; import { ChatModule } from './chat/chat.module'; import { FileModule } from './file/file.module'; @Module({ imports: [ MongooseModule.forRoot('mongodb://localhost/chat-app'), AuthModule, ChatModule, FileModule, ], }) export class AppModule {}
-
Update App Module to Include File Module (
Frontend Code (Next.js)
-
API Call for File Upload and Download:
-
Create API Functions for File Upload and Download (
src/services/api.js
):
import axios from 'axios'; const API_URL = 'http://localhost:3000'; export const register = async (username, password) => { try { const response = await axios.post(`${API_URL}/auth/register`, { username, password }); return response.data; } catch (error) { throw error.response.data; } }; export const login = async (username, password) => { try { const response = await axios.post(`${API_URL}/auth/login`, { username, password }); return response.data; } catch (error) { throw error.response.data; } }; export const getProfile = async (token) => { try { const response = await axios.get(`${API_URL}/auth/me`, { headers: { Authorization: `Bearer ${token}`, }, }); return response.data; } catch (error) { throw error.response.data; } }; export const updateProfile = async (token, userData) => { try { const response = await axios.put(`${API_URL}/auth/me`, userData, { headers: { Authorization: `Bearer ${token}`, }, }); return response.data; } catch (error) { throw error.response.data; } }; export const createChat = async (token, participants) => { try { const response = await axios.post(`${API_URL}/chats`, { participants }, { headers: { Authorization: `Bearer ${token}`, }, }); return response.data; } catch (error) { throw error.response.data; } }; export const getChats = async (token) => { try { const response = await axios.get(`${API_URL}/chats`, { headers: { Authorization: `Bearer ${token}`, }, }); return response.data; } catch (error) { throw error.response.data; } }; export const getChatDetails = async (token, chatId) => { try { const response = await axios.get(`${API_URL}/chats/${chatId}`, { headers: { Authorization: `Bearer ${token}`, }, }); return response.data; } catch (error) { throw error.response.data; } }; export const sendMessage = async (token, chatId, content) => { try { const response = await axios.post(`${API_URL}/chats/${chatId}/messages`, { content }, { headers: { Authorization: `Bearer ${token}`, }, }); return response.data; } catch (error) { throw error.response.data; } }; export
-
Create API Functions for File Upload and Download (
const uploadFile = async (token, chatId, file) => {
try {
const formData = new FormData();
formData.append('file', file);
const response = await axios.post(`${API_URL}/chats/${chatId}/files`, formData, {
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'multipart/form-data',
},
});
return response.data;
} catch (error) {
throw error.response.data;
}
};
export const downloadFile = async (token, chatId, fileId) => {
try {
const response = await axios.get(`${API_URL}/chats/${chatId}/files/${fileId}`, {
headers: {
Authorization: `Bearer ${token}`,
},
responseType: 'blob',
});
return response.data;
} catch (error) {
throw error.response.data;
}
};
export const logout = () => {
localStorage.removeItem('token');
window.location.href = '/login'; // Redirect to login page
};
```
-
File Upload Component:
-
Create File Upload Component (
src/components/FileUpload.js
):
import { useState } from 'react'; import { uploadFile } from '../services/api'; export default function FileUpload({ chatId }) { const [file, setFile] = useState(null); const [message, setMessage] = useState(''); const handleFileChange = (e) => { setFile(e.target.files[0]); }; const handleUpload = async (e) => { e.preventDefault(); try { const token = localStorage.getItem('token'); await uploadFile(token, chatId, file); setMessage('File uploaded successfully'); setFile(null); } catch (err) { setMessage(`Error: ${err.message}`); } }; return ( <div className="mb-4"> <form onSubmit={handleUpload}> <input type="file" onChange={handleFileChange} className="mb-2" /> <button type="submit" className="bg-blue-500 text-white px-4 py-2 rounded"> Upload File </button> </form> {message && <p className="mt-2">{message}</p>} </div> ); }
-
Create File Upload Component (
-
File Download Component:
-
Create File Download Component (
src/components/FileDownload.js
):
import { downloadFile } from '../services/api'; export default function FileDownload({ chatId, file }) { const handleDownload = async () => { try { const token = localStorage.getItem('token'); const fileData = await downloadFile(token, chatId, file._id); const url = window.URL.createObjectURL(new Blob([fileData])); const link = document.createElement('a'); link.href = url; link.setAttribute('download', file.filename); document.body.appendChild(link); link.click(); } catch (err) { console.error('Error downloading file:', err); } }; return ( <div className="mb-2"> <button onClick={handleDownload} className="bg-green-500 text-white px-4 py-2 rounded"> Download {file.filename} </button> </div> ); }
-
Create File Download Component (
-
Chat Window with File Sharing:
-
Update Chat Window Component to Include File Upload and Download (
src/components/ChatWindow.js
):
import { useState } from 'react'; import useChat from '../hooks/useChat'; import FileUpload from './FileUpload'; import FileDownload from './FileDownload'; export default function ChatWindow({ chatId, userId }) { const { messages, message, setMessage, sendMessage } = useChat(chatId); const [files, setFiles] = useState([]); const handleSendMessage = (e) => { e.preventDefault(); sendMessage(userId, message); }; return ( <div className="max-w-md mx-auto mt-10"> <h2 className="text-2xl font-bold mb-5">Chat</h2> <div> <ul className="mb-5"> {messages.map((msg, index) => ( <li key={index} className="mb-2 p-2 border rounded"> <strong>{msg.sender.username}</strong>: {msg.content} <br /> <span className="text-gray-500 text-sm">{new Date(msg.timestamp).toLocaleString()}</span> </li> ))} </ul> <form onSubmit={handleSendMessage}> <input type="text" value={message} onChange={(e) => setMessage(e.target.value)} className="w-full px-3 py-2 border rounded mb-2" placeholder="Type your message..." /> <button type="submit" className="bg-blue-500 text-white px-4 py-2 rounded"> Send </button> </form> <FileUpload chatId={chatId} /> <div className="mt-4"> {files.map(file => ( <FileDownload key={file._id} chatId={chatId} file={file} /> ))} </div> </div> </div> ); }
-
Update Chat Window Component to Include File Upload and Download (
-
Usage in a Page:
-
Update Page to Include File Sharing (
src/pages/chat/[chatId].js
):
import { useRouter } from 'next/router'; import ChatWindow from '../../components/ChatWindow'; export default function ChatPage() { const router = useRouter(); const { chatId } = router.query; const userId = 'USER_ID'; // Replace with the actual user ID from authentication if (!chatId) { return <div>Loading...</div>; } return <ChatWindow chatId={chatId} userId={userId} />; }
-
Update Page to Include File Sharing (
Conclusion
This setup includes the backend implementation for uploading and downloading files in a chat session with NestJS, and the frontend implementation with Next.js. Users can upload files to a chat session, and download them from the chat session. The FileUpload
component handles file uploads, and the FileDownload
component handles file downloads. The ChatWindow
component is updated to include file upload and download functionalities, ensuring comprehensive file sharing capabilities in your chat application.
Settings: User Settings Page
Frontend Code (Next.js)
-
Create User Settings Page:
-
Create User Settings Page Component (
src/pages/settings.js
):
import { useState, useEffect } from 'react'; import { getProfile, updateProfile } from '../services/api'; export default function SettingsPage() { const [username, setUsername] = useState(''); const [notificationPreference, setNotificationPreference] = useState(true); const [message, setMessage] = useState(''); const [error, setError] = useState(''); useEffect(() => { const fetchProfile = async () => { try { const token = localStorage.getItem('token'); if (token) { const data = await getProfile(token); setUsername(data.username); setNotificationPreference(data.notificationPreference || true); } else { setError('No token found'); } } catch (err) { setError(err.message); } }; fetchProfile(); }, []); const handleSave = async (e) => { e.preventDefault(); try { const token = localStorage.getItem('token'); const updateData = { username, notificationPreference }; await updateProfile(token, updateData); setMessage('Settings updated successfully'); } catch (err) { setError(err.message); } }; return ( <div className="max-w-md mx-auto mt-10"> <h2 className="text-2xl font-bold mb-5">User Settings</h2> {error && <p className="text-red-500">{error}</p>} {message && <p className="text-green-500">{message}</p>} <form onSubmit={handleSave} className="space-y-4"> <div> <label className="block text-gray-700">Username</label> <input type="text" value={username} onChange={(e) => setUsername(e.target.value)} className="w-full px-3 py-2 border rounded" required /> </div> <div> <label className="block text-gray-700">Notification Preferences</label> <select value={notificationPreference} onChange={(e) => setNotificationPreference(e.target.value === 'true')} className="w-full px-3 py-2 border rounded" > <option value="true">Enable Notifications</option> <option value="false">Disable Notifications</option> </select> </div> <div> <button type="submit" className="w-full px-4 py-2 text-white bg-blue-500 rounded hover:bg-blue-700" > Save Settings </button> </div> </form> </div> ); }
-
Create User Settings Page Component (
-
API Call for Updating Profile:
-
Update API Function for Updating Profile (
src/services/api.js
):
import axios from 'axios'; const API_URL = 'http://localhost:3000'; export const register = async (username, password) => { try { const response = await axios.post(`${API_URL}/auth/register`, { username, password }); return response.data; } catch (error) { throw error.response.data; } }; export const login = async (username, password) => { try { const response = await axios.post(`${API_URL}/auth/login`, { username, password }); return response.data; } catch (error) { throw error.response.data; } }; export const getProfile = async (token) => { try { const response = await axios.get(`${API_URL}/auth/me`, { headers: { Authorization: `Bearer ${token}`, }, }); return response.data; } catch (error) { throw error.response.data; } }; export const updateProfile = async (token, userData) => { try { const response = await axios.put(`${API_URL}/auth/me`, userData, { headers: { Authorization: `Bearer ${token}`, }, }); return response.data; } catch (error) { throw error.response.data; } }; export const createChat = async (token, participants) => { try { const response = await axios.post(`${API_URL}/chats`, { participants }, { headers: { Authorization: `Bearer ${token}`, }, }); return response.data; } catch (error) { throw error.response.data; } }; export const getChats = async (token) => { try { const response = await axios.get(`${API_URL}/chats`, { headers: { Authorization: `Bearer ${token}`, }, }); return response.data; } catch (error) { throw error.response.data; } }; export const getChatDetails = async (token, chatId) => { try { const response = await axios.get(`${API_URL}/chats/${chatId}`, { headers: { Authorization: `Bearer ${token}`, }, }); return response.data; } catch (error) { throw error.response.data; } }; export const sendMessage = async (token, chatId, content) => { try { const response = await axios.post(`${API_URL}/chats/${chatId}/messages`, { content }, { headers: { Authorization: `Bearer ${token}`, }, }); return response.data; } catch (error) { throw error.response.data; } }; export const uploadFile = async (token, chatId, file) => { try { const formData = new FormData(); formData.append('file', file); const response = await axios.post(`${API_URL}/chats/${chatId}/files`, formData, { headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'multipart/form-data', }, }); return response.data; } catch (error) { throw error.response.data; } }; export const downloadFile = async (token, chatId, fileId) => { try { const response = await axios.get(`${API_URL}/chats/${chatId}/files/${fileId}`, { headers: { Authorization: `Bearer ${token}`, }, responseType: 'blob', }); return response.data; } catch (error) { throw error.response.data; } }; export const logout = () => { localStorage.removeItem('token'); window.location.href = '/login'; // Redirect to login page };
-
Update API Function for Updating Profile (
Conclusion
This setup includes the frontend implementation for a user settings page using Next.js and Tailwind CSS. The user settings page allows users to update their username and notification preferences. The settings are fetched from and updated to the backend using appropriate API calls. This ensures that users can manage their account settings and preferences effectively.
Future Enhancements
1. Voice and Video Calls
Description: Adding support for voice and video calls.
Implementation Plan:
-
WebRTC Integration:
- Use WebRTC for real-time communication.
- Set up signaling server to manage WebRTC connections.
-
Signaling Server:
- Implement signaling server using Socket.io or any other preferred WebSocket library.
- Handle offer, answer, and ICE candidate exchange.
-
Frontend UI:
- Add buttons for voice and video calls.
- Create components for managing WebRTC streams (e.g.,
<VideoCall />
,<VoiceCall />
).
-
Backend API:
- Add endpoints for initiating and ending calls.
- Store call history and status.
Frontend Code Example:
// src/components/VideoCall.js
import React, { useRef, useEffect, useState } from 'react';
import io from 'socket.io-client';
const VideoCall = ({ userId, chatId }) => {
const [localStream, setLocalStream] = useState(null);
const [remoteStream, setRemoteStream] = useState(null);
const localVideoRef = useRef();
const remoteVideoRef = useRef();
const socket = useRef(null);
const peerConnection = useRef(null);
useEffect(() => {
socket.current = io('http://localhost:3000');
navigator.mediaDevices.getUserMedia({ video: true, audio: true })
.then(stream => {
setLocalStream(stream);
localVideoRef.current.srcObject = stream;
});
socket.current.on('offer', handleOffer);
socket.current.on('answer', handleAnswer);
socket.current.on('ice-candidate', handleICECandidate);
return () => {
socket.current.disconnect();
if (peerConnection.current) {
peerConnection.current.close();
}
};
}, []);
const handleOffer = async (offer) => {
peerConnection.current = createPeerConnection();
await peerConnection.current.setRemoteDescription(new RTCSessionDescription(offer));
const answer = await peerConnection.current.createAnswer();
await peerConnection.current.setLocalDescription(answer);
socket.current.emit('answer', { answer, chatId });
};
const handleAnswer = async (answer) => {
await peerConnection.current.setRemoteDescription(new RTCSessionDescription(answer));
};
const handleICECandidate = (candidate) => {
if (peerConnection.current) {
peerConnection.current.addIceCandidate(new RTCIceCandidate(candidate));
}
};
const createPeerConnection = () => {
const pc = new RTCPeerConnection();
pc.onicecandidate = (event) => {
if (event.candidate) {
socket.current.emit('ice-candidate', { candidate: event.candidate, chatId });
}
};
pc.ontrack = (event) => {
setRemoteStream(event.streams[0]);
remoteVideoRef.current.srcObject = event.streams[0];
};
localStream.getTracks().forEach(track => {
pc.addTrack(track, localStream);
});
return pc;
};
const startCall = async () => {
peerConnection.current = createPeerConnection();
const offer = await peerConnection.current.createOffer();
await peerConnection.current.setLocalDescription(offer);
socket.current.emit('offer', { offer, chatId });
};
return (
<div>
<video ref={localVideoRef} autoPlay muted />
<video ref={remoteVideoRef} autoPlay />
<button onClick={startCall}>Start Call</button>
</div>
);
};
export default VideoCall;
2. Group Chats
Description: Adding support for group chat functionality.
Implementation Plan:
-
Database Schema:
- Update chat schema to include group chat properties (e.g., group name, members).
- Modify existing chat schema to distinguish between individual and group chats.
-
Backend API:
- Create endpoints for creating, updating, and deleting group chats.
- Handle adding and removing members from group chats.
-
Frontend UI:
- Add UI for creating and managing group chats.
- Update chat list and chat window components to support group chats.
Backend Code Example:
// src/chat/chat.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { Chat, ChatDocument } from '../schemas/chat.schema';
import { CreateGroupChatDto } from './dto/create-group-chat.dto';
import { AddMembersDto } from './dto/add-members.dto';
@Injectable()
export class ChatService {
constructor(@InjectModel(Chat.name) private chatModel: Model<ChatDocument>) {}
async createGroupChat(createGroupChatDto: CreateGroupChatDto): Promise<Chat> {
const newGroupChat = new this.chatModel({
...createGroupChatDto,
isGroupChat: true,
messages: [],
});
return newGroupChat.save();
}
async addMembers(chatId: string, addMembersDto: AddMembersDto): Promise<Chat> {
const chat = await this.chatModel.findById(chatId);
if (!chat || !chat.isGroupChat) {
throw new NotFoundException('Group chat not found');
}
chat.members.push(...addMembersDto.members);
return chat.save();
}
}
3. Status Indicators
Description: Adding online/offline status indicators for users.
Implementation Plan:
-
Backend WebSocket Integration:
- Track user connection and disconnection events.
- Store user status in a database or in-memory store.
-
Frontend UI:
- Add UI elements to display user status (e.g., green dot for online, gray dot for offline).
- Update user list and chat components to show status indicators.
Backend Code Example:
// src/chat/chat.gateway.ts
import {
WebSocketGateway,
WebSocketServer,
OnGatewayInit,
OnGatewayConnection,
OnGatewayDisconnect,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
import { UserService } from '../user/user.service';
@WebSocketGateway()
export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect {
@WebSocketServer() server: Server;
constructor(private readonly userService: UserService) {}
afterInit(server: Server) {
console.log('WebSocket server initialized');
}
async handleConnection(client: Socket) {
const userId = client.handshake.query.userId;
await this.userService.updateUserStatus(userId, true);
this.server.emit('userStatus', { userId, status: 'online' });
}
async handleDisconnect(client: Socket) {
const userId = client.handshake.query.userId;
await this.userService.updateUserStatus(userId, false);
this.server.emit('userStatus', { userId, status: 'offline' });
}
}
Frontend Code Example:
// src/components/UserList.js
import { useEffect, useState } from 'react';
import io from 'socket.io-client';
export default function UserList() {
const [users, setUsers] = useState([]);
useEffect(() => {
const socket = io('http://localhost:3000');
socket.on('userStatus', (data) => {
setUsers((prevUsers) =>
prevUsers.map((user) =>
user._id === data.userId ? { ...user, status: data.status } : user
)
);
});
return () => socket.disconnect();
}, []);
return (
<div>
<h2 className="text-2xl font-bold mb-5">Users</h2>
<ul>
{users.map((user) => (
<li key={user._id} className="flex items-center mb-2">
<span
className={`w-2.5 h-2.5 rounded-full ${
user.status === 'online' ? 'bg-green-500' : 'bg-gray-500'
}`}
></span>
<span className="ml-2">{user.username}</span>
</li>
))}
</ul>
</div>
);
}
Conclusion
These enhancements outline the future features of the chat application, including voice and video calls, group chat functionality, and user status indicators. Each enhancement involves both backend and frontend changes to ensure seamless integration and improved user experience.
Implementing Google Meet or Zoom-like Meeting Feature with Sharable Link
To implement a Google Meet or Zoom-like meeting feature with a sharable link, we can use WebRTC for real-time video and audio communication. Additionally, we can use Socket.io for signaling and managing the connections. Here's a high-level overview and implementation guide:
Backend Code (NestJS)
- WebSocket Gateway Setup
- Install WebSocket Dependencies:
npm install @nestjs/websockets @nestjs/platform-socket.io
-
Create WebSocket Gateway:
-
Create WebSocket Gateway (
src/meeting/meeting.gateway.ts
):
import { SubscribeMessage, WebSocketGateway, WebSocketServer, OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect, } from '@nestjs/websockets'; import { Server, Socket } from 'socket.io'; @WebSocketGateway() export class MeetingGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect { @WebSocketServer() server: Server; afterInit(server: Server) { console.log('WebSocket server initialized'); } handleConnection(client: Socket) { console.log(`Client connected: ${client.id}`); } handleDisconnect(client: Socket) { console.log(`Client disconnected: ${client.id}`); } @SubscribeMessage('joinMeeting') handleJoinMeeting(client: Socket, meetingId: string) { client.join(meetingId); client.broadcast.to(meetingId).emit('userJoined', client.id); } @SubscribeMessage('signal') handleSignal(client: Socket, payload: { meetingId: string; signal: any }) { client.broadcast.to(payload.meetingId).emit('signal', { userId: client.id, signal: payload.signal, }); } }
-
Create WebSocket Gateway (
-
Meeting Module:
-
Create Meeting Module (
src/meeting/meeting.module.ts
):
import { Module } from '@nestjs/common'; import { MeetingGateway } from './meeting.gateway'; @Module({ providers: [MeetingGateway], }) export class MeetingModule {}
-
Create Meeting Module (
-
Update App Module:
-
Update App Module to Include Meeting Module (
src/app.module.ts
):
import { Module } from '@nestjs/common'; import { MongooseModule } from '@nestjs/mongoose'; import { AuthModule } from './auth/auth.module'; import { ChatModule } from './chat/chat.module'; import { FileModule } from './file/file.module'; import { MeetingModule } from './meeting/meeting.module'; @Module({ imports: [ MongooseModule.forRoot('mongodb://localhost/chat-app'), AuthModule, ChatModule, FileModule, MeetingModule, ], }) export class AppModule {}
-
Update App Module to Include Meeting Module (
Frontend Code (Next.js)
- Install Socket.io Client:
npm install socket.io-client
-
WebRTC Setup:
-
Create WebRTC Hook (
src/hooks/useWebRTC.js
):
import { useEffect, useRef, useState } from 'react'; import io from 'socket.io-client'; const useWebRTC = (meetingId) => { const [remoteStreams, setRemoteStreams] = useState([]); const localStream = useRef(null); const socket = useRef(null); const peerConnections = useRef({}); useEffect(() => { socket.current = io('http://localhost:3000'); navigator.mediaDevices.getUserMedia({ video: true, audio: true }) .then(stream => { localStream.current.srcObject = stream; socket.current.emit('joinMeeting', meetingId); socket.current.on('userJoined', userId => { const peerConnection = createPeerConnection(userId); peerConnections.current[userId] = peerConnection; stream.getTracks().forEach(track => peerConnection.addTrack(track, stream)); }); socket.current.on('signal', async ({ userId, signal }) => { if (peerConnections.current[userId]) { await peerConnections.current[userId].setRemoteDescription(new RTCSessionDescription(signal)); if (signal.type === 'offer') { const answer = await peerConnections.current[userId].createAnswer(); await peerConnections.current[userId].setLocalDescription(answer); socket.current.emit('signal', { meetingId, signal: answer }); } } else { const peerConnection = createPeerConnection(userId); await peerConnection.setRemoteDescription(new RTCSessionDescription(signal)); peerConnections.current[userId] = peerConnection; if (signal.type === 'offer') { const answer = await peerConnection.createAnswer(); await peerConnection.setLocalDescription(answer); socket.current.emit('signal', { meetingId, signal: answer }); } } }); }); return () => { Object.values(peerConnections.current).forEach(pc => pc.close()); socket.current.disconnect(); }; }, [meetingId]); const createPeerConnection = (userId) => { const pc = new RTCPeerConnection(); pc.onicecandidate = (event) => { if (event.candidate) { socket.current.emit('signal', { meetingId, signal: event.candidate }); } }; pc.ontrack = (event) => { setRemoteStreams((prevStreams) => { const existingStream = prevStreams.find(stream => stream.id === event.streams[0].id); if (existingStream) return prevStreams; return [...prevStreams, event.streams[0]]; }); }; return pc; }; return { localStream, remoteStreams }; }; export default useWebRTC;
-
Create WebRTC Hook (
-
Meeting Component:
-
Create Meeting Component (
src/components/Meeting.js
):
import React, { useRef } from 'react'; import useWebRTC from '../hooks/useWebRTC'; const Meeting = ({ meetingId }) => { const { localStream, remoteStreams } = useWebRTC(meetingId); const localVideoRef = useRef(); useEffect(() => { if (localStream.current) { localVideoRef.current.srcObject = localStream.current.srcObject; } }, [localStream]); return ( <div> <video ref={localVideoRef} autoPlay muted className="local-video" /> <div className="remote-videos"> {remoteStreams.map((stream, index) => ( <video key={index} autoPlay className="remote-video" ref={video => { if (video) { video.srcObject = stream; } }} /> ))} </div> </div> ); }; export default Meeting;
-
Create Meeting Component (
-
Usage in a Page:
-
Create Meeting Page (
src/pages/meeting/[meetingId].js
):
import { useRouter } from 'next/router'; import Meeting from '../../components/Meeting'; export default function MeetingPage() { const router = useRouter(); const { meetingId } = router.query; if (!meetingId) { return <div>Loading...</div>; } return <Meeting meetingId={meetingId} />; }
-
Create Meeting Page (
Conclusion
This setup provides a basic implementation of a Google Meet or Zoom-like meeting feature with a sharable link using WebRTC for real-time communication and Socket.io for signaling. The backend uses NestJS to manage WebSocket connections, and the frontend uses Next.js to handle the video call UI and WebRTC interactions. This implementation can be further extended with additional features such as screen sharing, chat, and more.
Adding Screen Sharing Feature
To add a screen sharing feature, we can use the getDisplayMedia
method from the WebRTC API. This allows users to share their screen during the video call.
Frontend Code (Next.js)
-
Update WebRTC Hook:
-
Enhance the
useWebRTC
hook to include screen sharing functionality (src/hooks/useWebRTC.js
):
import { useEffect, useRef, useState } from 'react'; import io from 'socket.io-client'; const useWebRTC = (meetingId) => { const [remoteStreams, setRemoteStreams] = useState([]); const localStream = useRef(null); const screenStream = useRef(null); const socket = useRef(null); const peerConnections = useRef({}); useEffect(() => { socket.current = io('http://localhost:3000'); navigator.mediaDevices.getUserMedia({ video: true, audio: true }) .then(stream => { localStream.current = stream; attachStreamToVideo(localStream.current, 'local-video'); socket.current.emit('joinMeeting', meetingId); socket.current.on('userJoined', userId => { const peerConnection = createPeerConnection(userId); peerConnections.current[userId] = peerConnection; stream.getTracks().forEach(track => peerConnection.addTrack(track, stream)); }); socket.current.on('signal', async ({ userId, signal }) => { if (peerConnections.current[userId]) { await peerConnections.current[userId].setRemoteDescription(new RTCSessionDescription(signal)); if (signal.type === 'offer') { const answer = await peerConnections.current[userId].createAnswer(); await peerConnections.current[userId].setLocalDescription(answer); socket.current.emit('signal', { meetingId, signal: answer }); } } else { const peerConnection = createPeerConnection(userId); await peerConnection.setRemoteDescription(new RTCSessionDescription(signal)); peerConnections.current[userId] = peerConnection; if (signal.type === 'offer') { const answer = await peerConnection.createAnswer(); await peerConnection.setLocalDescription(answer); socket.current.emit('signal', { meetingId, signal: answer }); } } }); }); return () => { Object.values(peerConnections.current).forEach(pc => pc.close()); socket.current.disconnect(); }; }, [meetingId]); const createPeerConnection = (userId) => { const pc = new RTCPeerConnection(); pc.onicecandidate = (event) => { if (event.candidate) { socket.current.emit('signal', { meetingId, signal: event.candidate }); } }; pc.ontrack = (event) => { setRemoteStreams((prevStreams) => { const existingStream = prevStreams.find(stream => stream.id === event.streams[0].id); if (existingStream) return prevStreams; return [...prevStreams, event.streams[0]]; }); }; return pc; }; const attachStreamToVideo = (stream, videoId) => { const videoElement = document.getElementById(videoId); if (videoElement) { videoElement.srcObject = stream; } }; const startScreenSharing = async () => { try { screenStream.current = await navigator.mediaDevices.getDisplayMedia({ video: true }); const screenTrack = screenStream.current.getVideoTracks()[0]; Object.values(peerConnections.current).forEach(pc => { const sender = pc.getSenders().find(s => s.track.kind === 'video'); if (sender) { sender.replaceTrack(screenTrack); } }); screenTrack.onended = () => { stopScreenSharing(); }; } catch (error) { console.error('Error starting screen sharing:', error); } }; const stopScreenSharing = () => { const videoTrack = localStream.current.getVideoTracks()[0]; Object.values(peerConnections.current).forEach(pc => { const sender = pc.getSenders().find(s => s.track.kind === 'video'); if (sender) { sender.replaceTrack(videoTrack); } }); screenStream.current.getTracks().forEach(track => track.stop()); screenStream.current = null; }; return { localStream, remoteStreams, startScreenSharing, stopScreenSharing }; }; export default useWebRTC;
-
Enhance the
-
Update Meeting Component:
-
Update the
Meeting
component to include screen sharing buttons (src/components/Meeting.js
):
import React, { useRef, useEffect } from 'react'; import useWebRTC from '../hooks/useWebRTC'; const Meeting = ({ meetingId }) => { const { localStream, remoteStreams, startScreenSharing, stopScreenSharing } = useWebRTC(meetingId); const localVideoRef = useRef(); useEffect(() => { if (localStream.current) { localVideoRef.current.srcObject = localStream.current.srcObject; } }, [localStream]); return ( <div> <video ref={localVideoRef} autoPlay muted id="local-video" className="local-video" /> <div className="remote-videos"> {remoteStreams.map((stream, index) => ( <video key={index} autoPlay className="remote-video" ref={video => { if (video) { video.srcObject = stream; } }} /> ))} </div> <button onClick={startScreenSharing} className="bg-blue-500 text-white px-4 py-2 rounded">Share Screen</button> <button onClick={stopScreenSharing} className="bg-red-500 text-white px-4 py-2 rounded">Stop Sharing</button> </div> ); }; export default Meeting;
-
Update the
-
Usage in a Page:
-
Ensure the meeting page uses the updated
Meeting
component (src/pages/meeting/[meetingId].js
):
import { useRouter } from 'next/router'; import Meeting from '../../components/Meeting'; export default function MeetingPage() { const router = useRouter(); const { meetingId } = router.query; if (!meetingId) { return <div>Loading...</div>; } return <Meeting meetingId={meetingId} />; }
-
Ensure the meeting page uses the updated
Backend Code (NestJS)
-
No Change Required for Backend:
- The existing WebSocket gateway setup remains the same, as the signaling process for screen sharing is handled similarly to regular video/audio streams. The
signal
event will carry the necessary signaling data for screen sharing as well.
- The existing WebSocket gateway setup remains the same, as the signaling process for screen sharing is handled similarly to regular video/audio streams. The
Conclusion
This setup includes the frontend implementation for a screen sharing feature using WebRTC and Next.js. The useWebRTC
hook is enhanced to manage screen sharing, allowing users to start and stop screen sharing during a video call. The Meeting
component includes buttons to control screen sharing. This feature enhances the overall meeting experience by enabling users to share their screens seamlessly.
If you enjoy my content and would like to support my work, you can buy me a coffee. Your support is greatly appreciated!
Disclaimer: This content is generated by AI.
Top comments (0)