commit 45d78dda8eede1228b28a943a6d70fc7154da4e8 Author: ch0ic3 Date: Sun Dec 15 06:11:44 2024 -0800 first commit diff --git a/.env b/.env new file mode 100644 index 0000000..23313f9 --- /dev/null +++ b/.env @@ -0,0 +1,6 @@ +DATABASE_URL="file:./dev.db" +SERVER_PRIVATE_KEY=BKnARIgeuGXwc9hDWtRliYDlO8kYpko3tQIjUlJ1V5bp+so= +SERVER_PUBLIC_KEY=pGJakB2GylbfI8mPYu8KK2p/jRPG20iylNReVX82z2hVkQknHoymVU3/LDzj0t+FCpj1qxDBJZHk2qX0RXx9FdwSzCaQ86ZnjPzlqAn7jITVknNve28113fKb0/TbgLZ3QK8k7C1rXbS13WtAcnssyMvvNeeha0l+mx4Bv/jt+AZrgTT2Cg1SqbwZhY1T+p6ULjvlbgoz7X+mncIctE5WC2al4+Syo5PZxnPLacMs5HsKhKeQwH40Nzx26aP4AiU6kFPAcBD8hJB0Q5GZK71EUZmghZCEdHMfWfYy6rebON8hGWT4VHnHUfO5HTydJJfIoDHrdqdzLGuffD1vyUURcS2zyYvRx4e2v5Da1oi2qaFdRTllIcr1/LAMxTfYfrsjtHGQQ5AcPqtNmlldrPStw7CkMxlgKTG8xBeUhndoLakyLU9Enj9HDT1QYh92OxLaSTI6BlxUjC7YYhYJ179yFfswXnvm8nBZlc+Gvr6XZG33nEgYb8BmwcfvhnirRTI +prime=2IGKThw2+H+X0Mc5ZxIfOGX1b3lLc/mDB2Ne73FdoEiJRRfWaETlLUg8dAFUWn4Jxg/QSbP7+f/Q0dZbPCShAXrqWsqxScNk+XvFRTUAhqq1h82Bh4Puok2P1ke6sPR/k+LCt9uHMlt/X0TodDonFIMr87KjB9bQ+zysfPXU2G9chMjqPpY2AkInPHfaMtKtIfBslXKbhGwtaK6t0h0GGJ7W3GU8e1dzo/JgwVDnqoTryAKdoFmrpjg41naQOC5Hl59i+Ik5yEL+NCvSis7IYo55sM6cbu8B4N4wNwDKkefgElADvhSKQJirbmSpPXs5Lr7GXgRBR6t/AGrYTgahPw== +generator=Ag== + diff --git a/package.json b/package.json new file mode 100644 index 0000000..2e0a483 --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "dependencies": { + "@prisma/client": "^5.22.0", + "bcryptjs": "^2.4.3", + "cors": "^2.8.5", + "crypto": "^1.0.1", + "dotenv": "^16.4.5", + "express": "^4.21.1", + "express-ws": "^5.0.2", + "js-chacha20": "^1.1.0", + "js-sha256": "^0.11.0", + "jsonwebtoken": "^9.0.2", + "prisma": "^5.22.0" + } +} diff --git a/prisma/dev.db b/prisma/dev.db new file mode 100644 index 0000000..caa77dd Binary files /dev/null and b/prisma/dev.db differ diff --git a/prisma/migrations/20241214175430_/migration.sql b/prisma/migrations/20241214175430_/migration.sql new file mode 100644 index 0000000..f8b7ebc --- /dev/null +++ b/prisma/migrations/20241214175430_/migration.sql @@ -0,0 +1,39 @@ +-- CreateTable +CREATE TABLE "FriendRequest" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "senderUsername" TEXT NOT NULL, + "receiverUsername" TEXT NOT NULL, + "status" TEXT NOT NULL DEFAULT 'pending', + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "FriendRequest_senderUsername_fkey" FOREIGN KEY ("senderUsername") REFERENCES "user" ("username") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "FriendRequest_receiverUsername_fkey" FOREIGN KEY ("receiverUsername") REFERENCES "user" ("username") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "Message" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "content" TEXT NOT NULL, + "nonce" TEXT NOT NULL, + "senderUsername" TEXT NOT NULL, + "receiverUsername" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "pubKey" TEXT NOT NULL, + CONSTRAINT "Message_senderUsername_fkey" FOREIGN KEY ("senderUsername") REFERENCES "user" ("username") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "Message_receiverUsername_fkey" FOREIGN KEY ("receiverUsername") REFERENCES "user" ("username") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "user" ( + "username" TEXT NOT NULL PRIMARY KEY, + "email" TEXT NOT NULL, + "password" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "pubKey" TEXT NOT NULL +); + +-- CreateIndex +CREATE UNIQUE INDEX "user_email_key" ON "user"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "user_pubKey_key" ON "user"("pubKey"); diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..e5e5c47 --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "sqlite" \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..ff517a0 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,44 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "sqlite" + url = "file:./dev.db" +} + +model FriendRequest { + id Int @id @default(autoincrement()) + senderUsername String + receiverUsername String + status String @default("pending") // Allowed values: "pending", "accepted", "rejected" + createdAt DateTime @default(now()) + + sender user @relation("SentRequests", fields: [senderUsername], references: [username]) + receiver user @relation("ReceivedRequests", fields: [receiverUsername], references: [username]) +} + +model Message { + id Int @id @default(autoincrement()) + content String + nonce String + senderUsername String + receiverUsername String + createdAt DateTime @default(now()) + pubKey String + sender user @relation("SentMessages", fields: [senderUsername], references: [username]) + receiver user @relation("ReceivedMessages", fields: [receiverUsername], references: [username]) +} + +model user { + username String @id + email String @unique + password String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + pubKey String @unique + sentMessages Message[] @relation("SentMessages") + receivedMessages Message[] @relation("ReceivedMessages") + sentRequests FriendRequest[] @relation("SentRequests") + receivedRequests FriendRequest[] @relation("ReceivedRequests") +} diff --git a/server.js b/server.js new file mode 100644 index 0000000..bbf0cb8 --- /dev/null +++ b/server.js @@ -0,0 +1,315 @@ +const express = require("express"); +const bcrypt = require("bcryptjs"); +const jwt = require("jsonwebtoken"); +const { PrismaClient } = require("@prisma/client"); +const crypto = require("crypto"); +const cors = require("cors"); +require("dotenv").config(); + +const prisma = new PrismaClient(); +const app = express(); +app.use(express.json()); + + +app.use(cors({ + origin: "*", // Allow all origins + methods: ["GET", "POST", "PUT", "DELETE"], // Allow all methods +})); + +const JWT_SECRET = "your_jwt_secret"; // Replace with a secure secret + +// Authentication Middleware +const authenticate = async (req, res, next) => { + const token = req.header("Authorization")?.replace("Bearer ", ""); + if (!token) return res.status(401).send("Access denied"); + + try { + const decoded = jwt.verify(token, JWT_SECRET); + req.user = decoded; + next(); + } catch (err) { + return res.status(401).send("Invalid token"); + } +}; + +// Register User +app.post("/register", async (req, res) => { + const { email, username, password, pubKey } = req.body; + + const userExists = await prisma.user.findUnique({ where: { username } }); + if (userExists) return res.status(400).send("User already exists"); + + const hashedPassword = await bcrypt.hash(password, 10); + const user = await prisma.user.create({ + data: { + email, + username, + password: hashedPassword, + pubKey, + }, + }); + + const token = jwt.sign({ username: user.username }, JWT_SECRET, { expiresIn: "1h" }); + res.send({ token }); +}); + +// Update Public Key +app.post("/user/updatePublicKey", authenticate, async (req, res) => { + const { pubKey } = req.body; + const username = req.user.username; + + if (!pubKey) return res.status(400).send("Public key is required"); + + try { + await prisma.user.update({ + where: { username }, + data: { pubKey }, + }); + + res.send({ message: "Public key updated successfully" }); + } catch (error) { + console.error("Error updating public key:", error); + res.status(500).send({ error: "Failed to update public key" }); + } +}); + + +// Login User +// Login User +app.post("/login", async (req, res) => { + const { username, password } = req.body; + + const user = await prisma.user.findUnique({ + where: { username }, + select: { password: true, pubKey: true }, + }); + if (!user) return res.status(400).send("Invalid credentials"); + + const isMatch = await bcrypt.compare(password, user.password); + if (!isMatch) return res.status(400).send("Invalid credentials"); + + const token = jwt.sign({ username }, JWT_SECRET, { expiresIn: "1h" }); + res.send({ token, pubKey: user.pubKey }); +}); + + +// Fetch pending friend requests sent to the authenticated user +app.get("/friend-requests/pending", authenticate, async (req, res) => { + const username = req.user.username; + + try { + const pendingRequests = await prisma.friendRequest.findMany({ + where: { + receiverUsername: username, + status: "pending", + }, + include: { + sender: { + select: { username: true }, + }, + }, + }); + + res.send(pendingRequests); + } catch (err) { + res.status(500).send({ error: "Failed to fetch pending friend requests" }); + } +}); + +// Send Friend Request +app.post("/friend-request", authenticate, async (req, res) => { + const { receiverUsername } = req.body; + const senderUsername = req.user.username; + + const receiver = await prisma.user.findUnique({ where: { username: receiverUsername } }); + if (!receiver) return res.status(404).send("User not found"); + + const existingRequest = await prisma.friendRequest.findFirst({ + where: { + OR: [ + { senderUsername, receiverUsername }, + { senderUsername: receiverUsername, receiverUsername: senderUsername }, + ], + }, + }); + + if (existingRequest) return res.status(400).send("Friend request already exists"); + + const request = await prisma.friendRequest.create({ + data: { + senderUsername, + receiverUsername, + status: "pending", + }, + }); + + res.send(request); +}); + +// Respond to Friend Request +app.post("/friend-request/respond", authenticate, async (req, res) => { + const { requestId, response } = req.body; + const username = req.user.username; + + const request = await prisma.friendRequest.findUnique({ where: { id: requestId } }); + if (!request || request.receiverUsername !== username) + return res.status(403).send("You cannot respond to this request"); + + const status = response === "accepted" ? "accepted" : "rejected"; + + await prisma.friendRequest.update({ + where: { id: requestId }, + data: { status }, + }); + + res.send("Response recorded"); +}); + +// Fetch Friends +app.get("/friends", authenticate, async (req, res) => { + const username = req.user.username; + + try { + const friends = await prisma.friendRequest.findMany({ + where: { + OR: [ + { senderUsername: username, status: "accepted" }, + { receiverUsername: username, status: "accepted" }, + ], + }, + include: { + sender: { select: { username: true, email: true } }, + receiver: { select: { username: true, email: true } }, + }, + }); + + const friendList = friends.map((f) => ({ + username: f.senderUsername === username ? f.receiver.username : f.sender.username, + email: f.senderUsername === username ? f.receiver.email : f.sender.email, + })); + + res.send(friendList); + } catch (err) { + res.status(500).send({ error: "Failed to fetch friends" }); + } +}); + +// Fetch Messages Between Two Users +app.get("/messages", authenticate, async (req, res) => { + const { receiverUsername } = req.query; + const username = req.user.username; + + if (!receiverUsername) { + return res.status(400).send("Receiver username is required"); + } + + const messages = await prisma.message.findMany({ + where: { + OR: [ + { senderUsername: username, receiverUsername }, + { senderUsername: receiverUsername, receiverUsername: username }, + ], + }, + orderBy: { createdAt: "asc" }, + }); + + res.send(messages); +}); + + +// Send a Message +app.post("/messages", authenticate, async (req, res) => { + const senderUsername = req.user.username; + const { encrypted, nonce, pubKey, receiverUsername } = req.body; + console.log(encrypted) + if (!encrypted || !nonce || !pubKey || !receiverUsername) { + return res.status(400).send("Missing required fields (encrypted, nonce, pubKey, receiverUsername)"); + } + + const receiver = await prisma.user.findUnique({ where: { username: receiverUsername } }); + if (!receiver) return res.status(404).send("Receiver not found"); + + const isFriend = await prisma.friendRequest.findFirst({ + where: { + OR: [ + { senderUsername, receiverUsername, status: "accepted" }, + { senderUsername: receiverUsername, receiverUsername: senderUsername, status: "accepted" }, + ], + }, + }); + + if (!isFriend) return res.status(403).send("You can only message friends"); + + const message = await prisma.message.create({ + data: { + content: encrypted, + senderUsername, + receiverUsername, + nonce: nonce, + pubKey: pubKey, + }, + }); + + res.send(message); +}); + +app.get("/test" , async(req,res) => { + const messages = await prisma.message.findMany() + res.send(messages) +}) + + +// Get User Public Key if they are friends +app.get("/user/pubkey", authenticate, async (req, res) => { + const { username: friendUsername } = req.query; // Username of the requested friend + const username = req.user.username; // Authenticated user's username + + try { + // Check if the user exists + const user = await prisma.user.findUnique({ + where: { username: friendUsername }, + select: { pubKey: true }, // Only select the public key + }); + + if (!user) { + return res.status(404).send("User not found"); + } + + // Check if the authenticated user is friends with the requested user + const isFriend = await prisma.friendRequest.findFirst({ + where: { + OR: [ + { senderUsername: username, receiverUsername: friendUsername, status: "accepted" }, + { senderUsername: friendUsername, receiverUsername: username, status: "accepted" }, + ], + }, + }); + + if (!isFriend) { + return res.status(403).send("You can only fetch the public key of your friends"); + } + + // Return the public key + res.send({ pubKey: user.pubKey }); + } catch (err) { + res.status(500).send({ error: "Failed to fetch the public key" }); + } +}); + + +// Delete Message +app.delete("/messages/:id", authenticate, async (req, res) => { + const { id } = req.params; + const username = req.user.username; + + const message = await prisma.message.findUnique({ where: { id: parseInt(id) } }); + if (!message || message.senderUsername !== username) + return res.status(403).send("You can only delete your own messages"); + + await prisma.message.delete({ where: { id: parseInt(id) } }); + res.send("Message deleted successfully"); +}); + +app.listen(4000, () => { + console.log("Server running on http://localhost:4000"); +});