Files
relay/server/server.js
2024-11-21 22:44:15 +01:00

312 lines
9.6 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 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).toString();
const originalName = file.originalname;
const filename = originalName?.substring(0, originalName.lastIndexOf("."));
//const finalName = uniqueSuffix + extension;
const finalName = `${filename}-${Math.round(Math.random() * 1e9)}${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().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}/attachments/${finalName}`;
res.json({
message: "File uploaded successfully",
attachment_url: url,
});
},
);
initializeSocket(io);
server.listen(PORT, () => {
console.log(`Server is running on port: ${PORT}`);
});