usable application #1
27
index.js
27
index.js
@ -4,6 +4,7 @@ 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 = [];
|
||||
@ -32,12 +33,12 @@ app.post("/api/register", async (req, res) => {
|
||||
return res.status(400).json({ ok: false, error: "bad request" });
|
||||
}
|
||||
const existingUser = users.find(user => user.username === username);
|
||||
if (existingUser === undefined) {
|
||||
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 };
|
||||
const user = { id, username, password: passwordHash };
|
||||
users.push(user);
|
||||
return res.status(200).json({ ok: true, user });
|
||||
});
|
||||
@ -63,6 +64,28 @@ app.post("/api/login", async (req, res) => {
|
||||
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 = (() => {
|
||||
|
11
package-lock.json
generated
11
package-lock.json
generated
@ -12,7 +12,8 @@
|
||||
"bcrypt": "^5.1.1",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.18.2",
|
||||
"express-fileupload": "^1.4.3"
|
||||
"express-fileupload": "^1.4.3",
|
||||
"js-levenshtein": "^1.1.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/node-pre-gyp": {
|
||||
@ -668,6 +669,14 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/js-levenshtein": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz",
|
||||
"integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
|
@ -14,6 +14,7 @@
|
||||
"bcrypt": "^5.1.1",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.18.2",
|
||||
"express-fileupload": "^1.4.3"
|
||||
"express-fileupload": "^1.4.3",
|
||||
"js-levenshtein": "^1.1.6"
|
||||
}
|
||||
}
|
||||
|
@ -4,11 +4,10 @@
|
||||
<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">
|
||||
<form method="GET" target="_self" action="/search">
|
||||
<input type="text" name="query" placeholder="...">
|
||||
<input type="submit" value="Search">
|
||||
</form>
|
||||
|
35
public/login/index.html
Normal file
35
public/login/index.html
Normal file
@ -0,0 +1,35 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>MaoTube</title>
|
||||
<link rel="stylesheet" href="/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<h1>MaoTube</h1>
|
||||
<div>
|
||||
<label for="username"><p>Username</p></label>
|
||||
<input type="text" name="username" id="username">
|
||||
<label for="password"><p>Password</p></label>
|
||||
<input type="text" name="password" id="password">
|
||||
<br>
|
||||
<br>
|
||||
<input type="submit" id="submit" value="Login">
|
||||
</div>
|
||||
<br>
|
||||
<div id="result">
|
||||
<noscript>
|
||||
<div class="mao-error">
|
||||
<p>javascript not enabled</p>
|
||||
<p>bottom text</p>
|
||||
</div>
|
||||
</noscript>
|
||||
<div id="mao-error" class="mao-error hidden">
|
||||
<p id="mao-error-message"></p>
|
||||
<p>bottom text</p>
|
||||
</div>
|
||||
</div>
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
46
public/login/script.js
Normal file
46
public/login/script.js
Normal file
@ -0,0 +1,46 @@
|
||||
function error(message) {
|
||||
const errorContainer = document.getElementById("mao-error");
|
||||
const errorElement = document.getElementById("mao-error-message");
|
||||
|
||||
errorElement.innerText = message;
|
||||
errorContainer.classList.remove("hidden");
|
||||
}
|
||||
|
||||
function displayResponse(response) {
|
||||
if (!response.ok) {
|
||||
error(response.error);
|
||||
return;
|
||||
}
|
||||
window.location.href = "/";
|
||||
}
|
||||
|
||||
function click() {
|
||||
const username = document.getElementById("username").value;
|
||||
const password = document.getElementById("password").value;
|
||||
|
||||
if (username.trim().length === 0) {
|
||||
error("username cannot be empty");
|
||||
return;
|
||||
} else if (password.trim().length === 0) {
|
||||
error("password cannot be empty");
|
||||
return;
|
||||
}
|
||||
const method = "POST";
|
||||
const body = JSON.stringify({ username, password });
|
||||
const headers = new Headers({ "Content-Type": "application/json" });
|
||||
return fetch("/api/login", {
|
||||
method,
|
||||
body,
|
||||
headers,
|
||||
})
|
||||
.then(v => v.json())
|
||||
.then(displayResponse);
|
||||
|
||||
}
|
||||
|
||||
function main() {
|
||||
const submit = document.getElementById("submit");
|
||||
submit.addEventListener("click", () => click())
|
||||
}
|
||||
|
||||
main();
|
35
public/register/index.html
Normal file
35
public/register/index.html
Normal file
@ -0,0 +1,35 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>MaoTube</title>
|
||||
<link rel="stylesheet" href="/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<h1>MaoTube</h1>
|
||||
<div>
|
||||
<label for="username"><p>Username</p></label>
|
||||
<input type="text" name="username" id="username">
|
||||
<label for="password"><p>Password</p></label>
|
||||
<input type="text" name="password" id="password">
|
||||
<br>
|
||||
<br>
|
||||
<input type="submit" id="submit" value="Register">
|
||||
</div>
|
||||
<br>
|
||||
<div id="result">
|
||||
<noscript>
|
||||
<div class="mao-error">
|
||||
<p>javascript not enabled</p>
|
||||
<p>bottom text</p>
|
||||
</div>
|
||||
</noscript>
|
||||
<div id="mao-error" class="mao-error hidden">
|
||||
<p id="mao-error-message"></p>
|
||||
<p>bottom text</p>
|
||||
</div>
|
||||
</div>
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
46
public/register/script.js
Normal file
46
public/register/script.js
Normal file
@ -0,0 +1,46 @@
|
||||
function error(message) {
|
||||
const errorContainer = document.getElementById("mao-error");
|
||||
const errorElement = document.getElementById("mao-error-message");
|
||||
|
||||
errorElement.innerText = message;
|
||||
errorContainer.classList.remove("hidden");
|
||||
}
|
||||
|
||||
function displayResponse(response) {
|
||||
if (!response.ok) {
|
||||
error(response.error);
|
||||
return;
|
||||
}
|
||||
window.location.href = "/login";
|
||||
}
|
||||
|
||||
function click() {
|
||||
const username = document.getElementById("username").value;
|
||||
const password = document.getElementById("password").value;
|
||||
|
||||
if (username.trim().length === 0) {
|
||||
error("username cannot be empty");
|
||||
return;
|
||||
} else if (password.trim().length === 0) {
|
||||
error("password cannot be empty");
|
||||
return;
|
||||
}
|
||||
const method = "POST";
|
||||
const body = JSON.stringify({ username, password });
|
||||
const headers = new Headers({ "Content-Type": "application/json" });
|
||||
return fetch("/api/register", {
|
||||
method,
|
||||
body,
|
||||
headers,
|
||||
})
|
||||
.then(v => v.json())
|
||||
.then(displayResponse);
|
||||
|
||||
}
|
||||
|
||||
function main() {
|
||||
const submit = document.getElementById("submit");
|
||||
submit.addEventListener("click", () => click())
|
||||
}
|
||||
|
||||
main();
|
@ -1,3 +0,0 @@
|
||||
|
||||
|
||||
|
30
public/search/index.html
Normal file
30
public/search/index.html
Normal file
@ -0,0 +1,30 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>MaoTube</title>
|
||||
<link rel="stylesheet" href="/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<h1>MaoTube</h1>
|
||||
<form method="GET" action="/search" target="_self">
|
||||
<input type="text" name="query" placeholder="...">
|
||||
<input type="submit" value="Search">
|
||||
</form>
|
||||
<br>
|
||||
<div id="result">
|
||||
<noscript>
|
||||
<div class="mao-error">
|
||||
<p>javascript not enabled</p>
|
||||
<p>bottom text</p>
|
||||
</div>
|
||||
</noscript>
|
||||
<div id="mao-error" class="mao-error hidden">
|
||||
<p id="mao-error-message"></p>
|
||||
<p>bottom text</p>
|
||||
</div>
|
||||
</div>
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
38
public/search/script.js
Normal file
38
public/search/script.js
Normal file
@ -0,0 +1,38 @@
|
||||
|
||||
function error(message) {
|
||||
const errorContainer = document.getElementById("mao-error");
|
||||
const errorElement = document.getElementById("mao-error-message");
|
||||
|
||||
errorElement.innerText = message;
|
||||
errorContainer.classList.remove("hidden");
|
||||
}
|
||||
|
||||
function displayResponse(response) {
|
||||
if (!response.ok) {
|
||||
error(response.error);
|
||||
return;
|
||||
}
|
||||
const { videos, total } = response;
|
||||
if (videos.length === 0) {
|
||||
error("search returned no results");
|
||||
return;
|
||||
}
|
||||
const resultElement = document.getElementById("result");
|
||||
resultElement.innerHTML = `<p>Showing ${videos.length}/${total} results.</p>`
|
||||
+ "<ul id='video-list'>"
|
||||
+ videos
|
||||
.map(v => {
|
||||
return `<li><p><a href="${v.path}">${v.title}</a> - ${v.author}</p></li>`
|
||||
})
|
||||
.join("")
|
||||
+ "</ul>";
|
||||
}
|
||||
|
||||
function main() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
return fetch(`/api/search?${params.toString()}`)
|
||||
.then(v => v.json())
|
||||
.then(displayResponse);
|
||||
}
|
||||
|
||||
main();
|
@ -12,3 +12,44 @@ body {
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mao-error {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: url("/chairman_1.jpg");
|
||||
background-size: auto 600px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
height: 600px;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mao-error p {
|
||||
color: #fff;
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
font-family: "Impact", "Bebas", "League Gothic", "Oswald", "Coluna", "Ubuntu Condensed", system-ui, sans-serif;
|
||||
text-shadow:
|
||||
3px 3px 0 black,
|
||||
-3px -3px 0 black,
|
||||
3px -3px 0 black,
|
||||
-3px 3px 0 black,
|
||||
|
||||
-3px 0px 0 black,
|
||||
3px 0px 0 black,
|
||||
0px 3px 0 black,
|
||||
0px -3px 0 black;
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
.mao-error p:last-child {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
ul#video-list {
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user