This commit is contained in:
parent
27a06adbe6
commit
365aea2997
@ -60,7 +60,7 @@ jobs:
|
|||||||
image: harbor.galpodlipnik.com/chat-app/backend:latest
|
image: harbor.galpodlipnik.com/chat-app/backend:latest
|
||||||
restart: always
|
restart: always
|
||||||
env_file:
|
env_file:
|
||||||
- ./backend/.env
|
- ./.env
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "3000:3000"
|
||||||
networks:
|
networks:
|
||||||
|
|||||||
@ -6,6 +6,8 @@ COPY package.json package-lock.json ./
|
|||||||
RUN npm ci
|
RUN npm ci
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
|
RUN npm run db:generate
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|||||||
22
backend/dist/config/database.js
vendored
Normal file
22
backend/dist/config/database.js
vendored
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.disconnectDatabase = exports.connectDatabase = exports.prisma = void 0;
|
||||||
|
const client_1 = require("@prisma/client");
|
||||||
|
exports.prisma = new client_1.PrismaClient({
|
||||||
|
log: ["query", "info", "warn", "error"],
|
||||||
|
});
|
||||||
|
const connectDatabase = async () => {
|
||||||
|
try {
|
||||||
|
await exports.prisma.$connect();
|
||||||
|
console.log("✅ Database connected successfully");
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error("❌ Database connection failed:", error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
exports.connectDatabase = connectDatabase;
|
||||||
|
const disconnectDatabase = async () => {
|
||||||
|
await exports.prisma.$disconnect();
|
||||||
|
};
|
||||||
|
exports.disconnectDatabase = disconnectDatabase;
|
||||||
12
backend/dist/config/env.js
vendored
Normal file
12
backend/dist/config/env.js
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.env = void 0;
|
||||||
|
const zod_1 = require("zod");
|
||||||
|
const envSchema = zod_1.z.object({
|
||||||
|
CORS_ORIGIN: zod_1.z.string().default("*"),
|
||||||
|
DATABASE_URL: zod_1.z.string(),
|
||||||
|
JWT_SECRET: zod_1.z.string(),
|
||||||
|
NODE_ENV: zod_1.z.enum(["development", "production", "test"]).default("development"),
|
||||||
|
PORT: zod_1.z.string().transform(Number).default("3000"),
|
||||||
|
});
|
||||||
|
exports.env = envSchema.parse(process.env);
|
||||||
17
backend/dist/controllers/authController.js
vendored
Normal file
17
backend/dist/controllers/authController.js
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.AuthController = void 0;
|
||||||
|
const authService_js_1 = require("../services/authService.js");
|
||||||
|
class AuthController {
|
||||||
|
static async register(req, res) {
|
||||||
|
const data = req.body;
|
||||||
|
const result = await authService_js_1.AuthService.register(data);
|
||||||
|
res.status(result.success ? 201 : 400).json(result);
|
||||||
|
}
|
||||||
|
static async login(req, res) {
|
||||||
|
const data = req.body;
|
||||||
|
const result = await authService_js_1.AuthService.login(data);
|
||||||
|
res.status(result.success ? 200 : 400).json(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exports.AuthController = AuthController;
|
||||||
76
backend/dist/controllers/chatController.js
vendored
Normal file
76
backend/dist/controllers/chatController.js
vendored
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.ChatController = void 0;
|
||||||
|
const chatService_js_1 = require("../services/chatService.js");
|
||||||
|
class ChatController {
|
||||||
|
static async getChatRooms(req, res) {
|
||||||
|
if (!req.user) {
|
||||||
|
res.status(401).json({ success: false, error: "Unauthorized" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const result = await chatService_js_1.ChatService.getChatRooms(req.user.userId);
|
||||||
|
res.status(result.success ? 200 : 500).json(result);
|
||||||
|
}
|
||||||
|
static async createChatRoom(req, res) {
|
||||||
|
if (!req.user) {
|
||||||
|
res.status(401).json({ success: false, error: "Unauthorized" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = req.body;
|
||||||
|
const result = await chatService_js_1.ChatService.createChatRoom(req.user.userId, data);
|
||||||
|
res.status(result.success ? 201 : 500).json(result);
|
||||||
|
}
|
||||||
|
static async getMessages(req, res) {
|
||||||
|
if (!req.user) {
|
||||||
|
res.status(401).json({ success: false, error: "Unauthorized" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const query = {
|
||||||
|
roomId: req.params.roomId,
|
||||||
|
page: req.query.page,
|
||||||
|
limit: req.query.limit,
|
||||||
|
};
|
||||||
|
const result = await chatService_js_1.ChatService.getMessages(query);
|
||||||
|
res.status(result.success ? 200 : 500).json(result);
|
||||||
|
}
|
||||||
|
static async updateChatRoom(req, res) {
|
||||||
|
if (!req.user) {
|
||||||
|
res.status(401).json({ success: false, error: "Unauthorized" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const roomId = req.params.roomId;
|
||||||
|
const data = req.body;
|
||||||
|
const result = await chatService_js_1.ChatService.updateChatRoom(req.user.userId, roomId, data);
|
||||||
|
res.status(result.success ? 200 : result.status || 400).json(result);
|
||||||
|
}
|
||||||
|
static async deleteChatRoom(req, res) {
|
||||||
|
if (!req.user) {
|
||||||
|
res.status(401).json({ success: false, error: "Unauthorized" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const roomId = req.params.roomId;
|
||||||
|
const result = await chatService_js_1.ChatService.deleteChatRoom(req.user.userId, roomId);
|
||||||
|
res.status(result.success ? 200 : result.status || 500).json(result);
|
||||||
|
}
|
||||||
|
static async addChatRoomMember(req, res) {
|
||||||
|
if (!req.user) {
|
||||||
|
res.status(401).json({ success: false, error: "Unauthorized" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const roomId = req.params.roomId;
|
||||||
|
const data = req.body;
|
||||||
|
const result = await chatService_js_1.ChatService.addChatRoomMember(req.user.userId, roomId, data);
|
||||||
|
res.status(result.success ? 200 : result.status || 500).json(result);
|
||||||
|
}
|
||||||
|
static async removeChatRoomMember(req, res) {
|
||||||
|
if (!req.user) {
|
||||||
|
res.status(401).json({ success: false, error: "Unauthorized" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const roomId = req.params.roomId;
|
||||||
|
const data = req.body;
|
||||||
|
const result = await chatService_js_1.ChatService.removeChatRoomMember(req.user.userId, roomId, data);
|
||||||
|
res.status(result.success ? 200 : result.status || 500).json(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exports.ChatController = ChatController;
|
||||||
16
backend/dist/controllers/userController.js
vendored
Normal file
16
backend/dist/controllers/userController.js
vendored
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.UserController = void 0;
|
||||||
|
const userService_1 = require("../services/userService");
|
||||||
|
class UserController {
|
||||||
|
static async updateProfile(req, res) {
|
||||||
|
if (!req.user) {
|
||||||
|
res.status(401).json({ success: false, error: "Unauthorized" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = req.body;
|
||||||
|
const result = await userService_1.UserService.updateProfile(req.user.userId, data);
|
||||||
|
res.status(result.success ? 200 : 400).json(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exports.UserController = UserController;
|
||||||
66
backend/dist/index.js
vendored
Normal file
66
backend/dist/index.js
vendored
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
"use strict";
|
||||||
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||||
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
const cors_1 = __importDefault(require("cors"));
|
||||||
|
const express_1 = __importDefault(require("express"));
|
||||||
|
const http_1 = __importDefault(require("http"));
|
||||||
|
const socket_io_1 = require("socket.io");
|
||||||
|
const database_js_1 = require("./config/database.js");
|
||||||
|
const env_js_1 = require("./config/env.js");
|
||||||
|
const auth_js_1 = require("./middleware/auth.js");
|
||||||
|
const index_js_1 = require("./routes/index.js");
|
||||||
|
const socketHandlers_js_1 = require("./socket/socketHandlers.js");
|
||||||
|
const app = (0, express_1.default)();
|
||||||
|
const server = http_1.default.createServer(app);
|
||||||
|
const io = new socket_io_1.Server(server, {
|
||||||
|
cors: {
|
||||||
|
methods: ["GET", "POST", "PUT", "DELETE"],
|
||||||
|
origin: "http://localhost:5173",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
app.use((0, cors_1.default)({
|
||||||
|
methods: ["GET", "POST", "PUT", "DELETE"],
|
||||||
|
origin: "http://localhost:5173",
|
||||||
|
}));
|
||||||
|
app.use(express_1.default.json({ limit: "5mb" }));
|
||||||
|
app.use("/api", index_js_1.apiRoutes);
|
||||||
|
app.get("/health", (_, res) => {
|
||||||
|
res.json({ status: "OK", timestamp: new Date().toISOString() });
|
||||||
|
});
|
||||||
|
io.use(auth_js_1.authenticateSocket);
|
||||||
|
io.on("connection", async (socket) => {
|
||||||
|
await (0, socketHandlers_js_1.handleConnection)(io, socket);
|
||||||
|
});
|
||||||
|
app.use((err, _req, res, _next) => {
|
||||||
|
console.error("Unhandled error:", err);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: env_js_1.env.NODE_ENV === "production" ? "Internal server error" : err.message,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const startServer = async () => {
|
||||||
|
try {
|
||||||
|
await (0, database_js_1.connectDatabase)();
|
||||||
|
server.listen(env_js_1.env.PORT, () => {
|
||||||
|
console.log(`🚀 Server running on port ${env_js_1.env.PORT}`);
|
||||||
|
console.log(`📊 Environment: ${env_js_1.env.NODE_ENV}`);
|
||||||
|
console.log(`🔗 CORS Origin: ${env_js_1.env.CORS_ORIGIN}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error("Failed to start server:", error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
process.on("SIGTERM", () => {
|
||||||
|
console.log("SIGTERM received, shutting down gracefully...");
|
||||||
|
server.close(() => {
|
||||||
|
console.log("HTTP server closed");
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
(async () => {
|
||||||
|
await startServer();
|
||||||
|
})();
|
||||||
51
backend/dist/middleware/auth.js
vendored
Normal file
51
backend/dist/middleware/auth.js
vendored
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
"use strict";
|
||||||
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||||
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.authenticateSocket = exports.authenticateToken = void 0;
|
||||||
|
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
||||||
|
const database_js_1 = require("../config/database.js");
|
||||||
|
const env_js_1 = require("../config/env.js");
|
||||||
|
const authenticateToken = (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const authHeader = req.headers.authorization;
|
||||||
|
const token = authHeader?.split(" ")[1];
|
||||||
|
if (!token) {
|
||||||
|
res.status(401).json({ success: false, error: "Access token required" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const decoded = jsonwebtoken_1.default.verify(token, env_js_1.env.JWT_SECRET);
|
||||||
|
req.user = decoded;
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error("Authentication error:", error);
|
||||||
|
res.status(403).json({ success: false, error: "Invalid or expired token" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
exports.authenticateToken = authenticateToken;
|
||||||
|
const authenticateSocket = async (socket, next) => {
|
||||||
|
try {
|
||||||
|
const token = socket.handshake.auth.token;
|
||||||
|
if (!token) {
|
||||||
|
return next(new Error("Authentication token required"));
|
||||||
|
}
|
||||||
|
const decoded = jsonwebtoken_1.default.verify(token, env_js_1.env.JWT_SECRET);
|
||||||
|
const user = await database_js_1.prisma.user.findUnique({
|
||||||
|
where: {
|
||||||
|
id: decoded.userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!user)
|
||||||
|
return next(new Error("User not found"));
|
||||||
|
const authenticateSocket = socket;
|
||||||
|
authenticateSocket.userId = user.id;
|
||||||
|
authenticateSocket.user = user;
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error("Socket authentication error:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
exports.authenticateSocket = authenticateSocket;
|
||||||
42
backend/dist/middleware/rateLimiter.js
vendored
Normal file
42
backend/dist/middleware/rateLimiter.js
vendored
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.checkMessageRateLimit = exports.messageRateLimiter = void 0;
|
||||||
|
class RateLimiter {
|
||||||
|
limits = new Map();
|
||||||
|
maxMessages;
|
||||||
|
windowMs;
|
||||||
|
constructor(maxMessages = 10, windowMs = 60000) {
|
||||||
|
this.maxMessages = maxMessages;
|
||||||
|
this.windowMs = windowMs;
|
||||||
|
}
|
||||||
|
checkLimit(userId) {
|
||||||
|
const now = Date.now();
|
||||||
|
const userLimit = this.limits.get(userId) ?? { count: 0, resetTime: now + this.windowMs };
|
||||||
|
if (now > userLimit.resetTime) {
|
||||||
|
userLimit.count = 0;
|
||||||
|
userLimit.resetTime = now + this.windowMs;
|
||||||
|
}
|
||||||
|
userLimit.count++;
|
||||||
|
this.limits.set(userId, userLimit);
|
||||||
|
return userLimit.count <= this.maxMessages;
|
||||||
|
}
|
||||||
|
getRemainingTime(userId) {
|
||||||
|
const userLimit = this.limits.get(userId);
|
||||||
|
if (!userLimit)
|
||||||
|
return 0;
|
||||||
|
return Math.max(0, userLimit.resetTime - Date.now());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exports.messageRateLimiter = new RateLimiter(10, 60000); // 10 messages per minute
|
||||||
|
const checkMessageRateLimit = (socket) => {
|
||||||
|
const canSend = exports.messageRateLimiter.checkLimit(socket.userId);
|
||||||
|
if (!canSend) {
|
||||||
|
const remainingTime = exports.messageRateLimiter.getRemainingTime(socket.userId);
|
||||||
|
socket.emit("rate_limit_exceeded", {
|
||||||
|
message: `Too many messages. Please wait ${Math.ceil(remainingTime / 1000)} seconds before sending another message.`,
|
||||||
|
remainingTime,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return canSend;
|
||||||
|
};
|
||||||
|
exports.checkMessageRateLimit = checkMessageRateLimit;
|
||||||
54
backend/dist/middleware/validation.js
vendored
Normal file
54
backend/dist/middleware/validation.js
vendored
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.removeChatRoomMemberSchema = exports.addChatRoomMemberSchema = exports.updateChatRoomSchema = exports.updateUserSchema = exports.createChatRoomSchema = exports.registerSchema = exports.loginSchema = exports.validate = void 0;
|
||||||
|
const zod_1 = require("zod");
|
||||||
|
const validate = (schema) => {
|
||||||
|
return (req, res, next) => {
|
||||||
|
try {
|
||||||
|
schema.parse(req.body);
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
if (error instanceof zod_1.z.ZodError) {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: "Validation failed",
|
||||||
|
details: error.errors,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
exports.validate = validate;
|
||||||
|
exports.loginSchema = zod_1.z.object({
|
||||||
|
email: zod_1.z.string().email("Invalid email format"),
|
||||||
|
password: zod_1.z.string().min(6, "Password must be at least 6 characters long"),
|
||||||
|
});
|
||||||
|
exports.registerSchema = zod_1.z.object({
|
||||||
|
username: zod_1.z.string().min(3, "Username must be at least 3 characters long"),
|
||||||
|
email: zod_1.z.string().email("Invalid email format"),
|
||||||
|
password: zod_1.z.string().min(6, "Password must be at least 6 characters long"),
|
||||||
|
});
|
||||||
|
exports.createChatRoomSchema = zod_1.z.object({
|
||||||
|
name: zod_1.z.string().min(1, "Room name is required"),
|
||||||
|
description: zod_1.z.string().optional(),
|
||||||
|
memberUsernames: zod_1.z.array(zod_1.z.string()).default([]),
|
||||||
|
});
|
||||||
|
exports.updateUserSchema = zod_1.z.object({
|
||||||
|
username: zod_1.z.string().min(3, "Username must be at least 3 characters long").optional(),
|
||||||
|
email: zod_1.z.string().email("Invalid email format").optional(),
|
||||||
|
password: zod_1.z.string().min(6, "Password must be at least 6 characters long").optional(),
|
||||||
|
avatar: zod_1.z.string().nullable().optional(),
|
||||||
|
});
|
||||||
|
exports.updateChatRoomSchema = zod_1.z.object({
|
||||||
|
name: zod_1.z.string().min(1, "Room name is required"),
|
||||||
|
description: zod_1.z.string().optional(),
|
||||||
|
});
|
||||||
|
exports.addChatRoomMemberSchema = zod_1.z.object({
|
||||||
|
username: zod_1.z.string().min(1, "Username is required"),
|
||||||
|
});
|
||||||
|
exports.removeChatRoomMemberSchema = zod_1.z.object({
|
||||||
|
userId: zod_1.z.string().min(1, "User ID is required"),
|
||||||
|
});
|
||||||
10
backend/dist/routes/authRoutes.js
vendored
Normal file
10
backend/dist/routes/authRoutes.js
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.authRoutes = void 0;
|
||||||
|
const authController_js_1 = require("../controllers/authController.js");
|
||||||
|
const validation_js_1 = require("../middleware/validation.js");
|
||||||
|
const express_1 = require("express");
|
||||||
|
const router = (0, express_1.Router)();
|
||||||
|
exports.authRoutes = router;
|
||||||
|
router.post("/register", (0, validation_js_1.validate)(validation_js_1.registerSchema), authController_js_1.AuthController.register);
|
||||||
|
router.post("/login", (0, validation_js_1.validate)(validation_js_1.loginSchema), authController_js_1.AuthController.login);
|
||||||
17
backend/dist/routes/chatRoutes.js
vendored
Normal file
17
backend/dist/routes/chatRoutes.js
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.chatRoutes = void 0;
|
||||||
|
const chatController_js_1 = require("../controllers/chatController.js");
|
||||||
|
const auth_js_1 = require("../middleware/auth.js");
|
||||||
|
const validation_js_1 = require("../middleware/validation.js");
|
||||||
|
const express_1 = require("express");
|
||||||
|
const router = (0, express_1.Router)();
|
||||||
|
exports.chatRoutes = router;
|
||||||
|
router.use(auth_js_1.authenticateToken);
|
||||||
|
router.get("/chat-rooms", chatController_js_1.ChatController.getChatRooms);
|
||||||
|
router.post("/chat-rooms", (0, validation_js_1.validate)(validation_js_1.createChatRoomSchema), chatController_js_1.ChatController.createChatRoom);
|
||||||
|
router.put("/chat-rooms/:roomId", (0, validation_js_1.validate)(validation_js_1.updateChatRoomSchema), chatController_js_1.ChatController.updateChatRoom);
|
||||||
|
router.delete("/chat-rooms/:roomId", chatController_js_1.ChatController.deleteChatRoom);
|
||||||
|
router.post("/chat-rooms/:roomId/members", (0, validation_js_1.validate)(validation_js_1.addChatRoomMemberSchema), chatController_js_1.ChatController.addChatRoomMember);
|
||||||
|
router.delete("/chat-rooms/:roomId/members", (0, validation_js_1.validate)(validation_js_1.removeChatRoomMemberSchema), chatController_js_1.ChatController.removeChatRoomMember);
|
||||||
|
router.get("/messages/:roomId", chatController_js_1.ChatController.getMessages);
|
||||||
12
backend/dist/routes/index.js
vendored
Normal file
12
backend/dist/routes/index.js
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.apiRoutes = void 0;
|
||||||
|
const express_1 = require("express");
|
||||||
|
const authRoutes_js_1 = require("./authRoutes.js");
|
||||||
|
const chatRoutes_js_1 = require("./chatRoutes.js");
|
||||||
|
const userRoutes_js_1 = require("./userRoutes.js");
|
||||||
|
const router = (0, express_1.Router)();
|
||||||
|
exports.apiRoutes = router;
|
||||||
|
router.use("/auth", authRoutes_js_1.authRoutes);
|
||||||
|
router.use("/users", userRoutes_js_1.userRoutes);
|
||||||
|
router.use("/", chatRoutes_js_1.chatRoutes);
|
||||||
11
backend/dist/routes/userRoutes.js
vendored
Normal file
11
backend/dist/routes/userRoutes.js
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.userRoutes = void 0;
|
||||||
|
const userController_js_1 = require("../controllers/userController.js");
|
||||||
|
const auth_js_1 = require("../middleware/auth.js");
|
||||||
|
const validation_js_1 = require("../middleware/validation.js");
|
||||||
|
const express_1 = require("express");
|
||||||
|
const router = (0, express_1.Router)();
|
||||||
|
exports.userRoutes = router;
|
||||||
|
router.use(auth_js_1.authenticateToken);
|
||||||
|
router.put("/profile", (0, validation_js_1.validate)(validation_js_1.updateUserSchema), userController_js_1.UserController.updateProfile);
|
||||||
100
backend/dist/services/authService.js
vendored
Normal file
100
backend/dist/services/authService.js
vendored
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
"use strict";
|
||||||
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||||
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.AuthService = void 0;
|
||||||
|
const database_js_1 = require("../config/database.js");
|
||||||
|
const env_js_1 = require("../config/env.js");
|
||||||
|
const bcryptjs_1 = __importDefault(require("bcryptjs"));
|
||||||
|
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
||||||
|
class AuthService {
|
||||||
|
static async register(data) {
|
||||||
|
try {
|
||||||
|
const existingUser = await database_js_1.prisma.user.findFirst({
|
||||||
|
where: {
|
||||||
|
OR: [{ email: data.email }, { username: data.username }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (existingUser) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: existingUser.email === data.email ? "Email already exists" : "Username already exists",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const hashedPassword = await bcryptjs_1.default.hash(data.password, 12);
|
||||||
|
const user = await database_js_1.prisma.user.create({
|
||||||
|
data: {
|
||||||
|
username: data.username,
|
||||||
|
email: data.email,
|
||||||
|
password: hashedPassword,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const token = this.generateToken({
|
||||||
|
userId: user.id,
|
||||||
|
username: user.username,
|
||||||
|
email: user.email,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
token,
|
||||||
|
user: {
|
||||||
|
id: user.id,
|
||||||
|
username: user.username,
|
||||||
|
email: user.email,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error("Registration error:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Registration failed",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static async login(data) {
|
||||||
|
try {
|
||||||
|
const user = await database_js_1.prisma.user.findUnique({
|
||||||
|
where: {
|
||||||
|
email: data.email,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!user || !(await bcryptjs_1.default.compare(data.password, user.password))) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Invalid email or password",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const token = this.generateToken({
|
||||||
|
userId: user.id,
|
||||||
|
username: user.username,
|
||||||
|
email: user.email,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
token,
|
||||||
|
user: {
|
||||||
|
id: user.id,
|
||||||
|
username: user.username,
|
||||||
|
email: user.email,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error("Login error:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Login failed",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static generateToken(payload) {
|
||||||
|
return jsonwebtoken_1.default.sign(payload, env_js_1.env.JWT_SECRET, { expiresIn: "7d" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exports.AuthService = AuthService;
|
||||||
490
backend/dist/services/chatService.js
vendored
Normal file
490
backend/dist/services/chatService.js
vendored
Normal file
@ -0,0 +1,490 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.ChatService = void 0;
|
||||||
|
const database_js_1 = require("../config/database.js");
|
||||||
|
class ChatService {
|
||||||
|
static async getChatRooms(userId) {
|
||||||
|
try {
|
||||||
|
const chatRooms = await database_js_1.prisma.chatRoom.findMany({
|
||||||
|
where: {
|
||||||
|
members: {
|
||||||
|
some: {
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
members: {
|
||||||
|
include: {
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
username: true,
|
||||||
|
isOnline: true,
|
||||||
|
lastSeen: true,
|
||||||
|
avatar: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
_count: {
|
||||||
|
select: {
|
||||||
|
messages: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
updatedAt: "desc",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: chatRooms,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error("Get chat rooms error:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Failed to retrieve chat rooms",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static async createChatRoom(userId, data) {
|
||||||
|
try {
|
||||||
|
const chatRoom = await database_js_1.prisma.chatRoom.create({
|
||||||
|
data: {
|
||||||
|
name: data.name,
|
||||||
|
description: data.description,
|
||||||
|
createdBy: userId,
|
||||||
|
members: {
|
||||||
|
create: [
|
||||||
|
{
|
||||||
|
userId,
|
||||||
|
role: "admin",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (data.memberUsernames.length > 0) {
|
||||||
|
const users = await database_js_1.prisma.user.findMany({
|
||||||
|
where: {
|
||||||
|
username: {
|
||||||
|
in: data.memberUsernames,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const memberData = users.map((user) => ({
|
||||||
|
userId: user.id,
|
||||||
|
roomId: chatRoom.id,
|
||||||
|
}));
|
||||||
|
await database_js_1.prisma.chatRoomMember.createMany({
|
||||||
|
data: memberData,
|
||||||
|
skipDuplicates: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const fullChatRoom = await database_js_1.prisma.chatRoom.findUnique({
|
||||||
|
where: { id: chatRoom.id },
|
||||||
|
include: {
|
||||||
|
members: {
|
||||||
|
include: {
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
username: true,
|
||||||
|
isOnline: true,
|
||||||
|
avatar: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
_count: {
|
||||||
|
select: {
|
||||||
|
messages: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: fullChatRoom,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error("Create chat room error:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Failed to create chat room",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static async getMessages(query) {
|
||||||
|
try {
|
||||||
|
const page = Number.parseInt(query.page ?? "1");
|
||||||
|
const limit = Number.parseInt(query.limit ?? "50");
|
||||||
|
const skip = (page - 1) * limit;
|
||||||
|
const messages = await database_js_1.prisma.message.findMany({
|
||||||
|
where: {
|
||||||
|
roomId: query.roomId,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
username: true,
|
||||||
|
avatar: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
reactions: {
|
||||||
|
include: {
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
username: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
seenBy: {
|
||||||
|
select: {
|
||||||
|
userId: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
createdAt: "desc",
|
||||||
|
},
|
||||||
|
take: limit,
|
||||||
|
skip,
|
||||||
|
});
|
||||||
|
const formattedMessages = messages.map((message) => ({
|
||||||
|
...message,
|
||||||
|
seenBy: message.seenBy.map((seen) => seen.userId),
|
||||||
|
}));
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: formattedMessages.reverse(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error("Get messages error:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Failed to retrieve messages",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static async checkRoomMembership(userId, roomId) {
|
||||||
|
try {
|
||||||
|
const membership = await database_js_1.prisma.chatRoomMember.findUnique({
|
||||||
|
where: {
|
||||||
|
userId_roomId: {
|
||||||
|
userId,
|
||||||
|
roomId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return !!membership;
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error("Check room membership error:", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static async updateChatRoom(userId, roomId, data) {
|
||||||
|
try {
|
||||||
|
const membership = await database_js_1.prisma.chatRoomMember.findUnique({
|
||||||
|
where: {
|
||||||
|
userId_roomId: {
|
||||||
|
userId,
|
||||||
|
roomId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!membership) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "You are not a member of this chat room",
|
||||||
|
status: 403,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (membership.role !== "admin") {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Only admins can update chat room settings",
|
||||||
|
status: 403,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const updatedRoom = await database_js_1.prisma.chatRoom.update({
|
||||||
|
where: { id: roomId },
|
||||||
|
data: {
|
||||||
|
name: data.name,
|
||||||
|
description: data.description,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
members: {
|
||||||
|
include: {
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
username: true,
|
||||||
|
isOnline: true,
|
||||||
|
lastSeen: true,
|
||||||
|
avatar: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
_count: {
|
||||||
|
select: {
|
||||||
|
messages: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: updatedRoom,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error("Update chat room error:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Failed to update chat room",
|
||||||
|
status: 500,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static async deleteChatRoom(userId, roomId) {
|
||||||
|
try {
|
||||||
|
const membership = await database_js_1.prisma.chatRoomMember.findUnique({
|
||||||
|
where: {
|
||||||
|
userId_roomId: {
|
||||||
|
userId,
|
||||||
|
roomId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!membership) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "You are not a member of this chat room",
|
||||||
|
status: 403,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (membership.role !== "admin") {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Only admins can delete chat rooms",
|
||||||
|
status: 403,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
await database_js_1.prisma.chatRoom.delete({
|
||||||
|
where: { id: roomId },
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: { id: roomId },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error("Delete chat room error:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Failed to delete chat room",
|
||||||
|
status: 500,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static async addChatRoomMember(userId, roomId, data) {
|
||||||
|
try {
|
||||||
|
const membership = await database_js_1.prisma.chatRoomMember.findUnique({
|
||||||
|
where: {
|
||||||
|
userId_roomId: {
|
||||||
|
userId,
|
||||||
|
roomId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!membership) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "You are not a member of this chat room",
|
||||||
|
status: 403,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (membership.role !== "admin") {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Only admins can add members",
|
||||||
|
status: 403,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const user = await database_js_1.prisma.user.findUnique({
|
||||||
|
where: { username: data.username },
|
||||||
|
});
|
||||||
|
if (!user) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "User not found",
|
||||||
|
status: 404,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const existingMembership = await database_js_1.prisma.chatRoomMember.findUnique({
|
||||||
|
where: {
|
||||||
|
userId_roomId: {
|
||||||
|
userId: user.id,
|
||||||
|
roomId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (existingMembership) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "User is already a member of this chat room",
|
||||||
|
status: 409,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
await database_js_1.prisma.chatRoomMember.create({
|
||||||
|
data: {
|
||||||
|
userId: user.id,
|
||||||
|
roomId,
|
||||||
|
role: "member",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const updatedRoom = await database_js_1.prisma.chatRoom.findUnique({
|
||||||
|
where: { id: roomId },
|
||||||
|
include: {
|
||||||
|
members: {
|
||||||
|
include: {
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
username: true,
|
||||||
|
isOnline: true,
|
||||||
|
lastSeen: true,
|
||||||
|
avatar: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
_count: {
|
||||||
|
select: {
|
||||||
|
messages: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: updatedRoom,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error("Add chat room member error:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Failed to add member to chat room",
|
||||||
|
status: 500,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static async removeChatRoomMember(userId, roomId, data) {
|
||||||
|
try {
|
||||||
|
const membership = await database_js_1.prisma.chatRoomMember.findUnique({
|
||||||
|
where: {
|
||||||
|
userId_roomId: {
|
||||||
|
userId,
|
||||||
|
roomId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!membership) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "You are not a member of this chat room",
|
||||||
|
status: 403,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (membership.role !== "admin") {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Only admins can remove members",
|
||||||
|
status: 403,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Check if target user is an admin
|
||||||
|
const targetMembership = await database_js_1.prisma.chatRoomMember.findUnique({
|
||||||
|
where: {
|
||||||
|
userId_roomId: {
|
||||||
|
userId: data.userId,
|
||||||
|
roomId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!targetMembership) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "User is not a member of this chat room",
|
||||||
|
status: 404,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (targetMembership.role === "admin") {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Cannot remove an admin from the chat room",
|
||||||
|
status: 403,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Remove user from chat room
|
||||||
|
await database_js_1.prisma.chatRoomMember.delete({
|
||||||
|
where: {
|
||||||
|
userId_roomId: {
|
||||||
|
userId: data.userId,
|
||||||
|
roomId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// Get updated chat room
|
||||||
|
const updatedRoom = await database_js_1.prisma.chatRoom.findUnique({
|
||||||
|
where: { id: roomId },
|
||||||
|
include: {
|
||||||
|
members: {
|
||||||
|
include: {
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
username: true,
|
||||||
|
isOnline: true,
|
||||||
|
lastSeen: true,
|
||||||
|
avatar: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
_count: {
|
||||||
|
select: {
|
||||||
|
messages: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: updatedRoom,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error("Remove chat room member error:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Failed to remove member from chat room",
|
||||||
|
status: 500,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exports.ChatService = ChatService;
|
||||||
191
backend/dist/services/messageService.js
vendored
Normal file
191
backend/dist/services/messageService.js
vendored
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.MessageService = void 0;
|
||||||
|
const database_js_1 = require("../config/database.js");
|
||||||
|
class MessageService {
|
||||||
|
static async sendMessage(userId, data) {
|
||||||
|
try {
|
||||||
|
const message = await database_js_1.prisma.message.create({
|
||||||
|
data: {
|
||||||
|
content: data.content,
|
||||||
|
image: data.image,
|
||||||
|
userId,
|
||||||
|
roomId: data.roomId,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
username: true,
|
||||||
|
avatar: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
reactions: {
|
||||||
|
include: {
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
username: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error("Send message error:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Failed to send message",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static async reactToMessage(userId, data) {
|
||||||
|
try {
|
||||||
|
const existingReaction = await database_js_1.prisma.messageReaction.findUnique({
|
||||||
|
where: {
|
||||||
|
userId_messageId_type: {
|
||||||
|
userId,
|
||||||
|
messageId: data.messageId,
|
||||||
|
type: data.type,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (existingReaction) {
|
||||||
|
await database_js_1.prisma.messageReaction.delete({
|
||||||
|
where: {
|
||||||
|
id: existingReaction.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
await database_js_1.prisma.messageReaction.create({
|
||||||
|
data: {
|
||||||
|
userId,
|
||||||
|
messageId: data.messageId,
|
||||||
|
type: data.type,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const message = await database_js_1.prisma.message.findUnique({
|
||||||
|
where: {
|
||||||
|
id: data.messageId,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
reactions: {
|
||||||
|
include: {
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
username: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
messageId: data.messageId,
|
||||||
|
reactions: message?.reactions ?? [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error("React to message error:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Failed to react to message",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static async markMessagesAsSeen(userId, messageIds) {
|
||||||
|
try {
|
||||||
|
const seenEntries = messageIds.map((messageId) => ({
|
||||||
|
userId,
|
||||||
|
messageId,
|
||||||
|
}));
|
||||||
|
await database_js_1.prisma.messageSeen.createMany({
|
||||||
|
data: seenEntries,
|
||||||
|
skipDuplicates: true,
|
||||||
|
});
|
||||||
|
const messages = await database_js_1.prisma.message.findMany({
|
||||||
|
where: {
|
||||||
|
id: { in: messageIds },
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
seenBy: {
|
||||||
|
include: {
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
username: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const seenData = messages.map((message) => ({
|
||||||
|
messageId: message.id,
|
||||||
|
seenBy: message.seenBy.map((seen) => seen.userId),
|
||||||
|
}));
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: seenData,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error("Mark messages as seen error:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Failed to mark messages as seen",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static async deleteMessage(userId, messageId, roomId) {
|
||||||
|
try {
|
||||||
|
const message = await database_js_1.prisma.message.findFirst({
|
||||||
|
where: {
|
||||||
|
id: messageId,
|
||||||
|
userId: userId,
|
||||||
|
roomId: roomId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!message) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Message not found or you don't have permission to delete it",
|
||||||
|
status: 403,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
await database_js_1.prisma.message.delete({
|
||||||
|
where: {
|
||||||
|
id: messageId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
messageId,
|
||||||
|
roomId,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error("Delete message error:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Failed to delete message",
|
||||||
|
status: 500,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exports.MessageService = MessageService;
|
||||||
74
backend/dist/services/userService.js
vendored
Normal file
74
backend/dist/services/userService.js
vendored
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
"use strict";
|
||||||
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||||
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.UserService = void 0;
|
||||||
|
const database_js_1 = require("../config/database.js");
|
||||||
|
const bcryptjs_1 = __importDefault(require("bcryptjs"));
|
||||||
|
class UserService {
|
||||||
|
static async updateProfile(userId, data) {
|
||||||
|
try {
|
||||||
|
if (data.username) {
|
||||||
|
const existingUser = await database_js_1.prisma.user.findFirst({
|
||||||
|
where: {
|
||||||
|
username: data.username,
|
||||||
|
id: { not: userId },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (existingUser) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Username already taken",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (data.email) {
|
||||||
|
const existingUser = await database_js_1.prisma.user.findFirst({
|
||||||
|
where: {
|
||||||
|
email: data.email,
|
||||||
|
id: { not: userId },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (existingUser) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Email already taken",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const updateData = {};
|
||||||
|
if (data.username)
|
||||||
|
updateData.username = data.username;
|
||||||
|
if (data.email)
|
||||||
|
updateData.email = data.email;
|
||||||
|
if (data.avatar !== undefined)
|
||||||
|
updateData.avatar = data.avatar;
|
||||||
|
if (data.password) {
|
||||||
|
updateData.password = await bcryptjs_1.default.hash(data.password, 12);
|
||||||
|
}
|
||||||
|
const updatedUser = await database_js_1.prisma.user.update({
|
||||||
|
where: { id: userId },
|
||||||
|
data: updateData,
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
username: true,
|
||||||
|
email: true,
|
||||||
|
avatar: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: updatedUser,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error("Update profile error:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Failed to update profile",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exports.UserService = UserService;
|
||||||
225
backend/dist/socket/socketHandlers.js
vendored
Normal file
225
backend/dist/socket/socketHandlers.js
vendored
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.handleConnection = void 0;
|
||||||
|
const database_js_1 = require("../config/database.js");
|
||||||
|
const rateLimiter_js_1 = require("../middleware/rateLimiter.js");
|
||||||
|
const chatService_js_1 = require("../services/chatService.js");
|
||||||
|
const messageService_js_1 = require("../services/messageService.js");
|
||||||
|
const handleConnection = async (io, socket) => {
|
||||||
|
console.log(`User connected: ${socket.user.username} (${socket.user.id})`);
|
||||||
|
await updateUserOnlineStatus(socket.userId, true);
|
||||||
|
const onlineUsers = await database_js_1.prisma.user.findMany({
|
||||||
|
where: { isOnline: true },
|
||||||
|
select: { id: true, username: true },
|
||||||
|
});
|
||||||
|
socket.emit("online_users", onlineUsers);
|
||||||
|
await joinUserRooms(socket);
|
||||||
|
socket.on("send_message", (data) => handleSendMessage(io, socket, data));
|
||||||
|
socket.on("delete_message", (data) => handleDeleteMessage(io, socket, data));
|
||||||
|
socket.on("react_to_message", (data) => handleReactToMessage(io, socket, data));
|
||||||
|
socket.on("typing_start", (data) => handleTypingStart(io, socket, data));
|
||||||
|
socket.on("typing_stop", (data) => handleTypingStop(io, socket, data));
|
||||||
|
socket.on("join_room", (roomId) => handleJoinRoom(socket, roomId));
|
||||||
|
socket.on("leave_room", (roomId) => handleLeaveRoom(socket, roomId));
|
||||||
|
socket.on("messages_seen", (data) => handleMessagesSeen(io, socket, data));
|
||||||
|
socket.on("disconnect", () => handleDisconnect(socket));
|
||||||
|
};
|
||||||
|
exports.handleConnection = handleConnection;
|
||||||
|
const handleTypingStart = async (io, socket, data) => {
|
||||||
|
try {
|
||||||
|
const isMember = await chatService_js_1.ChatService.checkRoomMembership(socket.userId, data.roomId);
|
||||||
|
if (!isMember)
|
||||||
|
return;
|
||||||
|
io.to(data.roomId).emit("user_typing", {
|
||||||
|
roomId: data.roomId,
|
||||||
|
username: data.username,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error(`Error handling typing start: ${error}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const handleTypingStop = async (io, socket, data) => {
|
||||||
|
try {
|
||||||
|
const isMember = await chatService_js_1.ChatService.checkRoomMembership(socket.userId, data.roomId);
|
||||||
|
if (!isMember)
|
||||||
|
return;
|
||||||
|
io.to(data.roomId).emit("user_stopped_typing", {
|
||||||
|
roomId: data.roomId,
|
||||||
|
username: data.username,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error(`Error handling typing stop: ${error}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const updateUserOnlineStatus = async (userId, isOnline) => {
|
||||||
|
try {
|
||||||
|
await database_js_1.prisma.user.update({
|
||||||
|
where: { id: userId },
|
||||||
|
data: {
|
||||||
|
isOnline,
|
||||||
|
lastSeen: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error(`Error updating user online status: ${error}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const joinUserRooms = async (socket) => {
|
||||||
|
try {
|
||||||
|
const userRooms = await database_js_1.prisma.chatRoomMember.findMany({
|
||||||
|
where: { userId: socket.userId },
|
||||||
|
include: { room: true },
|
||||||
|
});
|
||||||
|
userRooms.forEach(async (member) => {
|
||||||
|
await socket.join(member.roomId);
|
||||||
|
socket.to(member.roomId).emit("user_online", {
|
||||||
|
userId: socket.userId,
|
||||||
|
username: socket.user.username,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error(`Error joining user rooms: ${error}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const handleSendMessage = async (io, socket, data) => {
|
||||||
|
try {
|
||||||
|
if (!data.roomId) {
|
||||||
|
socket.emit("error", { message: "Room ID is required to send a message" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data.image && data.image.length > 2 * 1024 * 1024) {
|
||||||
|
socket.emit("error", { message: "Image size exceeds 2MB limit" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!data.content && !data.image) {
|
||||||
|
socket.emit("error", { message: "Message cannot be empty" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!(0, rateLimiter_js_1.checkMessageRateLimit)(socket))
|
||||||
|
return;
|
||||||
|
const isMember = await chatService_js_1.ChatService.checkRoomMembership(socket.userId, data.roomId);
|
||||||
|
if (!isMember) {
|
||||||
|
socket.emit("error", { message: "Not authorized to send messages to this room" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const result = await messageService_js_1.MessageService.sendMessage(socket.userId, data);
|
||||||
|
if (result.success) {
|
||||||
|
io.to(data.roomId).emit("new_message", result.data);
|
||||||
|
io.to(data.roomId).emit("user_stopped_typing", {
|
||||||
|
roomId: data.roomId,
|
||||||
|
username: socket.user.username,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
socket.emit("error", { message: "Failed to send message" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error(`Error handling send message: ${error}`);
|
||||||
|
socket.emit("error", { message: "Failed to send message" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const handleDeleteMessage = async (io, socket, data) => {
|
||||||
|
try {
|
||||||
|
const result = await messageService_js_1.MessageService.deleteMessage(socket.userId, data.messageId, data.roomId);
|
||||||
|
if (result.success) {
|
||||||
|
io.to(data.roomId).emit("message_deleted", {
|
||||||
|
messageId: data.messageId,
|
||||||
|
roomId: data.roomId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
socket.emit("error", { message: result.error });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error(`Error handling delete message: ${error}`);
|
||||||
|
socket.emit("error", { message: "Failed to delete message" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const handleReactToMessage = async (io, socket, data) => {
|
||||||
|
try {
|
||||||
|
const result = await messageService_js_1.MessageService.reactToMessage(socket.userId, data);
|
||||||
|
if (result.success) {
|
||||||
|
const message = await database_js_1.prisma.message.findUnique({
|
||||||
|
where: { id: data.messageId },
|
||||||
|
select: { roomId: true },
|
||||||
|
});
|
||||||
|
if (message) {
|
||||||
|
io.to(message.roomId).emit("message_reaction_updated", result.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
socket.emit("error", { message: result.error });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error(`Error handling react to message: ${error}`);
|
||||||
|
socket.emit("error", { message: "Failed to react to message" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const handleMessagesSeen = async (io, socket, data) => {
|
||||||
|
try {
|
||||||
|
const isMember = await chatService_js_1.ChatService.checkRoomMembership(socket.userId, data.roomId);
|
||||||
|
if (!isMember) {
|
||||||
|
socket.emit("error", { message: "Not authorized to access this room" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const messagesInRoom = await database_js_1.prisma.message.findMany({
|
||||||
|
where: {
|
||||||
|
id: { in: data.messageIds },
|
||||||
|
roomId: data.roomId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const validMessageIds = messagesInRoom.map((msg) => msg.id);
|
||||||
|
if (validMessageIds.length === 0)
|
||||||
|
return;
|
||||||
|
const result = await messageService_js_1.MessageService.markMessagesAsSeen(socket.userId, validMessageIds);
|
||||||
|
if (result.success && result.data) {
|
||||||
|
for (const item of result.data) {
|
||||||
|
io.to(data.roomId).emit("message_seen_update", {
|
||||||
|
messageId: item.messageId,
|
||||||
|
seenBy: item.seenBy,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error(`Error handling messages seen: ${error}`);
|
||||||
|
socket.emit("error", { message: "Failed to mark messages as seen" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const handleJoinRoom = async (socket, roomId) => {
|
||||||
|
await socket.join(roomId);
|
||||||
|
};
|
||||||
|
const handleLeaveRoom = async (socket, roomId) => {
|
||||||
|
await socket.leave(roomId);
|
||||||
|
};
|
||||||
|
const handleDisconnect = async (socket) => {
|
||||||
|
console.log(`User disconnected: ${socket.user.username}`);
|
||||||
|
await updateUserOnlineStatus(socket.userId, false);
|
||||||
|
try {
|
||||||
|
const userRooms = await database_js_1.prisma.chatRoomMember.findMany({
|
||||||
|
where: {
|
||||||
|
userId: socket.userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
userRooms.forEach((member) => {
|
||||||
|
socket.to(member.roomId).emit("user_offline", {
|
||||||
|
userId: socket.userId,
|
||||||
|
username: socket.user.username,
|
||||||
|
});
|
||||||
|
socket.to(member.roomId).emit("user_stopped_typing", {
|
||||||
|
roomId: member.roomId,
|
||||||
|
username: socket.user.username,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error(`Error handling user disconnect: ${error}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
2
backend/dist/types/index.js
vendored
Normal file
2
backend/dist/types/index.js
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
Loading…
x
Reference in New Issue
Block a user