How to create a full stack Chat App using Next js & Nest js?

RMAG news

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.

User Login:

Endpoint: /auth/login

Method: POST

Payload: { “username”: “string”, “password”: “string” }

Description: Authenticates a user and returns a JWT token.

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.

Update Profile:

Endpoint: /users/me

Method: PUT

Payload: { “username”: “string”, “password”: “string” }

Description: Updates the logged-in user’s profile information.

3. Chat Management

Create Chat:

Endpoint: /chats

Method: POST

Payload: { “participants”: [“userId1”, “userId2”] }

Description: Creates a new chat session between users.

Fetch Chats:

Endpoint: /chats

Method: GET

Description: Fetches all chat sessions for the logged-in user.

Fetch Chat Details:

Endpoint: /chats/:chatId

Method: GET

Description: Fetches messages in a specific chat session.

4. Messaging

Send Message:

Endpoint: /chats/:chatId/messages

Method: POST

Payload: { “content”: “string” }

Description: Sends a new message in a chat session.

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.

Download File:

Endpoint: /chats/:chatId/files/:fileId

Method: GET

Description: Downloads a file from a chat session.

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 and globals.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);

User DTO (Data Transfer Object):

Create DTO for User Registration (src/auth/dto/register-user.dto.ts):

export class RegisterUserDto {
username: string;
password: string;
}

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();
}
}

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);
}
}

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 {}

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;
}
};

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>
);
}

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;
}

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),
};
}
}

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);
}
}

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 };
}
}

JWT Payload Interface:

Create JWT Payload Interface (src/auth/jwt-payload.interface.ts):

export interface JwtPayload {
username: string;
sub: string;
}

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;
}
};

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>
);
}

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;
}

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 };
}
}

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);
}
}

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) {}

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 };
}
}

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
};

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>
);
}

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;
}

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();
}
}

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);
}
}

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
};

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>
);
}

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 DTO:

Create DTO for Creating Chat (src/chat/dto/create-chat.dto.ts):

export class CreateChatDto {
participants: string[];
}

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();
}
}

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);
}
}

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 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>
);
}

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();
}
}

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);
}
}

Ensure JWT Auth Guard is Applied:

Ensure that JwtAuthGuard is properly set up and imported as shown in previous examples.

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
};

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>
);
}

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();
}
}

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);
}
}

Ensure JWT Auth Guard is Applied:

Ensure that JwtAuthGuard is properly set up and imported as shown in previous examples.

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
};

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>
);
}

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} />;
}

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;
}

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);

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();
}
}

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);
}
}

Ensure JWT Auth Guard is Applied:

Ensure that JwtAuthGuard is properly set up and imported as shown in previous examples.

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
};

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

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} />;
}

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}`);
}
}

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();
}
}

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 {}

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;

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>
);
}

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} />;
}

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}`);
}
}

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();
}
}

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 {}

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;

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>
);
}

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} />;
}

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>
);
}

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
};

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>
);
}

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
};

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>
);
}

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>
);
}

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;

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} />;
}

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;

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>
);
}

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>
);
}

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} />;
}

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);

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);

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);
}
}

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));
}
}

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 {}

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 {}

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

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>
);
}

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>
);
}

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>
);
}

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} />;
}

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>
);
}

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
};

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,
});
}
}

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 {}

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 {}

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;

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;

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} />;
}

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;

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;

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} />;
}

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.

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.

Disclaimer: This content is generated by AI.