const express = require("express"); const { createServer } = require("http"); const app = express(); const cors = require("cors"); const server = createServer(app); const bodyParser = require("body-parser"); const cookieParser = require("cookie-parser"); const bcrypt = require("bcrypt"); const saltRounds = 10; const { Server } = require("socket.io"); const io = new Server(server); const multer = require("multer"); const { join } = require("path"); const { existsSync, mkdirSync } = require("fs"); require("dotenv").config(); const PORT = process.env.SERVER_PORT; const { insertUser, checkUserExist, getPassword, getUserId, deleteContact, updateContactStatus, getMessages, } = require("./db/db.js"); const authorizeUser = require("./utils/authorize"); const { isValidUsername, MIN_USERNAME_LENGTH, MAX_USERNAME_LENGTH, MAX_PASSWORD_LENGTH, MIN_PASSWORD_LENGTH, } = require("./utils/filter"); const { generateJwtToken } = require("./auth/jwt"); const { initializeSocket } = require("./socket/socket"); const { getContacts, insertContact, createGroup, addMemberToGroupByUsername, contactSuggestion, deleteMessage, getMembers, insertContactByUsername, isConversationMember, isAdmin, getLatestMessage, } = require("./db/db"); const { extname } = require("node:path"); const corsOptions = { origin: process.env.ORIGIN, optionsSuccessStatus: 200, credentials: true, }; const storage = multer.diskStorage({ destination: function (req, file, cb) { const dir = join(__dirname, "attachments"); if (!existsSync(dir)) { mkdirSync(dir, { recursive: true }); } cb(null, dir); }, filename: function (req, file, cb) { const uniqueSuffix = Date.now() + "-" + Math.round(Math.random() * 1e9); const extension = extname(file.originalname).toString(); const originalName = file.originalname; const filename = originalName?.substring(0, originalName.lastIndexOf(".")); const finalName = `${filename}-${uniqueSuffix}${extension}`; file.finalName = finalName; cb(null, finalName); }, }); const upload = multer({ storage: storage }); // Serve socket.io js app.use("/socket.io", express.static("./node_modules/socket.io/client-dist/")); app.use("/attachments", express.static("./attachments")); app.use(cors(corsOptions)); app.use(bodyParser.json()); app.use(cookieParser()); app.use(express.json({ limit: "100G" })); app.use(express.urlencoded({ extended: true, limit: "100G" })); app.post("/api/auth/signup", async (req, res) => { try { const username = req.body.username; const password = req.body.password; if (!username) { return res.status(400).json({ message: "No username provided" }); } else if (!password) { return res.status(400).json({ message: "No password provided" }); } // Check for invalid characters in password const validChars = /^[A-Za-z0-9!@#$%^&*(),.?":{}|<>]+$/; if (!validChars.test(password)) { return res .status(400) .json({ message: "Password contains invalid character" }); } // Validate username for invalid characters, length, and type if (!isValidUsername(username)) { return res.status(400).json({ message: "Invalid username provided" }); } // Validate form data length if ( !password || password.length < MIN_PASSWORD_LENGTH || password.length > MAX_PASSWORD_LENGTH ) { return res.status(400).json({ message: "Invalid password length" }); } // Checks if user already exist in database const exist = await checkUserExist(username); if (exist) { return res.status(409).json({ message: "User already exist" }); } // Hash password and insert hash and username to database const hash = await bcrypt.hash(password, saltRounds); // Insert username and password hash to database await insertUser(username, hash); // Get user id from database to store it in jwt token const { user_id } = await getUserId(username); console.log(`Registered: ${username} with id: ${user_id}`); // Set JWT token to cookies const token = generateJwtToken(username, user_id); res.cookie("token", token, { maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days httpOnly: true, // secure: true, }); return res.status(200).json({ message: "Successfully signed up", username: username, user_id: user_id, }); } catch (e) { console.error("Signup error: ", e); return res.status(500).json({ message: "internal server error" }); } }); app.post("/api/auth/login", async (req, res) => { try { const username = req.body.username?.trim(); const password = req.body.password; if (!isValidUsername(username)) { return res.status(400).json({ message: "Invalid credentials" }); } if ( !username || !password || username.length < MIN_USERNAME_LENGTH || username.length > MAX_USERNAME_LENGTH || password.length < MIN_PASSWORD_LENGTH || password.length > MAX_PASSWORD_LENGTH ) { return res.status(400).json({ message: "Invalid credentials" }); } // Checks if the user exist const exist = await checkUserExist(username); if (!exist) { return res.status(404).json({ message: "User does not exist" }); } const hashedPassword = await getPassword(username); // Compare passwords bcrypt .compare(password, hashedPassword) .then(async (result) => { if (!result) { res.status(401).json({ message: "Invalid password" }); return; } const { user_id, dbusername } = await getUserId(username); const token = generateJwtToken(dbusername, user_id); res.cookie("token", token, { maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days }); return res.status(200).json({ message: "Successfully logged In", username: username, user_id: user_id, }); }) .catch((e) => { console.error("Failed to compare password: ", e); return res.status(500).json({ message: "Internal server error" }); }); } catch (e) { console.error("Login error: ", e); return res.status(500).json({ message: "Internal server error" }); } }); app.get("/api/auth/validate", authorizeUser, (req, res) => { return res.status(200).json({ message: "Authorized", username: req.user.username, user_id: req.user.user_id, }); }); app.delete( "/api/chat/contacts/:contact_id/:conversation_id", authorizeUser, async (req, res) => { const contact_id = parseInt(req.params.contact_id); const conversation_id = parseInt(req.params.conversation_id); console.log("Delete contact for: ", req.params.contact_id, conversation_id); const result = await deleteContact( req.user.user_id, contact_id, conversation_id, ); if (result?.message) { return res.status(400).json({ message: result.message }); } return res.status(200).json({ message: "Successfully deleted contact" }); }, ); app.put( "/api/chat/contacts/:conversation_id", authorizeUser, async (req, res) => { const conversation_id = req.params.conversation_id; if (!conversation_id) { return res .status(400) .json({ message: "Missing conversation id parameter" }); } const read = req.body.status; //await updateContactStatus(req.user.user_id, conversation_id, read); return res .status(200) .json({ message: `Successfully updated contact status to: ${read}` }); }, ); app.post("/api/chat/contact/:contact", authorizeUser, async (req, res) => { if (!req.params.contact) { return res.status(400).json({ message: "Missing contact parameter" }); } const contactUsername = req.params.contact; // Validate username for invalid characters, length, and type if (!isValidUsername(contactUsername)) { return res.status(400).json({ message: "Invalid username provided" }); } // Get contact user id and check if exist const contact = await getUserId(contactUsername); if (!contact) { return res.status(400).json({ message: "User does not exist" }); } try { const result = await insertContact( req.user.user_id, contact.user_id, contactUsername, true, ); if (result === null) { return res.status(500).json({ message: "Failed to create conversation" }); } if (!result.user_id) { return res.status(500).json({ message: "Something went wrong" }); } console.log("sent contact post response: ", result); return res.status(200).json(result); } catch (e) { console.error("Failed to insert contact: ", e); return res.status(500).json({ message: "Failed to insert contact" }); } }); app.get("/api/chat/contacts", authorizeUser, async (req, res) => { if (!req.user.user_id) { return res.status(401).json({ message: "Missing username (that's weird you shouldn't see that)", }); } const contacts = await getContacts(req.user.user_id); console.log("Sent contacts list for: ", req.user.user_id); return res.status(200).json(contacts); }); app.get( "/api/chat/contacts/suggestions/:contact", authorizeUser, async (req, res) => { const contact = req.params.contact; if (!contact) { return res.status(400).json({ message: "contact not provided", }); } try { const suggestions = await contactSuggestion(contact); return res.status(200).json(suggestions); } catch (e) { res.status(500).json({ message: "Failed to get contact suggestions" }); } }, ); app.get("/api/chat/messages/:contact", authorizeUser, async (req, res) => { if (!req.params.contact) { return res.status(400).json({ message: "Missing contact parameter" }); } const limit = parseInt(req.query.limit) || 50; const cursor = parseInt(req.query.cursor) || 0; const messages = await getMessages( req.user.user_id, req.params.contact, limit, cursor, ); if (messages && messages.length === 0) { return res .status(200) .json({ messages: [], message: "No more messages found" }); } console.log("Sent messages for: ", req.user.user_id); return res.status(200).json({ messages }); }); app.post( "/api/chat/attachments", upload.any("attachments"), authorizeUser, async (req, res) => { if (req.files?.length < 1) { return res.status(400).json({ message: "No file specified" }); } const attachment_urls = req.files.map((file) => { return `${process.env.ORIGIN}/attachments/${file.finalName}`; }); res.json(attachment_urls); }, ); app.post("/api/chat/groups/create", authorizeUser, async (req, res) => { const user_id = req.user.user_id; const groupname = req.body.groupName; if (!groupname) { return res.status(400).json({ message: "Groupname not provided" }); } const { group_id, contact_user_id, errorMessage } = await createGroup( user_id, groupname, ); if (errorMessage) { return res.status(401).json({ message: errorMessage }); } if (!group_id) { return res.status(500).json({ message: "Failed to create group" }); } console.log("Successfully created group: ", groupname, "id: ", group_id); console.log(`io.to: ${contact_user_id} added to group`); io.to(contact_user_id).emit("added to group", { group_id, username: groupname, }); return res.status(200).json({ message: `Successfully created group: ${groupname}`, group_id: group_id, }); }); app.post("/api/chat/groups/addMember", authorizeUser, async (req, res) => { const username = req.body.username; const group_id = req.body.group_id; const user_id = req.user.user_id; if (!username) { return res.status(400).json({ message: "Username not provided" }); } if (!group_id) { return res.status(400).json({ message: "group_id not provided" }); } const isUserAdmin = await isAdmin(user_id, group_id); if (!isUserAdmin) { return res.status(401).json({ message: "You are not group administrator" }); } const result = await addMemberToGroupByUsername(group_id, username); const lastMessage = await getLatestMessage(group_id); console.error("ADDED TO GROUP: ", { username, user_id: result, group_id, ...lastMessage, }); if (result !== null) { io.to(result) .to(group_id) .emit("added to group", { username, user_id: result, group_id, ...lastMessage, }); console.log("added to group: ", result); return res.status(200).json({ message: "Successfully added member" }); } res.status(500).json({ message: "Failed to add member" }); }); app.delete( "/api/chat/messages/:message_id", authorizeUser, async (req, res) => { const message_id = req.params.message_id; console.log("delete message: ", req.user.user_id, message_id); if (!message_id) { return res.status(500).json({ message: "No message_id provided" }); } const result = await deleteMessage(req.user.user_id, message_id); if (result?.message) { return res.status(401).json({ message: result.message }); } else { return res.status(200).json({ message: "Successfully deleted message" }); } }, ); app.get( "/api/chat/groups/getMembers/:conversation_id", authorizeUser, async (req, res) => { const conversation_id = req.params.conversation_id; if (!conversation_id) { return res.status(400).json({ message: "No conversation_id provided" }); } const isMember = await isConversationMember( req.user.user_id, conversation_id, ); if (!isMember) { return res .status(401) .json({ message: "You are not member of this conversation" }); } const participants = await getMembers(conversation_id); console.log( `getMemers for conversation: ${conversation_id}, participants: ${participants} `, ); if (participants) { return res.status(200).json(participants); } else { return res.status(500).json({ message: "Failed to get group members" }); } }, ); app.get( "/api/chat/messages/lastMessage", authorizeUser, async (req, res) => {}, ); initializeSocket(io); server.listen(PORT, () => { console.log(`Server is running on port: ${PORT}`); });