From 694d50b278728a7357ee32434d0ed4a6e4272ee4 Mon Sep 17 00:00:00 2001 From: sabarinathan Date: Tue, 27 May 2025 18:00:35 +0530 Subject: [PATCH] creation thread and comments in socket --- src/shared/V1Models/Thread/thread-Model.ts | 59 ++++++ src/shared/services/Thread/ThreadService.ts | 173 +++++++++++++++++ src/shared/services/builder/lineService.ts | 68 +++---- .../services/helpers/v1projecthelperFns.ts | 10 + .../controllers/builder/cameraController.ts | 51 +++++ .../controllers/thread/threadController.ts | 177 ++++++++++++++++++ src/socket-server/index.ts | 1 + src/socket-server/manager/manager.ts | 91 ++++++--- src/socket-server/socket/events.ts | 11 ++ src/socket-server/utils/emitEventResponse.ts | 1 + 10 files changed, 583 insertions(+), 59 deletions(-) create mode 100644 src/shared/V1Models/Thread/thread-Model.ts create mode 100644 src/shared/services/Thread/ThreadService.ts create mode 100644 src/socket-server/controllers/builder/cameraController.ts create mode 100644 src/socket-server/controllers/thread/threadController.ts diff --git a/src/shared/V1Models/Thread/thread-Model.ts b/src/shared/V1Models/Thread/thread-Model.ts new file mode 100644 index 0000000..6bac201 --- /dev/null +++ b/src/shared/V1Models/Thread/thread-Model.ts @@ -0,0 +1,59 @@ +import { Document, Schema } from "mongoose"; +import { User } from "../Auth/userAuthModel.ts"; +import { Project } from "../Project/project-model.ts"; +import { Version } from "../Version/versionModel.ts"; +import MainModel from "../../connect/mongoose.ts"; +interface IComment { + userId: User["_id"]; + // createdAt: string; + comment: string; + // lastUpdatedAt: string; + timestamp:Number +} +export interface IThread extends Document { + projectId: Project["_id"]; + versionId: Version["_id"]; + state: string; + commentId: string; + createdBy: User["_id"]; + createdAt: number; + lastUpdatedAt: string; + position: [number, number, number]; + rotation: [number, number, number]; + replies: IComment[]; + +} +const CommentSchema = new Schema( + { + userId: { type: Schema.Types.ObjectId, ref: 'User', required: true }, + // createdAt: { type: String, }, + comment: { type: String,}, + // lastUpdatedAt: { type: String, }, + timestamp:{ + type: Number, + default:Date.now()} + }, + // { _id: false } // Prevent automatic _id for each reply object +); +const threadSchema = new Schema({ + projectId: { type: Schema.Types.ObjectId, ref: 'Project', required: true }, + state: { type: String, enum: ['active', 'inactive'], required: true }, + commentId: { type: String, }, + createdBy: { type: Schema.Types.ObjectId, ref: 'User', required: true }, + createdAt: { type: Number,}, + lastUpdatedAt: { type: String, }, + position: { + type: [Number], + required: true, + }, + rotation: { + type: [Number], + required: true, + }, + replies: { type: [CommentSchema ], default: [] }, +}); + +const ThreadModel = (db: string) => { + return MainModel(db, "Threads", threadSchema, "Threads"); +}; +export default ThreadModel; \ No newline at end of file diff --git a/src/shared/services/Thread/ThreadService.ts b/src/shared/services/Thread/ThreadService.ts new file mode 100644 index 0000000..1f4fff7 --- /dev/null +++ b/src/shared/services/Thread/ThreadService.ts @@ -0,0 +1,173 @@ +import ThreadModel from "../../V1Models/Thread/thread-Model.ts"; +import { existingProjectByIdWithoutUser, existingUser } from "../helpers/v1projecthelperFns.ts"; + + +interface IThread { + projectId: string; + versionId: string; + state: string + commentId: string; + threadId: string; + userId: string; + createdAt: string; + lastUpdatedAt: string; + position: [number, number, number]; + rotation: [number, number, number]; + comments: string + timestamp: number; + + organization: string; +} +export const createThread = async (data: IThread) => { + try { + const { projectId, versionId, state, userId, position, rotation, comments, organization, threadId } = data + const userExisting = await existingUser(userId, organization); + if (!userExisting) { + return { + status: "user_not_found", + }; + } + const projectExisting = await existingProjectByIdWithoutUser( + projectId, + organization, + + ); + + if (!projectExisting) { + return { status: "Project not found" }; + } + const newThread = await ThreadModel(organization).create({ + projectId, + versionId, + state, + createdBy: userId, + position, + rotation, + comments, + createdAt: Date.now() + }); + return { + status: "Success", + data: newThread, + }; + } catch (error) { + return { + status: error, + }; + } +} +export const deleteThread = async (data: IThread) => { + try { + const { projectId, versionId, state, userId, organization, threadId } = data + const userExisting = await existingUser(userId, organization); + if (!userExisting) { + return { + status: "user_not_found", + }; + } + const projectExisting = await existingProjectByIdWithoutUser( + projectId, + organization, + + ); + + if (!projectExisting) { + return { status: "Project not found" }; + } + const findThreadId = await ThreadModel(organization).findOne({ _id: threadId, createdBy: userId }) + if (!findThreadId) { + return { status: "can't deleted" }; + } + const deleteThread = await ThreadModel(organization).findOneAndDelete({ _id: threadId, createdBy: userId }) + return { + status: "Success", + data: deleteThread, + }; + } catch (error) { + console.log("error: ", error); + return { + status: error, + }; + } +} +export const addComments = async (data: IThread) => { + try { + const { projectId, versionId, userId, comments, organization, threadId } = data + const userExisting = await existingUser(userId, organization); + if (!userExisting) { + return { + status: "user_not_found", + }; + } + const projectExisting = await existingProjectByIdWithoutUser( + projectId, + organization, + ); + + if (!projectExisting) { + return { status: "Project not found" }; + } + const findThreadId = await ThreadModel(organization).findById(threadId) + + const newComment = { userId, comment: comments, timestamp: Date.now() }; + findThreadId?.replies.push(newComment) + await findThreadId?.save() + return { + status: "Success", + data: newComment.comment, + }; + } catch (error) { + console.log("error: ", error); + return { + status: error, + }; + } +} +export const deleteComments = async (data: IThread) => { + try { + const { projectId, versionId, userId, commentId, organization, threadId } = data + const userExisting = await existingUser(userId, organization); + if (!userExisting) { + return { + status: "user_not_found", + }; + } + const projectExisting = await existingProjectByIdWithoutUser( + projectId, + organization, + ); + + if (!projectExisting) { + return { status: "Project not found" }; + } + const findThreadId = await ThreadModel(organization).findOne({ _id: threadId }) + if (!findThreadId) { + return { status: "thread not found" }; + } + + const deleted = await ThreadModel(organization).updateOne( + { _id: threadId }, + { + $pull: { + replies: { + _id: commentId, + userId: userId, + }, + }, + } + ); + + if (deleted.modifiedCount === 0) { + return { status: "unauthorized" }; + } + return { + status: "Success", + data: deleted + }; + } catch (error) { + console.log("error: ", error); + return { + status: error, + }; + } +} \ No newline at end of file diff --git a/src/shared/services/builder/lineService.ts b/src/shared/services/builder/lineService.ts index 57634b4..e630d95 100644 --- a/src/shared/services/builder/lineService.ts +++ b/src/shared/services/builder/lineService.ts @@ -176,38 +176,38 @@ export const DeleteLayer = async ( } }; -export const DeleteLinePoints = async ( - data: ILinePointsDelete -): Promise<{ status: string; data?: object }> => { - try { - const { organization, projectId, uuid, userId } = data; - const findValue = await lineModel(organization).deleteMany({ - "line.uuid": uuid, - }); +// export const DeleteLinePoints = async ( +// data: ILinePointsDelete +// ): Promise<{ status: string; data?: object }> => { +// try { +// const { organization, projectId, uuid, userId } = data; +// const findValue = await lineModel(organization).deleteMany({ +// "line.uuid": uuid, +// }); - if (!findValue) { - return { - success: false, - message: "line not found", - organization: organization, - }; - } else { - return { - success: true, - message: "point deleted", - data: uuid, - organization: organization, - }; - } - } catch (error: unknown) { - if (error instanceof Error) { - return { - status: error.message, - }; - } else { - return { - status: "An unexpected error occurred", - }; - } - } -}; +// // if (!findValue) { +// // return { +// // success: false, +// // message: "line not found", +// // organization: organization, +// // }; +// // } else { +// // return { +// // success: true, +// // message: "point deleted", +// // data: uuid, +// // organization: organization, +// // }; +// // } +// } catch (error: unknown) { +// if (error instanceof Error) { +// return { +// status: error.message, +// }; +// } else { +// return { +// status: "An unexpected error occurred", +// }; +// } +// } +// }; diff --git a/src/shared/services/helpers/v1projecthelperFns.ts b/src/shared/services/helpers/v1projecthelperFns.ts index 0d4e0ac..9a773a6 100644 --- a/src/shared/services/helpers/v1projecthelperFns.ts +++ b/src/shared/services/helpers/v1projecthelperFns.ts @@ -88,3 +88,13 @@ export const existingProjectById = async ( }); return projectData; }; +export const existingProjectByIdWithoutUser = async ( + projectId: string, + organization: string, +) => { + const projectData = await projectModel(organization).findOne({ + _id: projectId, + isArchive: false, + }); + return projectData; +}; \ No newline at end of file diff --git a/src/socket-server/controllers/builder/cameraController.ts b/src/socket-server/controllers/builder/cameraController.ts new file mode 100644 index 0000000..e3dcda1 --- /dev/null +++ b/src/socket-server/controllers/builder/cameraController.ts @@ -0,0 +1,51 @@ +import { Socket, Server } from "socket.io"; +import { EVENTS } from "../../socket/events.ts"; +import { emitEventResponse } from "../../utils/emitEventResponse.ts"; +import { SetCamera } from "../../../shared/services/builder/cameraService.ts"; +export const SetCameraHandleEvent = async ( event: string,socket: Socket,data: any,) => { + if (event !== EVENTS.setCamera || !data?.organization) return; + const requiredFields = ["userId", "position", "target", "rotation", "organization","projectId","versionId"]; + const missingFields = requiredFields.filter(field => !data?.[field]); + + if (missingFields.length > 0) { + const response = { + success: false, + message: `Missing required field(s): ${missingFields.join(", ")}`, + status: "MissingFields", + socketId: socket.id, + organization: data?.organization ?? "unknown", + }; + + emitEventResponse(socket, data?.organization, EVENTS.cameraCreateResponse, response); + return; + } + const result = await SetCamera(data); + const status = typeof result?.status === "string" ? result.status : "unknown"; + + const messages: Record = { + "Creation Success": { message: "Camera created Successfully" }, + "User not found": { message: "User not found" }, + "Project not found": { message: "Project not found" }, + "Update Success": { message: "Camera updated Successfully" }, + }; +const msg = messages[status] || { message: "Internal server error" }; + const isSuccess = status === "Creation Success" || status === "Update Success"; + + +const response = { + success: status === "Success", + message: msg.message, + status, + socketId: socket.id, + organization: data.organization, + ...(isSuccess && result?.data ? { data: result.data }: {}), + }; + + + const responseEvent = + status === "Creation Success" + ? EVENTS.cameraCreateResponse + : EVENTS.cameraUpdateResponse; + + emitEventResponse(socket, data.organization, responseEvent, response); +} \ No newline at end of file diff --git a/src/socket-server/controllers/thread/threadController.ts b/src/socket-server/controllers/thread/threadController.ts new file mode 100644 index 0000000..98322b6 --- /dev/null +++ b/src/socket-server/controllers/thread/threadController.ts @@ -0,0 +1,177 @@ +import { Socket, Server } from "socket.io"; +import { EVENTS } from "../../socket/events.ts"; +import { emitEventResponse } from "../../utils/emitEventResponse.ts"; +import { addComments, createThread, deleteComments, deleteThread } from "../../../shared/services/Thread/ThreadService.ts"; +export const createThreadHandleEvent = async (event: string, socket: Socket, data: any,) => { + if (event !== EVENTS.createThread || !data?.organization) return; + const requiredFields = ["userId", "position", "rotation", "organization", "projectId", "versionId"]; + const missingFields = requiredFields.filter(field => !data?.[field]); + + if (missingFields.length > 0) { + const response = { + success: false, + message: `Missing required field(s): ${missingFields.join(", ")}`, + status: "MissingFields", + socketId: socket.id, + organization: data?.organization ?? "unknown", + }; + + emitEventResponse(socket, data?.organization, EVENTS.ThreadCreateResponse, response); + return; + } + + const result = await createThread(data); + const status = typeof result?.status === "string" ? result.status : "unknown"; + + const messages: Record = { + Success: { message: "Thread created Successfully" }, + "User not found": { message: "User not found" }, + "Project not found": { message: "Project not found" }, + }; + const msg = messages[status] || { message: "Internal server error" }; + const threadDatas = + status === "Success" && result?.data + + const response = { + success: status === "Success", + message: msg.message, + status, + socketId: socket.id, + organization: data.organization, + ...(threadDatas ? { data: threadDatas } : {}), + }; + + + emitEventResponse(socket, data.organization, EVENTS.ThreadCreateResponse, response); +} +export const deleteThreadHandleEvent = async (event: string, socket: Socket, data: any,) => { + if (event !== EVENTS.deleteThread || !data?.organization) return; + const requiredFields = ["userId", "threadId", "organization", "projectId", "versionId"]; + const missingFields = requiredFields.filter(field => !data?.[field]); + + if (missingFields.length > 0) { + const response = { + success: false, + message: `Missing required field(s): ${missingFields.join(", ")}`, + status: "MissingFields", + socketId: socket.id, + organization: data?.organization ?? "unknown", + }; + + emitEventResponse(socket, data?.organization, EVENTS.deleteThreadResponse, response); + return; + } + + const result = await deleteThread(data); + const status = typeof result?.status === "string" ? result.status : "unknown"; + + const messages: Record = { + Success: { message: "Thread deleted Successfully" }, + "User not found": { message: "User not found" }, + "Project not found": { message: "Project not found" }, + "can't deleted": { message: "Thread could not be deleted" }, + }; + const msg = messages[status] || { message: "Internal server error" }; + const threadDatas = + status === "Success" && result?.data + + const response = { + success: status === "Success", + message: msg.message, + status, + socketId: socket.id, + organization: data.organization, + ...(threadDatas ? { data: threadDatas } : {}), + }; + + + emitEventResponse(socket, data.organization, EVENTS.deleteThreadResponse, response); +} +export const addCommentHandleEvent = async (event: string, socket: Socket, data: any,) => { + if (event !== EVENTS.addComment || !data?.organization) return; + const requiredFields = ["userId", "comments", "organization", "commentId", "projectId", "versionId"]; + const missingFields = requiredFields.filter(field => !data?.[field]); + + if (missingFields.length > 0) { + const response = { + success: false, + message: `Missing required field(s): ${missingFields.join(", ")}`, + status: "MissingFields", + socketId: socket.id, + organization: data?.organization ?? "unknown", + }; + + emitEventResponse(socket, data?.organization, EVENTS.addCommentResponse, response); + return; + } + + const result = await addComments(data); + const status = typeof result?.status === "string" ? result.status : "unknown"; + + const messages: Record = { + Success: { message: "Thread created Successfully" }, + "User not found": { message: "User not found" }, + "Project not found": { message: "Project not found" }, + }; + const msg = messages[status] || { message: "Internal server error" }; + const commentsData = + status === "Success" && result?.data + + + const response = { + success: status === "Success", + message: msg.message, + status, + socketId: socket.id, + organization: data.organization, + ...(commentsData ? { data: commentsData } : {}), + }; + + + emitEventResponse(socket, data.organization, EVENTS.addCommentResponse, response); +} +export const deleteCommentHandleEvent = async (event: string, socket: Socket, data: any,) => { + if (event !== EVENTS.deleteComment || !data?.organization) return; + const requiredFields = ["userId", "organization", "commentId", "projectId", "versionId"]; + const missingFields = requiredFields.filter(field => !data?.[field]); + + if (missingFields.length > 0) { + const response = { + success: false, + message: `Missing required field(s): ${missingFields.join(", ")}`, + status: "MissingFields", + socketId: socket.id, + organization: data?.organization ?? "unknown", + }; + + emitEventResponse(socket, data?.organization, EVENTS.deleteCommentResponse, response); + return; + } + + const result = await deleteComments(data); + const status = typeof result?.status === "string" ? result.status : "unknown"; + + const messages: Record = { + Success: { message: "Thread created Successfully" }, + "User not found": { message: "User not found" }, + "Project not found": { message: "Project not found" }, + "unauthorized": { message: "You can only delete your own comment." }, + "thread not found": { message: "thread not found" }, + }; + const msg = messages[status] || { message: "Internal server error" }; + const commentsData = + status === "Success" && result?.data + + + const response = { + success: status === "Success", + message: msg.message, + status, + socketId: socket.id, + organization: data.organization, + ...(commentsData ? { data: commentsData } : {}), + }; + + + emitEventResponse(socket, data.organization, EVENTS.deleteCommentResponse, response); +} \ No newline at end of file diff --git a/src/socket-server/index.ts b/src/socket-server/index.ts index 39d702b..88dbdbd 100644 --- a/src/socket-server/index.ts +++ b/src/socket-server/index.ts @@ -16,6 +16,7 @@ app.get('/', (req: Request, res: Response) => { res.send('Hello, I am Major-Dwinzo RealTime!'); }); initSocketServer(server); +// SocketServer(server) server.listen(PORT, () => { console.log(`socket-Server is running on http://localhost:${PORT}`); }); diff --git a/src/socket-server/manager/manager.ts b/src/socket-server/manager/manager.ts index fe0adb2..83a26c6 100644 --- a/src/socket-server/manager/manager.ts +++ b/src/socket-server/manager/manager.ts @@ -1,6 +1,10 @@ import { Server, Socket } from 'socket.io'; +import jwt from 'jsonwebtoken'; +import dotenv from 'dotenv'; +import AuthModel from '../../shared/V1Models/Auth/userAuthModel.ts'; +dotenv.config(); import { projectDeleteHandleEvent, projectHandleEvent, projecUpdateHandleEvent } from '../controllers/project/projectController.ts'; - +import { addCommentHandleEvent, createThreadHandleEvent, deleteCommentHandleEvent, deleteThreadHandleEvent } from '../controllers/thread/threadController.ts'; export const SocketServer = (httpServer: any) => { const io = new Server(httpServer, { cors: { @@ -9,29 +13,65 @@ export const SocketServer = (httpServer: any) => { }, }); - - const namespaces = { - project: io.of('/project'), - + thread: io.of('/thread'), }; // const onlineUsers = new Map>(); const onlineUsers: { [organization: string]: Set } = {}; - const handleNamespace = ( namespace: any, ...eventHandlers: Function[]) => { - + const handleNamespace = (namespace: any, ...eventHandlers: Function[]) => { + namespace.use(async (socket: Socket, next: any) => { + const token = socket.handshake.auth.token; + // const token = socket.handshake.query.token as string; + const jwt_secret = process.env.JWT_SECRET as string; + + if (!token) { + console.log(" No token provided"); + return next(new Error("Authentication error: No token")); + } + if (!jwt_secret) { + console.error(" JWT secret is not defined in environment variables"); + return next(new Error("Server configuration error: Missing secret")); + } + try { + // 1. Verify token + const decoded = jwt.verify(token, jwt_secret) as { + userId: string; + Email: string; + organization: string + }; + // 2. Find user in DB + const user = await AuthModel(decoded.organization).findOne({ _id: decoded.userId, Email: decoded.Email, }); + + if (!user) { + console.log(" User not found in DB"); + return next(new Error("Authentication error: User not found")); + } + + // 3. Attach to socket for later use + (socket as any).user = { + // ...user.toObject(), // convert Mongoose doc to plain object + organization: decoded.organization, // manually add org + Email: decoded.Email, // manually add org + }; + + next(); + } catch (error: any) { + console.error("❌ Authentication failed:", error.message); + return next(new Error("Authentication error")); + + } + }); namespace.on("connection", (socket: Socket) => { - console.log(`✅ Client connected to ${namespace.name}: ${socket.id}`); // Extract organization from query parameters - - const organization = socket.handshake.query.organization as string; - const email = socket.handshake.query.email as string; - // const {organization,email} = socket.handshake.auth + const user = (socket as any).user; + const organization = user.organization; + const email = user.email; console.log(`🔍 Received organization: ${organization}`); - + if (organization) { socket.join(organization); // console.log(`🔹 Socket ${socket.id} joined room: ${organization}`); @@ -39,40 +79,41 @@ export const SocketServer = (httpServer: any) => { // Handle all events if (organization && email) { if (!onlineUsers[organization]) { - onlineUsers[organization] = new Set(); + onlineUsers[organization] = new Set(); } onlineUsers[organization].add(socket.id); - - } - // userStatus(EVENTS.connection, socket, socket.handshake.auth, socket); + } - socket.onAny((event: string, data: any ,callback:any) => { - eventHandlers.forEach(handler => handler(event, socket, data, namespace,io,callback)); + // userStatus(EVENTS.connection, socket, socket.handshake.auth, socket); + + socket.onAny((event: string, data: any, callback: any) => { + eventHandlers.forEach(handler => handler(event, socket, data, namespace, io, callback)); }); - + // Handle disconnection socket.on("disconnect", () => { onlineUsers[organization]?.delete(socket.id); if (onlineUsers[organization]?.size === 0) delete onlineUsers[organization]; // userStatus(EVENTS.disconnect, socket, socket.handshake.auth, socket); - + }); - + // Handle reconnection (Auto rejoin) socket.on("reconnect", (attempt: number) => { - + if (organization) { socket.join(organization); - + } }); }); }; - + handleNamespace(namespaces.project,projectHandleEvent,projectDeleteHandleEvent,projecUpdateHandleEvent) +handleNamespace(namespaces.thread,createThreadHandleEvent,deleteThreadHandleEvent,addCommentHandleEvent,deleteCommentHandleEvent) return io; }; \ No newline at end of file diff --git a/src/socket-server/socket/events.ts b/src/socket-server/socket/events.ts index a152c8d..8505567 100644 --- a/src/socket-server/socket/events.ts +++ b/src/socket-server/socket/events.ts @@ -98,6 +98,7 @@ export const EVENTS = { update3dPositionResponse:"viz-widget3D:response:modifyPositionRotation", delete3DWidget:"v2:viz-3D-widget:delete", widget3DDeleteResponse:"viz-widget3D:response:delete", +/////////////////........................................................ //PROJECT addProject: "v1:project:add", @@ -106,4 +107,14 @@ export const EVENTS = { deleteProjectResponse: "v1-project:response:delete", ProjectUpdate: "v1:project:update", projectUpdateResponse: "v1-project:response:update", + +//THREAD +createThread:"v1:thread:create", +ThreadCreateResponse:"v1-thread:response:create", +deleteThread:"v1:thread:delete", +deleteThreadResponse:"v1-thread:response:delete", +addComment:"v1-Comment:response:add", +addCommentResponse:"v1-Comment:response:add", +deleteComment:"v1-Comment:response:delete", +deleteCommentResponse:"v1-Comment:response:delete", } \ No newline at end of file diff --git a/src/socket-server/utils/emitEventResponse.ts b/src/socket-server/utils/emitEventResponse.ts index 797c61d..48c0a0f 100644 --- a/src/socket-server/utils/emitEventResponse.ts +++ b/src/socket-server/utils/emitEventResponse.ts @@ -24,6 +24,7 @@ export const emitEventResponse = ( console.log(`Organization missing in response for event: ${event}`); return; } + console.log('result: ', result); socket.to(organization).emit(event, { // success: result.success,