Skip to content

Commit

Permalink
feat: completed and tested the messaging feature
Browse files Browse the repository at this point in the history
  • Loading branch information
alexindevs committed Oct 16, 2024
1 parent 1947d8c commit 140b9de
Show file tree
Hide file tree
Showing 9 changed files with 378 additions and 65 deletions.
2 changes: 2 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
import helmet from 'helmet';
import { xssMiddleware } from './core/middlewares/xss.middleware';
import { IoAdapter } from '@nestjs/platform-socket.io';

async function bootstrap() {
const app = await NestFactory.create(AppModule);
Expand All @@ -26,6 +27,7 @@ async function bootstrap() {
);

app.enableCors({ origin: '*' });
app.useWebSocketAdapter(new IoAdapter(app));
app.use(xssMiddleware);

await app.listen(process.env.PORT || 3000);
Expand Down
6 changes: 6 additions & 0 deletions src/modules/authentication/guards/active-user.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ export class ActiveUserGuard implements CanActivate {
: new UnauthorizedException('User not found');
}

if (!user.verified) {
throw isWs
? new WsException('User not verified')
: new UnauthorizedException('User not verified');
}

if (user.account_deactivated) {
throw isWs
? new WsException('Account is deactivated')
Expand Down
60 changes: 38 additions & 22 deletions src/modules/authentication/guards/jwt-auth.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,23 @@ import { WsException } from '@nestjs/websockets';
export class JwtAuthGuard implements CanActivate {
constructor(private readonly accessTokenService: AccessTokenService) {}

async canActivate(
context: ExecutionContext,
): Promise<boolean> {
if (context.getType() === 'http') {
const request = context.switchToHttp().getRequest<Request>();
return this.validateHttpRequest(request);
} else if (context.getType() === 'ws') {
const client = context.switchToWs().getClient<Socket>();
return this.validateWsRequest(client);
async canActivate(context: ExecutionContext): Promise<boolean> {
try {
if (context.getType() === 'http') {
const request = context.switchToHttp().getRequest<Request>();
return this.validateHttpRequest(request);
} else if (context.getType() === 'ws') {
const client = context.switchToWs().getClient<Socket>();
console.log('Client connected:', client.id);
return this.validateWsRequest(client);
}
return false; // Default for unknown context
} catch (error) {
if (context.getType() === 'ws') {
throw new WsException(error.message);
}
throw error; // For HTTP, we can throw the original error
}
return false; // Default for unknown context
}

private async validateHttpRequest(request: Request): Promise<boolean> {
Expand All @@ -35,30 +41,40 @@ export class JwtAuthGuard implements CanActivate {
}

const token = authHeader.split(' ')[1];
const { isValid, payload } = this.accessTokenService.verifyAccessToken(token);

if (!isValid) {
throw new UnauthorizedException('Invalid token');
}

request['user'] = payload; // Attach the payload to the request object
return true;
return this.validateToken(token, (payload) => {
request['user'] = payload;
});
}

private async validateWsRequest(client: Socket): Promise<boolean> {
const token = client.handshake.query.token as string;
console.log('Token:', token);

if (!token) {
throw new WsException('Missing authentication token');
}

const { isValid, payload } = this.accessTokenService.verifyAccessToken(token);
return this.validateToken(token, (payload) => {
client['user'] = payload;
});
}

private async validateToken(
token: string,
attachPayload: (payload: any) => void,
): Promise<boolean> {
if (!this.accessTokenService) {
throw new Error('AccessTokenService is undefined');
}

const { isValid, payload } = await this.accessTokenService.verifyAccessToken(token);

if (!isValid) {
throw new WsException('Invalid token');
throw new UnauthorizedException('Invalid token');
}

client['user'] = payload; // Attach the payload to the client object
attachPayload(payload);
return true;
}

}
105 changes: 81 additions & 24 deletions src/modules/direct-messaging/direct-messaging.gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,51 +8,80 @@ import {
MessageBody,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
import { JwtAuthGuard } from '../authentication/guards//jwt-auth.guard'; // Import your guard
import { JwtAuthGuard } from '../authentication/guards/jwt-auth.guard';
import { DirectMessagingService } from './direct-messaging.service';
import { ConnectionService } from '../connections/connections.service';
import { ListingType } from './direct-messaging.schema';
import {
ForbiddenException,
Logger,
UnauthorizedException,
UseGuards,
} from '@nestjs/common';

@WebSocketGateway({ cors: true, namespace: 'direct-messaging' })
@UseGuards(JwtAuthGuard)
import { UseWsGuards } from 'src/shared/decorators/use-ws-guard';
import { AccessTokenService } from '../authentication/tokens/accesstoken.service';

@WebSocketGateway({
cors: {
origin: '*',
methods: ['GET', 'POST'],
credentials: true,
allowedHeaders: ['Authorization', 'Content-Type'],
},
namespace: 'direct-messaging',
})
@UseWsGuards(JwtAuthGuard)
export class DirectMessagingGateway
implements OnGatewayConnection, OnGatewayDisconnect
{
@WebSocketServer() server: Server;
private readonly logger = new Logger(DirectMessagingGateway.name);

constructor(
private readonly directMessagingService: DirectMessagingService,
private readonly connectionService: ConnectionService,
private readonly accessTokenService: AccessTokenService,
) {}

async handleConnection(@ConnectedSocket() client: Socket) {
const userId = client['user'].userId;
try {
const token =
client.handshake.auth.token || (client.handshake.query.token as string);

if (!token) {
this.logger.warn(`No token provided for client: ${client.id}`);
throw new UnauthorizedException('Missing authentication token');
}

const { isValid, payload } =
this.accessTokenService.verifyAccessToken(token);

if (!isValid || !payload.userId) {
this.logger.warn(`Invalid token for client: ${client.id}`);
throw new UnauthorizedException('Invalid token');
}

const userId = payload.userId;

if (userId) {
await this.connectionService.addConnection(
userId,
client.id,
'messaging',
);
console.log(`Client connected: ${client.id}, User: ${userId}`);

// Join a room for the user's ID to allow direct messaging
client.join(userId);

// Send unread message count to the user
const unreadCount =
await this.directMessagingService.getUnreadMessageCount(userId);
client.emit('unreadMessageCount', unreadCount);
} catch (error) {
this.logger.error(`Connection error: ${error.message}`, error.stack);
client.emit('error', 'Authentication failed');
client.disconnect();
}
}

async handleDisconnect(@ConnectedSocket() client: Socket) {
console.log(`Client disconnected: ${client.id}`);
this.logger.log(`Client disconnected: ${client.id}`);
await this.connectionService.removeConnection(client.id, 'messaging');
}

Expand All @@ -66,7 +95,16 @@ export class DirectMessagingGateway
listingId?: string;
},
) {
const clientId = client['user'].userId;
const token =
client.handshake.auth.token || (client.handshake.query.token as string);
const { isValid, payload } =
this.accessTokenService.verifyAccessToken(token);

if (!isValid || !payload.userId) {
throw new UnauthorizedException('Invalid token');
}

const clientId = payload.userId;
if (!clientId || !data.userIds.includes(clientId)) {
throw new UnauthorizedException(
'You cannot create a conversation between other people',
Expand All @@ -83,7 +121,6 @@ export class DirectMessagingGateway
data.listingId,
);

// Notify all participants about the new conversation
data.userIds.forEach((userId) => {
this.server.to(userId).emit('newConversation', conversation);
});
Expand All @@ -96,7 +133,16 @@ export class DirectMessagingGateway
@ConnectedSocket() client: Socket,
@MessageBody() data: { conversationId: string; content: string },
) {
const userId = client.handshake.query.userId as string;
const token =
client.handshake.auth.token || (client.handshake.query.token as string);
const { isValid, payload } =
this.accessTokenService.verifyAccessToken(token);

if (!isValid || !payload.userId) {
throw new UnauthorizedException('Invalid token');
}

const userId = payload.userId;
const message = await this.directMessagingService.sendMessage(
data.conversationId,
userId,
Expand All @@ -107,12 +153,10 @@ export class DirectMessagingGateway
data.conversationId,
);

// Send the message to all participants in the conversation
conversation.user_ids.forEach(async (participantId) => {
conversation.users.forEach(async (participantId) => {
if (participantId.toString() !== userId) {
this.server.to(participantId.toString()).emit('newMessage', message);

// Update the other client's unread count
const unreadCount =
await this.directMessagingService.getUnreadMessageCount(
participantId.toString(),
Expand Down Expand Up @@ -146,13 +190,21 @@ export class DirectMessagingGateway
@ConnectedSocket() client: Socket,
@MessageBody() data: { conversationId: string },
) {
const userId = client.handshake.query.userId as string;
const token =
client.handshake.auth.token || (client.handshake.query.token as string);
const { isValid, payload } =
this.accessTokenService.verifyAccessToken(token);

if (!isValid || !payload.userId) {
throw new UnauthorizedException('Invalid token');
}

const userId = payload.userId;
await this.directMessagingService.markMessagesAsRead(
data.conversationId,
userId,
);

// Notify the user about the updated unread count
const unreadCount =
await this.directMessagingService.getUnreadMessageCount(userId);
client.emit('unreadMessageCount', unreadCount);
Expand All @@ -161,21 +213,26 @@ export class DirectMessagingGateway
@SubscribeMessage('getUserConversations')
async handleGetUserConversations(
@ConnectedSocket() client: Socket,
@MessageBody() data: { page?: number; limit?: number }, // Add pagination data from the client
@MessageBody() data: { page?: number; limit?: number },
) {
const userId = client.handshake.query.userId as string;
const token =
client.handshake.auth.token || (client.handshake.query.token as string);
const { isValid, payload } =
this.accessTokenService.verifyAccessToken(token);

if (!isValid || !payload.userId) {
throw new UnauthorizedException('Invalid token');
}

// Destructure page and limit from the data sent by the client, with defaults
const userId = payload.userId;
const { page = 1, limit = 20 } = data;
// Use the new paginated method
const conversations =
await this.directMessagingService.getUserConversations(
userId,
page,
limit,
);

// Emit the paginated conversations to the client
client.emit('getUserConversationsResponse', conversations);
}
}
10 changes: 5 additions & 5 deletions src/modules/direct-messaging/direct-messaging.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ export enum ListingType {
@Schema()
export class Conversation {
@Prop({ type: [Types.ObjectId], ref: 'User', required: true }) // user_ids of participants
user_ids: Types.ObjectId[];
users: Types.ObjectId[];

@Prop({ type: Types.ObjectId, ref: 'RoomListing' }) // Optional: Room listing
room_listing_id?: Types.ObjectId;
room_listing?: Types.ObjectId;

@Prop({ type: Types.ObjectId, ref: 'RoommateListing' }) // Optional: Roommate listing
roommate_listing_id?: Types.ObjectId;
roommate_listing?: Types.ObjectId;

@Prop({ enum: ListingType, type: String, required: true }) // Specifies whether it's a room or roommate listing
listing_type: ListingType;
Expand All @@ -36,10 +36,10 @@ export type MessageDocument = HydratedDocument<Message>;
@Schema()
export class Message {
@Prop({ type: Types.ObjectId, ref: 'Conversation', required: true })
conversation_id: Types.ObjectId;
conversation: Types.ObjectId;

@Prop({ type: Types.ObjectId, ref: 'User', required: true }) // sender of the message
sender_id: Types.ObjectId;
sender: Types.ObjectId;

@Prop({ type: String, required: true }) // message content
content: string;
Expand Down
Loading

0 comments on commit 140b9de

Please sign in to comment.