diff --git a/src/IOC/ioc.config.ts b/src/IOC/ioc.config.ts index 804b469..264acdf 100644 --- a/src/IOC/ioc.config.ts +++ b/src/IOC/ioc.config.ts @@ -58,7 +58,6 @@ import { ChatbotGateway } from "../modules/chatbot/chatbot.gateway"; import { ChatbotService } from "../modules/chatbot/providers/chatbot.service"; import { DataContextService } from "../modules/chatbot/providers/data-context.service"; import { LLMService } from "../modules/chatbot/providers/llm.service"; -import { WebSocketAuthService } from "../modules/chatbot/providers/websocket-auth.service"; import { ContactUsRepo, CreateContactUsRepo } from "../modules/contact-us/contactUs.repository"; import { ContactUsService } from "../modules/contact-us/contactUs.service"; import { CouponRepo, CouponUsageRepo, createCouponRepo, createCouponUsageRepo } from "../modules/Coupon/coupon.repository"; @@ -205,7 +204,6 @@ const containerModules = new AsyncContainerModule(async (bind) => { bind(IOCTYPES.ChatbotService).to(ChatbotService).inSingletonScope(); bind(IOCTYPES.ChatbotLLMService).to(LLMService).inSingletonScope(); bind(IOCTYPES.ChatbotDataContextService).to(DataContextService).inSingletonScope(); - bind(IOCTYPES.ChatbotWebSocketAuthService).to(WebSocketAuthService).inSingletonScope(); bind(IOCTYPES.ChatbotGateway).to(ChatbotGateway).inSingletonScope(); // #endregion // #region repository diff --git a/src/modules/chatbot/chatbot.gateway.ts b/src/modules/chatbot/chatbot.gateway.ts index fee9f52..acdc5c2 100644 --- a/src/modules/chatbot/chatbot.gateway.ts +++ b/src/modules/chatbot/chatbot.gateway.ts @@ -2,26 +2,22 @@ import { Server as HttpServer } from "http"; import { inject, injectable } from "inversify"; import { Server, Socket } from "socket.io"; - -import { ChatbotService } from "./providers/chatbot.service"; -import { WebSocketAuthService } from "./providers/websocket-auth.service"; -import { SendMessageDto } from "./DTO/send-message.dto"; -import { WEBSOCKET_EVENTS } from "./constants/chatbot.constants"; -import { IOCTYPES } from "../../IOC/ioc.types"; -import { Logger } from "../../core/logging/logger"; -import { AuthenticatedSocket, WebSocketResponse } from "./interfaces/websocket.interface"; import * as ulidLib from "ulid"; +import { WEBSOCKET_EVENTS } from "./constants/chatbot.constants"; +import { SendMessageDto } from "./DTO/send-message.dto"; +import { AuthenticatedSocket, WebSocketResponse } from "./interfaces/websocket.interface"; +import { ChatbotService } from "./providers/chatbot.service"; +import { Logger } from "../../core/logging/logger"; +import { IOCTYPES } from "../../IOC/ioc.types"; + @injectable() export class ChatbotGateway { private io: Server; private readonly logger: Logger; private readonly CHATBOT_ULID_COOKIE_NAME = "chatbot_session_id"; - constructor( - @inject(IOCTYPES.ChatbotService) private chatbotService: ChatbotService, - @inject(IOCTYPES.ChatbotWebSocketAuthService) private wsAuthService: WebSocketAuthService, - ) { + constructor(@inject(IOCTYPES.ChatbotService) private chatbotService: ChatbotService) { this.logger = new Logger("ChatbotGateway"); } @@ -43,28 +39,15 @@ export class ChatbotGateway { private async handleConnection(socket: Socket): Promise { try { - // Try to authenticate if token is provided - const token = socket.handshake.query.token as string; let ulid: string | undefined; - if (token) { - // Authenticated connection - const authResult = await this.wsAuthService.authenticateClient(socket as AuthenticatedSocket); - if (!authResult.success) { - this.wsAuthService.handleAuthenticationFailure(socket, authResult.error || "Authentication failed"); - return; - } - this.wsAuthService.emitAuthenticationSuccess(socket, authResult.user!); - ulid = authResult.user!.id; - } else { - // Anonymous connection - generate or get ULID from cookie - const cookieHeader = socket.handshake.headers.cookie; - ulid = this.extractUlidFromCookie(cookieHeader); - if (!ulid) { - ulid = ulidLib.ulid(); - } - socket.data = { user: { id: ulid, sub: ulid } }; + // Anonymous connection - generate or get ULID from cookie + const cookieHeader = socket.handshake.headers.cookie; + ulid = this.extractUlidFromCookie(cookieHeader); + if (!ulid) { + ulid = ulidLib.ulid(); } + socket.data = { user: { id: ulid, sub: ulid } }; this.logger.info(`Client connected: ${socket.id}, ULID: ${ulid}`); diff --git a/src/modules/chatbot/providers/data-context.service.ts b/src/modules/chatbot/providers/data-context.service.ts index 2263a7f..f2275ca 100644 --- a/src/modules/chatbot/providers/data-context.service.ts +++ b/src/modules/chatbot/providers/data-context.service.ts @@ -1,7 +1,8 @@ import { injectable } from "inversify"; -import { IChatContext } from "../interfaces/chatbot.interface"; import { Logger } from "../../../core/logging/logger"; +import { ProductModel } from "../../product/models/product.model"; +import { IChatContext } from "../interfaces/chatbot.interface"; /** * Service for retrieving relevant context data from the database @@ -20,25 +21,14 @@ export class DataContextService { const sources: string[] = []; try { - // Analyze message to determine what data to fetch - // const keywords = this._extractKeywords(_message); // TODO: Use when implementing keyword-based filtering + // Fetch all non-deleted products + const products = await ProductModel.find({ deleted: false }).select("title_fa title_en model description _id").lean(); - // TODO: Implement actual data fetching based on your project's models - // For now, this returns empty data - you can extend this to fetch: - // - Product data from ProductModel - // - Category data from CategoryModel - // - Order data from OrderModel - // - User data from UserModel - // etc. - - // Example: If you want to fetch product data - // if (this.hasProductKeywords(keywords)) { - // const products = await ProductModel.find({ ... }).limit(5); - // products.forEach((product) => { - // relevantData.push(`Product: ${product.title_fa} - Price: ${product.price}`); - // sources.push("Products Database"); - // }); - // } + products.forEach((product) => { + const productInfo = `Product ID: ${product._id}, Title (FA): ${product.title_fa}, Title (EN): ${product.title_en}, Model: ${product.model}${product.description ? `, Description: ${product.description.substring(0, 100)}` : ""}`; + relevantData.push(productInfo); + sources.push("Products Database"); + }); return { data: relevantData, sources }; } catch (error) { @@ -46,34 +36,4 @@ export class DataContextService { return { data: [], sources: [] }; } } - - // TODO: Use this method when implementing keyword-based filtering - // @ts-expect-error - Method reserved for future use - // eslint-disable-next-line @typescript-eslint/no-unused-vars - private _extractKeywords(_message: string): string[] { - const commonWords = ["the", "is", "at", "which", "on", "a", "an", "and", "or", "but", "in", "with", "to", "for", "of", "as", "by"]; - - return _message - .toLowerCase() - .replace(/[^\w\s]/g, "") - .split(/\s+/) - .filter((word: string) => word.length > 2 && !commonWords.includes(word)); - } - - // Helper methods for keyword detection - can be extended - // TODO: Implement when needed - // private hasProductKeywords(keywords: string[]): boolean { - // const productTerms = ["product", "item", "goods", "merchandise", "کالا", "محصول"]; - // return keywords.some(keyword => productTerms.includes(keyword)); - // } - - // private hasOrderKeywords(keywords: string[]): boolean { - // const orderTerms = ["order", "purchase", "buy", "cart", "سفارش", "خرید"]; - // return keywords.some(keyword => orderTerms.includes(keyword)); - // } - - // private hasCategoryKeywords(keywords: string[]): boolean { - // const categoryTerms = ["category", "type", "kind", "دسته", "دسته‌بندی"]; - // return keywords.some(keyword => categoryTerms.includes(keyword)); - // } } diff --git a/src/modules/chatbot/providers/websocket-auth.service.ts b/src/modules/chatbot/providers/websocket-auth.service.ts deleted file mode 100644 index 68595b5..0000000 --- a/src/modules/chatbot/providers/websocket-auth.service.ts +++ /dev/null @@ -1,203 +0,0 @@ -import { inject, injectable } from "inversify"; -import { Socket } from "socket.io"; - -import { WebSocketMessage } from "../../../common/enums/message.enum"; -import { jwtExpiredErr } from "../../../core/app/app.errors"; -import { Logger } from "../../../core/logging/logger"; -import { IOCTYPES } from "../../../IOC/ioc.types"; -import { TokenService } from "../../token/token.service"; -import { WEBSOCKET_EVENTS } from "../constants/chatbot.constants"; -import { AuthenticationResult, WebSocketErrorContext } from "../interfaces/websocket.interface"; - -/** - * Service responsible for WebSocket authentication logic - * Follows single responsibility principle and provides reusable authentication methods - */ -@injectable() -export class WebSocketAuthService { - private readonly logger: Logger; - - constructor(@inject(IOCTYPES.TokenService) private readonly tokenService: TokenService) { - this.logger = new Logger("WebSocketAuthService"); - } - - /** - * Extracts token from WebSocket client - */ - private extractTokenFromClient(client: Socket): string | null { - // Try to get token from query parameters - const token = client.handshake.query.token as string; - if (token) { - return token; - } - - // Try to get token from auth header - const authHeader = client.handshake.headers.authorization; - if (authHeader && typeof authHeader === "string") { - const parts = authHeader.split(" "); - if (parts.length === 2 && parts[0] === "Bearer") { - return parts[1]; - } - } - - return null; - } - - /** - * Authenticates a WebSocket client and returns the result - * - * @param client - Socket.IO client to authenticate - * @returns Promise with authentication result - */ - async authenticateClient(client: Socket): Promise { - try { - const token = this.extractTokenFromClient(client); - - if (!token) { - this.logAuthenticationAttempt(client, false, "No token provided"); - return { - success: false, - error: WebSocketMessage.AUTHENTICATION_REQUIRED, - }; - } - - const payload = this.tokenService.verifyToken(token); - - if (!payload?.sub) { - this.logAuthenticationAttempt(client, false, "Invalid token payload"); - return { - success: false, - error: WebSocketMessage.INVALID_TOKEN, - }; - } - - // Store user data in client (convert sub to id for compatibility) - client.data.user = { id: payload.sub, sub: payload.sub }; - - this.logAuthenticationAttempt(client, true, undefined, payload.sub); - - return { - success: true, - user: { id: payload.sub, sub: payload.sub }, - }; - } catch (error) { - const errorMessage = this.getAuthErrorMessage(error); - this.logAuthenticationAttempt(client, false, errorMessage); - - return { - success: false, - error: errorMessage, - }; - } - } - - /** - * Emits authentication failure event to client and disconnects - * - * @param client - Socket.IO client - * @param error - Error message to send - */ - handleAuthenticationFailure(client: Socket, error: string): void { - client.emit(WEBSOCKET_EVENTS.UNAUTHORIZED, { - message: error, - timestamp: new Date().toISOString(), - }); - - // Graceful disconnect with delay to ensure message is received - setTimeout(() => { - client.disconnect(true); - }, 100); - } - - /** - * Emits successful authentication event to client - * - * @param client - Socket.IO client - * @param user - Authenticated user payload - */ - emitAuthenticationSuccess(client: Socket, user: { id: string; sub: string }): void { - client.emit(WEBSOCKET_EVENTS.AUTHENTICATED, { - message: WebSocketMessage.AUTHENTICATED, - userId: user.id, - timestamp: new Date().toISOString(), - }); - } - - /** - * Creates error context object for logging and monitoring - * - * @param client - Socket.IO client - * @param event - Event name (optional) - * @param data - Event data (optional) - * @returns WebSocket error context - */ - createErrorContext(client: Socket, event?: WEBSOCKET_EVENTS, data?: any): WebSocketErrorContext { - return { - clientId: client.id, - userId: client.data?.user?.id, - sessionId: client.data?.sessionId, - event, - data, - timestamp: new Date(), - }; - } - - /** - * Validates if client is authenticated - * - * @param client - Socket.IO client to validate - * @returns True if client has valid user data - */ - isClientAuthenticated(client: Socket): boolean { - return !!client.data?.user?.id; - } - - /** - * Gets user ID from authenticated client - * - * @param client - Socket.IO client - * @returns User ID or undefined if not authenticated - */ - getUserId(client: Socket): string | undefined { - return client.data?.user?.id; - } - - /** - * Logs authentication attempts with structured data - */ - private logAuthenticationAttempt(client: Socket, success: boolean, error?: string, userId?: string): void { - const logData = { - clientId: client.id, - clientIP: client.handshake.address, - success, - userId, - error, - timestamp: new Date().toISOString(), - }; - - if (success) { - this.logger.info(`WebSocket authentication successful`, logData); - } else { - this.logger.warn(`WebSocket authentication failed`, logData); - } - } - - /** - * Maps JWT errors to user-friendly messages - */ - private getAuthErrorMessage(error: any): string { - if (error instanceof jwtExpiredErr || error?.name === "TokenExpiredError") { - return WebSocketMessage.TOKEN_EXPIRED; - } - - if (error?.name === "JsonWebTokenError") { - return WebSocketMessage.INVALID_TOKEN; - } - - if (error?.name === "NotBeforeError") { - return WebSocketMessage.INVALID_TOKEN; - } - - return WebSocketMessage.INVALID_TOKEN; - } -}