This commit is contained in:
Simon 2024-01-19 00:45:23 +01:00
commit 79df1df06c
8 changed files with 1474 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
node_modules/
videos/*

143
index.js Normal file
View File

@ -0,0 +1,143 @@
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";
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, 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 });
});
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/");
})

1275
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

19
package.json Normal file
View File

@ -0,0 +1,19 @@
{
"name": "maotube",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "node index.js"
},
"keywords": [],
"author": "corruptocrat",
"license": "ISC",
"type": "module",
"dependencies": {
"bcrypt": "^5.1.1",
"cors": "^2.8.5",
"express": "^4.18.2",
"express-fileupload": "^1.4.3"
}
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

18
public/index.html Normal file
View File

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>MaoTube</title>
<link rel="stylesheet" href="style.css">
<script src="script.js" defer></script>
</head>
<body>
<h1>MaoTube</h1>
<form method="GET" target="/search">
<input type="text" name="query" placeholder="...search">
<input type="submit">
</form>
<img src="https://totallyhistory.com/wp-content/uploads/2013/04/Mao.jpg" alt="The chairman">
</body>
</html>

3
public/script.js Normal file
View File

@ -0,0 +1,3 @@

13
public/style.css Normal file
View File

@ -0,0 +1,13 @@
*, *::before, *::after {
box-sizing: border-box;
}
:root {
color-scheme: light dark;
}
body {
margin: 0 auto;
padding: 2rem;
}