init
This commit is contained in:
commit
79df1df06c
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
node_modules/
|
||||
videos/*
|
||||
|
143
index.js
Normal file
143
index.js
Normal 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
1275
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
19
package.json
Normal file
19
package.json
Normal 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
BIN
public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 200 KiB |
18
public/index.html
Normal file
18
public/index.html
Normal 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
3
public/script.js
Normal file
@ -0,0 +1,3 @@
|
||||
|
||||
|
||||
|
13
public/style.css
Normal file
13
public/style.css
Normal file
@ -0,0 +1,13 @@
|
||||
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
Loading…
Reference in New Issue
Block a user