cicd 3
Some checks failed
Build and Deploy / build-and-deploy (push) Failing after 58s

This commit is contained in:
Gal Podlipnik 2025-06-12 16:22:33 +02:00
parent 27a06adbe6
commit 365aea2997
21 changed files with 1491 additions and 1 deletions

View File

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

View File

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

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

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

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,2 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });