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 crypto = require("crypto"); 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, insertMessage, checkUserExist, changePassword, 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, verifyJwtToken } = require("./auth/jwt"); const { initializeSocket } = require("./socket/socket"); const { getContacts, insertContact } = 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); const finalName = uniqueSuffix + extension; 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.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" }); } 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().toLowerCase(); 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 = await getUserId(username); const token = generateJwtToken(username, user_id); res.cookie("token", token, { maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days }); return res.status(200).json({ message: "Successfully logged In" }); }) .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, }); }); app.delete("/api/chat/contacts/:contact", authorizeUser, async (req, res) => { if (!req.params.contact) { return res .status(400) .json({ message: "Missing usernameContact parameter" }); } const usernameContact = req.params.contact; // Validate username for invalid characters, length, and type if (!isValidUsername(usernameContact)) { return res.status(400).json({ message: "Invalid username provided" }); } await deleteContact(req.user.username, usernameContact); return res.status(200).json({ message: "Successfully deleted contact" }); }); app.put("/api/chat/contacts/:contact", authorizeUser, async (req, res) => { if (!req.params.contact) { return res.status(400).json({ message: "Missing contact parameter" }); } const usernameContact = req.params.contact; // Validate username for invalid characters, length, and type if (!isValidUsername(usernameContact)) { return res.status(400).json({ message: "Invalid contact provided" }); } const read = req.body.status; await updateContactStatus(req.user.username, usernameContact, read); return res .status(200) .json({ message: "Successfully updated contact status" }); }); app.post("/api/chat/contact/:contact", authorizeUser, async (req, res) => { if (!req.params.contact) { return res.status(400).json({ message: "Missing contact parameter" }); } const usernameContact = req.params.contact; // Validate username for invalid characters, length, and type if (!isValidUsername(usernameContact)) { return res.status(400).json({ message: "Invalid username provided" }); } await insertContact(req.user.username, usernameContact, true); return res.status(200).json({ message: "Successfully inserted contact" }); }); app.get("/api/chat/contacts", authorizeUser, async (req, res) => { if (!req.user.username) { return res.status(401).json({ message: "Missing username (that's weird you shouldn't see that)", }); } const contacts = await getContacts(req.user.username); console.log("Sent contacts list for: ", req.user.username); return res.status(200).json(contacts); }); 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.username, req.params.contact, limit, cursor, ); if (messages && messages.length === 0) { return res.status(404).json({ message: "No more messages found" }); } console.log("Sent messages for: ", req.user.username, "messages: ", messages); return res.status(200).json({ messages }); }); app.post("/api/chat/sendmessage", authorizeUser, async (req, res) => { const message = req.body.message?.trim(); //TODO filter for invalid characters in message return res.status(500).json({ message: "HUJ!" }); }); app.post( "/api/chat/attachment", upload.single("attachment"), authorizeUser, async (req, res) => { if (!req.file) { return res.status(400).json({ message: "No file specified" }); } const { finalName } = req.file; const url = process.env.ORIGIN; res.json({ message: "File uploaded successfully", attachmentUrl: req.file, url: url, }); }, ); initializeSocket(io); server.listen(PORT, () => { console.log(`Server is running on port: ${PORT}`); });