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"; import cookieParser from "cookie-parser"; import { fileURLToPath } from 'url'; const users = []; let sessions = []; const videos = []; function randomString(length) { const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789"; let result = ""; for (let i = 0; i < length; ++i) { result += chars[Math.floor(chars.length * Math.random())]; } return result; } function dirname() { return path.dirname(fileURLToPath(import.meta.url)); } const app = express(); app.use(cors()); app.use(express.json()); app.use(cookieParser()); 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 === video.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 (req.cookies && "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: "unauthorized" }); } const session = sessions.find(session => session.token === token); if (session === undefined) { return res.status(400).json({ ok: false, error: "unauthorized" }); } const user = users.find(user => user.id === session.userId); 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 || !req.files.video) { return res.status(400).json({ ok: false, error: "bad request" }); } if (req.files.video.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.video.tempFilePath; const newPath = path.join(dirname(), "videos", `${id}.mp4`); console.log(newPath); const exitCode = await new Promise((resolve, _reject) => { const process = childProcess.spawn("HandBrakeCLI", ["-i", tempPath, "-o", newPath, "-Z", "Social 50 MB 10 Minutes 480p30"]); process.stderr.on("data", (data) => { console.error(data.toString()); }); process.on("close", (code) => { resolve(code); }) }) if (exitCode !== 0) { console.log(":/"); // throw new Error("handbrake failed"); return res.status(500).json({ ok: false, error: "server error" }); } 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/"); })