Spaces:
Paused
Paused
| const express = require("express"); | |
| const bodyParser = require("body-parser"); | |
| const { sha } = require("sha256quanvn"); | |
| const { PrismaClient } = require("@prisma/client"); | |
| const multer = require("multer"); | |
| const fs = require("fs"); | |
| const statusMonitor = require("express-status-monitor")({ | |
| path: "/bee3hivestatuspagethatnoonewillrememberthisurl", | |
| title: "Bee3Hive API Status", | |
| }); | |
| const cors = require("cors"); | |
| const { withAccelerate } = require("@prisma/extension-accelerate"); | |
| const { error } = require("console"); | |
| const prisma = new PrismaClient().$extends(withAccelerate()) | |
| const app = express(); | |
| const SALT = process.env.PRIVATE_KEY; | |
| const services = ["https://quanvndzai-ipfs-server-1.hf.space"]; | |
| const queueFileUpload = {}; | |
| const info = (message) => { | |
| console.log(`<${new Date()}> [INFO] - ${message}`); | |
| }; | |
| const warn = (message) => { | |
| console.log(`<${new Date()}> [INFO] - ${message}`); | |
| }; | |
| //* Sussy bigint thing | |
| BigInt.prototype["toJSON"] = function () { | |
| return this.toString(); | |
| }; | |
| //* Safe Base64 Encode | |
| const safeb64 = (str) => { | |
| return btoa(str).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_"); | |
| }; | |
| /** | |
| * Returns a new array with all duplicates removed. | |
| * | |
| * @param {Array} a - The array to remove duplicates from. | |
| * @return {Array} - A new array with all duplicates removed. | |
| */ | |
| function uniq(a) { | |
| var seen = {}; | |
| return a.filter(function(item) { | |
| return seen.hasOwnProperty(item) ? false : (seen[item] = true); | |
| }); | |
| } | |
| /** | |
| * Generates a new JSON Web Token for the given user ID, and returns it as a string. | |
| * | |
| * The token is valid for 1 day, and is signed with the server's private key. | |
| * | |
| * @param {number} id - The user's ID. | |
| * @return {string} A new JSON Web Token, as a string. | |
| */ | |
| const generateJWT = async (id) => { | |
| const header = { | |
| alg: "SHA256QUANVN", | |
| typ: "JWT", | |
| }; | |
| const payload = { | |
| id: id, | |
| exp: Date.now() + 3600000 * 24 * 7, // 7 days | |
| }; | |
| await prisma.users.update({ | |
| where: { | |
| id: payload.id, | |
| }, | |
| data: { | |
| last_login: new Date(), | |
| }, | |
| }); | |
| const token = | |
| safeb64(JSON.stringify(header)) + "." + safeb64(JSON.stringify(payload)); | |
| const sig = sha(token + "." + SALT); | |
| info("Generated new token for user: " + id); | |
| // console.log("New Token generated: " + token + "." + sig) | |
| return token + "." + safeb64(sig); | |
| }; | |
| const generateFileID = () => { | |
| const characters = | |
| "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; | |
| let fileID = ""; | |
| for (let i = 0; i < 6; i++) { | |
| fileID += characters.charAt(Math.floor(Math.random() * characters.length)); | |
| } | |
| return fileID; | |
| }; | |
| async function checkServer() { | |
| // info("Checking servers..."); | |
| for(var i = services.length - 1; i >= 0; i--) { | |
| try { | |
| const resp = await fetch(services[i] + "/availible"); | |
| if(!resp.ok) { | |
| // This server is ded but can try again later lol | |
| info(await resp.text()); | |
| warn("Server " + services[i] + " fail to ping"); | |
| continue; | |
| } | |
| const data = await resp.text(); | |
| // info(data) | |
| if(data != "\"Ok!\"") { | |
| //This server is full | |
| warn("Server " + services[i] + " is full"); | |
| services.splice(i, 1); | |
| } | |
| } catch(e) { | |
| // This server is ded but can try again later lol | |
| warn("Server " + services[i] + " fail to fetch"); | |
| // services.splice(i, 1); | |
| } | |
| } | |
| } | |
| setInterval(checkServer, 60 * 1000) | |
| checkServer() | |
| // Configure Multer storage | |
| const storage = multer.diskStorage({ | |
| destination: (req, file, cb) => { | |
| cb(null, "uploads/"); | |
| }, | |
| filename: (req, file, cb) => { | |
| // cb(null, generateFileID() + '-' + Date.now()); | |
| cb(null, generateFileID() + "-" + file.originalname); | |
| }, | |
| }); | |
| // Set file size limit to 10GB | |
| const upload = multer({ | |
| storage: storage, | |
| limits: { fileSize: 10 * 1024 * 1024 * 1024 }, // 10GB | |
| }); | |
| // console.log(file) | |
| const current_connections = []; | |
| app.use( | |
| cors({ | |
| origin: "*", | |
| }) | |
| ); | |
| app.use(bodyParser.json()); //utilizes the body-parser package | |
| app.use(bodyParser.urlencoded({extended: true})); | |
| app.use(statusMonitor); | |
| app.use("/upload_queue", express.static("uploads")); | |
| app.get( | |
| "/bee3hivestatuspagethatnoonewillrememberthisurl", | |
| statusMonitor.pageRoute | |
| ); | |
| app.get("/bee3hive/internal/files/:node_id", async (req, res) => { | |
| // TODO: Make database query for services | |
| const node_id = req.params.node_id; | |
| const record = await prisma.storage.findMany({ | |
| where: { | |
| handle: node_id, | |
| }, | |
| cacheStrategy: { | |
| ttl: 60, //* 1 min | |
| }, | |
| }) | |
| if(!record) { | |
| res.sendStatus(404); | |
| return; | |
| } | |
| res.send(record); | |
| }) | |
| // TODO: Make database query for services | |
| app.post("/auth/email/register", async (req, res) => { | |
| let { username, email, password } = req.body; | |
| if (!username || !email || !password) { | |
| info("Missing required fields registration"); | |
| res.sendStatus(400); | |
| return; | |
| } | |
| if (username.length < 6) { | |
| info("Username: " + username + " is too short!"); | |
| res.sendStatus(400); | |
| return; | |
| } | |
| username = username.trim(); | |
| password = password.trim(); | |
| email = email.trim(); | |
| // console.log(username, email, password) | |
| if ( | |
| ["admin", "administrator", "root", "user", "anonymous", "guest"].includes( | |
| username | |
| ) | |
| ) { | |
| warn("Username not allowed: " + username); | |
| res.sendStatus(401); | |
| return; | |
| } | |
| const record = await prisma.users.findUnique({ | |
| where: { | |
| email: email, | |
| }, | |
| }); | |
| if (record) { | |
| info("User already exists: " + username); | |
| res.status(400).send({ error: "Username already exists" }); | |
| return; | |
| } | |
| try { | |
| info( | |
| "Creating user: " + | |
| email + | |
| " username: " + | |
| username + | |
| " password: " + | |
| sha(password) + | |
| " " + | |
| new Date() | |
| ); | |
| const created = await prisma.users.create({ | |
| data: { | |
| username: username, | |
| email: email, | |
| password_hash: sha(password), | |
| last_login: new Date(), | |
| }, | |
| }); | |
| info("User created: " + email + " username: " + username); | |
| res.send({ token: await generateJWT(created.id) }); | |
| return; | |
| } catch (e) { | |
| warn("Error on creating users"); | |
| console.log(e); | |
| res.sendStatus(500); | |
| return; | |
| } | |
| }); | |
| app.post("/auth/email/login", async (req, res) => { | |
| const { email, password } = req.body; | |
| if (!email || !password) { | |
| info("Missing required fields login"); | |
| res.sendStatus(400); | |
| return; | |
| } | |
| const record = await prisma.users.findUnique({ | |
| where: { | |
| email: email, | |
| }, | |
| }); | |
| if (!record) { | |
| info("User not found: " + email); | |
| res.sendStatus(401); | |
| return; | |
| } | |
| if (record.password_hash != sha(password)) { | |
| warn("Wrong password attempt: " + email); | |
| res.sendStatus(401); | |
| return; | |
| } | |
| info("User logged in: " + email); | |
| res.send({ token: await generateJWT(record.id) }); | |
| }); | |
| app.use(async (req, res, next) => { | |
| if (!req.headers.authorization) { | |
| info("Authorization header not found"); | |
| res.sendStatus(401); | |
| return; | |
| } | |
| let token; | |
| try { | |
| token = req.headers.authorization.slice(7); // Bearer token | |
| } catch (e) { | |
| warn("Parsing token fail"); | |
| console.log(e); | |
| return; | |
| } | |
| if (!token) { | |
| info("Token not found"); | |
| res.sendStatus(401); | |
| return; | |
| } | |
| const [tokenHeader, tokenPayload, tokenSig] = token.split("."); | |
| if (!tokenHeader || !tokenPayload || !tokenSig) { | |
| info("Invalid token"); | |
| res.sendStatus(401); | |
| return; | |
| } | |
| // console.log(tokenHeader, tokenPayload, tokenSig) | |
| try { | |
| // console.lo | |
| const header = JSON.parse(Buffer.from(tokenHeader, "base64").toString()); | |
| const payload = JSON.parse(Buffer.from(tokenPayload, "base64").toString()); | |
| const token_sig = Buffer.from(tokenSig, "base64").toString(); | |
| const sig = sha(tokenHeader + "." + tokenPayload + "." + SALT); | |
| // console.log(header, payload, token_sig, sig) | |
| if (sig !== token_sig) { | |
| warn("Invalid signature"); | |
| res.sendStatus(401); | |
| return; | |
| } | |
| if (Date.now() > payload.exp) { | |
| warn("Token expired"); | |
| res.sendStatus(401); | |
| return; | |
| } | |
| const record = await prisma.users.findUnique({ | |
| where: { | |
| id: payload.id, | |
| }, | |
| }); | |
| if (!record) { | |
| warn("Record not found"); | |
| res.sendStatus(500); // How? | |
| return; | |
| } | |
| info("Authorized: " + record.id); | |
| req.user = record; | |
| next(); | |
| return; | |
| } catch (e) { | |
| console.log(e); | |
| res.sendStatus(401); | |
| return; | |
| } | |
| }); | |
| app.post("/auth/me", async (req, res) => { | |
| if (!Array.isArray(queueFileUpload[req.user.username])) { | |
| queueFileUpload[req.user.username] = []; | |
| } | |
| // User already authorize | |
| res.send(req.user); | |
| }); | |
| app.post("/auth/delete", async (req, res) => { | |
| // User already authorize | |
| const { password } = req.body; | |
| if (!password) { | |
| info("Missing password when deleting user"); | |
| res.sendStatus(400); | |
| return; | |
| } | |
| // Check password | |
| if (sha(password) != req.user.password_hash) { | |
| res.sendStatus(401); | |
| return; | |
| } | |
| // Delete User | |
| try { | |
| await prisma.users.delete({ | |
| where: { | |
| id: req.user.id, | |
| }, | |
| }); | |
| res.sendStatus(200); | |
| return; | |
| } catch (e) { | |
| warn("Error on deleting user"); | |
| console.log(e); | |
| res.sendStatus(500); | |
| return; | |
| } | |
| }); | |
| app.post("/file/upload", upload.single("file"), async (req, res) => { | |
| try { | |
| const tests = fs.readdirSync("uploads"); | |
| console.log(tests) | |
| console.log(req.file) | |
| const upload_path = req.file.path; | |
| const file_path = req.body.filePath; | |
| const full_file_path = req.user.username + (file_path ? "/" + file_path : ""); | |
| // Upload the file to another backend | |
| if(services.length == 0) { | |
| res.status(500).send("Service unavailable"); | |
| return; | |
| } | |
| const server = services[Math.floor(Math.random() * services.length)]; | |
| const body = { | |
| filename: file_path, | |
| directory: full_file_path, | |
| username: req.user.username, | |
| fileURI: req.file.filename, | |
| } | |
| // console.log(body) | |
| // Upload the file | |
| try { | |
| info("Uploading file to " + server); | |
| const resp = await fetch(server + "/fetch", { | |
| method: "POST", | |
| headers: { | |
| "Content-Type": "application/json", | |
| }, | |
| body: JSON.stringify(body), | |
| }); | |
| if(!resp.ok) { | |
| warn(await resp.text()); | |
| res.sendStatus(500); | |
| return; | |
| } | |
| const json = await resp.json(); | |
| console.log(json) | |
| if(await prisma.storage.findUnique({ where: { dir: full_file_path } })) { | |
| // File already exists and same content | |
| warn("File already exists. No override: " + full_file_path); | |
| res.sendStatus(200); | |
| return; | |
| } | |
| if(await prisma.storage.findUnique({ where: { dir: full_file_path } })) { | |
| warn("File already exists. Override: " + full_file_path); | |
| await prisma.storage.update({ | |
| where:{ | |
| dir: full_file_path | |
| }, | |
| data: { | |
| handle: server, | |
| username: req.user.username, | |
| CID: json[0].Hash, | |
| size: +json[0].Size, | |
| dir: full_file_path, | |
| } | |
| }) | |
| } else { | |
| info("Adding record: " + full_file_path); | |
| //* Add record | |
| await prisma.storage.create({ | |
| data: { | |
| handle: server, | |
| username: req.user.username, | |
| CID: json[0].Hash, | |
| size: +json[0].Size, | |
| dir: full_file_path, | |
| } | |
| }) | |
| } | |
| } catch (e) { | |
| warn("Error on uploading file"); | |
| console.log(e); | |
| res.sendStatus(500); | |
| fs.unlinkSync(upload_path); | |
| return; | |
| } | |
| fs.unlinkSync(upload_path); | |
| info("File uploaded successfully: " + full_file_path); | |
| res.send("File uploaded successfully."); | |
| } catch (error) { | |
| // Fail to upload | |
| warn("File upload failed"); | |
| console.log(error); | |
| res.status(500).send("File upload failed."); | |
| } | |
| }); | |
| app.get("/file/list", async (req, res) => { | |
| const filePath = req.query.filePath; | |
| const recursive = req.query.recursive == "true"; | |
| const files = [] | |
| const setsDirectory = [] | |
| try{ | |
| const record = await prisma.storage.findMany({ | |
| where: { | |
| username: req.user.username, | |
| } | |
| }) | |
| for (const r of record) { | |
| const path = r.dir ? r.dir.split("/") : []; | |
| // remove first element too | |
| if(path.length > 1) { | |
| path.shift(); | |
| path.pop(); | |
| } else { | |
| path = []; | |
| } | |
| // console.log(path) | |
| let dir = ""; | |
| for (const p of path) { | |
| dir += p; | |
| setsDirectory.push({ | |
| type:"directory", | |
| dir | |
| }); | |
| } | |
| r.type = "file"; | |
| files.push(r) | |
| } | |
| } catch (e) { | |
| info("No file found") | |
| console.log(e) | |
| } | |
| res.send(files.concat(uniq(setsDirectory))); | |
| }) | |
| app.post("/file/delete", async (req, res) => { | |
| const filePath = req.body.filePath; | |
| const full_file_path = req.user.username + (filePath ? "/" + filePath : ""); | |
| const record = await prisma.storage.findUnique({ | |
| where: { | |
| dir: full_file_path, | |
| } | |
| }) | |
| if(!record) { | |
| warn("File not found: " + (full_file_path ? (full_file_path + "/") : "")) | |
| res.status(404).send("File not found."); | |
| return; | |
| } | |
| try { | |
| // Tell the server to unpin the file | |
| const resp = await fetch(record.handle + "/delete", { | |
| method: "POST", | |
| headers: { | |
| "Content-Type": "application/json", | |
| }, | |
| body: JSON.stringify({ | |
| CID: record.CID | |
| }), | |
| }) | |
| if(!resp.ok) { | |
| throw "Respone not ok"; | |
| } | |
| console.log(await resp.json()) | |
| await prisma.storage.delete({ | |
| where: { | |
| CID: record.CID | |
| } | |
| }) | |
| } catch (e) { | |
| console.log(e); | |
| res.sendStatus(500); | |
| return; | |
| } | |
| info("File deleted: " + (filePath ? (filePath + "/") : "")) | |
| res.send("File deleted successfully."); | |
| }) | |
| app.post("/file/getdownloadurl", async (req, res) => { | |
| const filePath = req.body.filePath; | |
| const full_file_path = req.user.username + (filePath ? "/" + filePath : ""); | |
| info("Get download url: " + full_file_path) | |
| const record = await prisma.storage.findUnique({ | |
| where: { | |
| dir: full_file_path | |
| } | |
| }) | |
| if(!record) { | |
| res.status(404).send("File not found") | |
| return; | |
| } | |
| try { | |
| const IPFS_GATEWAY = "https://ipfs.io/ipfs/"; | |
| res.send(IPFS_GATEWAY + record.CID); | |
| } catch(e) { | |
| console.log(e); | |
| res.sendStatus(500) | |
| } | |
| }) | |
| app.listen(7860, () => info("Server started on port 7860")); | |