457 lines
13 KiB
JavaScript
457 lines
13 KiB
JavaScript
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,
|
|
} = 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" });
|
|
} 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" });
|
|
})
|
|
.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 usernameContact = req.params.contact;
|
|
|
|
// Validate username for invalid characters, length, and type
|
|
if (!isValidUsername(usernameContact)) {
|
|
return res.status(400).json({ message: "Invalid username provided" });
|
|
}
|
|
|
|
try {
|
|
const result = await insertContact(
|
|
req.user.username,
|
|
usernameContact,
|
|
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 });
|
|
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);
|
|
if (result !== null) {
|
|
io.to(result).to(group_id).emit("added to group", {
|
|
username,
|
|
user_id: result,
|
|
group_id,
|
|
});
|
|
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" });
|
|
}
|
|
},
|
|
);
|
|
|
|
initializeSocket(io);
|
|
|
|
server.listen(PORT, () => {
|
|
console.log(`Server is running on port: ${PORT}`);
|
|
});
|