import express from "express"; import cors from "cors"; import bcrypt from "bcrypt"; import fileUpload from "express-fileupload"; import path from "path"; import childProcess from "child_process"; import levenshtein from "js-levenshtein"; const users = []; let sessions = []; const videos = []; function randomString(length) { const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789"; let result = ""; for (let i = 0; i < length; ++i) { result += chars[chars.length * Math.random()]; } return result; } const app = express(); app.use(cors()); app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use("/", express.static("public/")); app.use("/videos", express.static("videos/")); app.post("/api/register", async (req, res) => { const { username, password } = req.body; if (typeof username !== "string" || typeof password !== "string") { return res.status(400).json({ ok: false, error: "bad request" }); } const existingUser = users.find(user => user.username === username); if (existingUser !== undefined) { return res.status(400).json({ ok: false, error: "username taken" }); } const passwordHash = await bcrypt.hash(password, 10); const id = users.length; const user = { id, username, password: passwordHash }; users.push(user); return res.status(200).json({ ok: true, user }); }); app.post("/api/login", async (req, res) => { const { username, password } = req.body; if (typeof username !== "string" || typeof password !== "string") { return res.status(400).json({ ok: false, error: "bad request" }); } const user = users.find(user => user.username === username); if (user === undefined) { return res.status(400).json({ ok: false, error: "wrong username/password" }); } if (!await bcrypt.compare(password, user.password)) { return res.status(400).json({ ok: false, error: "wrong username/password" }); } sessions = sessions.filter(session => session.userId !== user.id); const token = randomString(64); const session = { userId: user.id, token }; sessions.push(session); res.clearCookie("token"); res.cookie("token", token); return res.status(200).json({ ok: true, session }); }); app.get("/api/search", async (req, res) => { const page = +req.query.page || 0; const search = req.query.query; if (!search) { return res.status(400).json({ ok: false, error: "bad request" }); } const [start, end] = [20 * page, 20 * (page + 1)]; const returnedVideos = videos .map(video => ({dist: levenshtein(search, video.title), ...video})) .toSorted((a, b) => a.dist - b.dist) .slice(start, end) .map(video => { const user = users.find(user => user.id === v.userId); if (!user) { return {...video, author: "[Liberal]"}; } return {...video, author: user.username}; }); return res.status(200).json({ ok: true, videos: returnedVideos, total: videos.length }); }); function authorized() { return (req, res, next) => { const token = (() => { if ("token" in req.cookies) { return req.cookies["token"]; } else if ("token" in req.query) { return req.query["token"]; } else if (req.method === "post" && "token" in req.body) { return req.body["token"]; } else { return null; } })(); if (token === null) { return res.status(400).json({ ok: false, error: "unathorized" }); } const session = sessions.find(session => session.token === token); if (session === undefined) { return res.status(400).json({ ok: false, error: "unathorized" }); } const user = user.find(user => user.id === session.id); if (user === undefined) { throw new Error("error: session with invalid userId"); } req.user = user; next(); } } app.get("/api/logout", authorized(), (req, res) => { sessions = sessions.filter(session => session.userId !== req.user.id); return res.status(200).json({ ok: true }); }); app.post("/api/upload_video", authorized(), fileUpload({ limits: { fileSize: 2 ** 26 }, useTempFiles: true }), async (req, res) => { const { title } = req.body; if (req.files === undefined || req.files === null || req.files.length !== 1) { return res.status(400).json({ ok: false, error: "bad request" }); } if (req.files[0].mimetype !== "video/mp4") { return res.status(400).json({ ok: false, error: "bad mimetype" }); } const userId = req.user.id; const id = randomString(4); const tempPath = req.files[0].tempFilePath; const newPath = path.join("/videos", id, ".mp4"); const exitCode = await new Promise((resolve, _reject) => { const process = childProcess.spawn("HandBrakeCLI", ["-i", tempPath, "-o", newPath, "-Z", "Social 25 MB 5 Minutes 360p60"]); process.stderr.on("data", (data) => { conole.error(data); }); process.on("close", (code) => { resolve(code); }) }); if (exitCode !== 0) { throw new Error("handbrake failed"); } const video = { id, userId, title, path: newPath, }; videos.push(video); return res.status(200).json({ ok: true, video }); }) app.use((err, req, res, next) => { console.error(err); res.status(500).json({ ok: false, error: "server error" }) }); app.listen(8000, () => { console.log("app at http://localhost:8000/"); })