Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions build-server.hxml
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
--library hxnodejs
--library hxnodejs-ws
--library json2object:git:https://github.com/RblSb/json2object.git
-D junsafe_compiler_cache
--library json2object:git:https://github.com/RblSb/json2object.git#nightly_safe_macros
# Client libs for completion
--library youtubeIFramePlayer:git:https://github.com/okawa-h/youtubeIFramePlayer-externs.git
--library hls.js-extern:git:https://github.com/zoldesi-andor/hls.js-haxe-extern.git
--library utest
--class-path src
--main server.Main
-D analyzer-optimize
-w -WDeprecatedEnumAbstract
# -w -WDeprecatedEnumAbstract
-w -WDeprecated
--dce full
--js build/server.js
1,778 changes: 949 additions & 829 deletions build/server.js

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion res/css/des.css
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ body.swap {
grid-template-areas: "chat gutter video";
}


@media only screen and (orientation: portrait) {
body {
display: flex;
Expand Down
49 changes: 49 additions & 0 deletions res/css/setup.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
body {
display: flex;
height: 100vh;
align-items: center;
justify-content: center;
}

.setup {
margin: auto;
padding: 2rem;
width: 320px;
display: flex;
flex-direction: column;
align-items: center;

/* debug */
background-color: #1a1a1f;
border-radius: 0.375rem;

& h1 {
font-size: 1.75rem;
}
}

.setup-form {
display: flex;
flex-direction: column;
gap: 1rem;
width: 100%;

& button {
margin: 0;
padding: .75rem .5rem;
justify-content: center;
background-color: var(--accent);
color: #fff;

&:hover {
filter: brightness(1.15);
}
}
}

.form-errors {
display: flex;
flex-direction: column;
gap: 0.5rem;
color: var(--error);
}
6 changes: 5 additions & 1 deletion res/langs/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
"openInApp": "Open in App",
"hideThisMessage": "Hide this message",
"usernameError": "Username length must be from 1 to $MAX characters and don't repeat another's. Characters &^<>'\" are not allowed.",
"passwordError": "Password length must be from $MIN to $MAX characters.",
"passwordsMismatchError": "Passwords do not match.",
"passwordMatchError": "Wrong password.",
"accessError": "Access error",
"noPermission": "No '$PERMISSION' permission.",
Expand Down Expand Up @@ -111,5 +113,7 @@
"off": "Off",

"areYouSure": "Are you sure?",
"dataWillBeLost": "The data will be lost."
"dataWillBeLost": "The data will be lost.",

"setupTitle": "Welcome to SyncTube!"
}
6 changes: 5 additions & 1 deletion res/langs/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
"openInApp": "Открыть в приложении",
"hideThisMessage": "Скрыть это сообщение",
"usernameError": "Ник должен быть от 1 до $MAX символов и не повторять чужие. Символы &^<>'\" запрещены.",
"passwordError": "Длина пароля должна быть от $MIN до $MAX символов.",
"passwordsMismatchError": "Пароли не совпадают.",
"passwordMatchError": "Неправильный пароль.",
"accessError": "Ошибка доступа",
"noPermission": "Нет '$PERMISSION' разрешения.",
Expand Down Expand Up @@ -111,5 +113,7 @@
"off": "Откл.",

"areYouSure": "Вы уверены?",
"dataWillBeLost": "Данные будут потеряны."
"dataWillBeLost": "Данные будут потеряны.",

"setupTitle": "Добро пожаловать в SyncTube!"
}
80 changes: 80 additions & 0 deletions res/setup.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<!DOCTYPE html>
<html>

<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<link rel="manifest" href="manifest.json">
<title>SyncTube</title>
<link rel="icon" href="img/favicon.svg" type="image/svg+xml">
<link id="usertheme" href="css/des.css" rel="stylesheet">
<link id="usertheme" href="css/setup.css" rel="stylesheet">
<link id="customcss" href="css/custom.css" rel="stylesheet">

</head>

<body>
<main class="setup">
<h1 class="setup-title">SyncTube</h1>
<p>Create your admin account</p>

<form id="setup-form" class="setup-form" action="/setup" method="POST">
<input type="text" name="name" placeholder="Name">
<input type="password" name="password" placeholder="Password">
<input type="password" name="confirmation" placeholder="Repeat password">

<div id="form-errors" class="form-errors"></div>

<button type="submit">Create</button>
</form>
</main>

<script>
const formElement = document.getElementById("setup-form");
const errorsElement = document.getElementById("form-errors");

formElement.addEventListener("submit", function (e) {
e.preventDefault();

const { name, password, confirmation } = formElement.elements;
const payload = {
name: name.value,
password: password.value,
passwordConfirmation: confirmation.value,
}

fetch("/setup", { method: "POST", body: JSON.stringify(payload) })
.then(res => res.json())
.then(response => handleResponse(response))
.catch(() => handleResponse(null));
}, true);


function handleResponse(response) {
if (response.success) {
return window.location.reload();
}

const errors = !response
? ["Unknown error"]
: (response.errors ?? []).map(item => item.error);

showErrors(errorsElement, errors);
}

function showErrors(container, errors) {
container.innerHTML = "";

errors.forEach((message) => {
const errorEl = document.createElement("div");
errorEl.innerText = message;
container.appendChild(errorEl);
});
}
</script>
</body>

</html>
103 changes: 95 additions & 8 deletions src/server/HttpServer.hx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package server;

import Types.UploadResponse;
import haxe.Json;
import haxe.io.Path;
import js.node.Buffer;
import js.node.Fs.Fs;
Expand All @@ -12,6 +11,8 @@ import js.node.http.ClientRequest;
import js.node.http.IncomingMessage;
import js.node.http.ServerResponse;
import js.node.url.URL;
import json2object.ErrorUtils;
import json2object.JsonParser;
import server.cache.Cache;
import sys.FileSystem;

Expand All @@ -23,6 +24,12 @@ private class HttpServerConfig {
public final cache:Cache = null;
}

typedef SetupAdminRequest = {
name:String,
password:String,
passwordConfirmation:String,
}

class HttpServer {
static final mimeTypes = [
"html" => "text/html",
Expand Down Expand Up @@ -87,6 +94,8 @@ class HttpServer {
uploadFileLastChunk(req, res);
case "/upload":
uploadFile(req, res);
case "/setup":
finishSetup(req, res);
}
return;
}
Expand All @@ -106,6 +115,20 @@ class HttpServer {
return;
}

if (url.pathname == "/setup") {
if (main.hasAdmins()) {
res.redirect("/");
return;
}

Fs.readFile('$dir/setup.html', (err:Dynamic, data:Buffer) -> {
data = Buffer.from(localizeHtml(data.toString(), req.headers["accept-language"]));
res.setHeader("content-type", getMimeType("html"));
res.end(data);
});
return;
}

if (url.pathname == "/proxy") {
if (!proxyUrl(req, res)) res.end('Proxy error: ${req.url}');
return;
Expand All @@ -127,7 +150,12 @@ class HttpServer {
readFileError(err, res, filePath);
return;
}

if (ext == "html") {
if (!main.isNoState && !main.hasAdmins()) {
res.redirect("/setup");
return;
}
// replace ${textId} to localized strings
data = cast localizeHtml(data.toString(), req.headers["accept-language"]);
}
Expand All @@ -145,14 +173,11 @@ class HttpServer {
req.on("end", () -> {
final buffer = Buffer.concat(body);
uploadingFilesLastChunks[filePath] = buffer;
res.writeHead(200, {
"content-type": getMimeType("json"),
});
final json:UploadResponse = {
info: "File last chunk uploaded",
url: cache.getFileUrl(name)
}
res.end(Json.stringify(json));
res.status(200).json(json);
});
}

Expand All @@ -164,9 +189,7 @@ class HttpServer {
final size = Std.parseInt(req.headers["content-length"]) ?? return;

inline function end(code:Int, json:UploadResponse):Void {
res.statusCode = code;
res.end(Json.stringify(json));

res.status(code).json(json);
uploadingFilesSizes.remove(filePath);
uploadingFilesLastChunks.remove(filePath);
}
Expand Down Expand Up @@ -215,6 +238,70 @@ class HttpServer {
});
}

function finishSetup(req:IncomingMessage, res:ServerResponse) {
if (main.hasAdmins()) {
return res.redirect("/");
}

final bodyChunks:Array<Buffer> = [];

req.on("data", chunk -> {
bodyChunks.push(chunk);
});

req.on("end", () -> {
final body = Buffer.concat(bodyChunks).toString();
final jsonParser = new JsonParser<SetupAdminRequest>();
final jsonData = jsonParser.fromJson(body);
if (jsonParser.errors.length > 0) {
final errors = ErrorUtils.convertErrorArray(jsonParser.errors);
trace(errors);
res.status(400).json({success: false, errors: []});
return;
}
final name = jsonData.name;
final password = jsonData.password;
final passwordConfirmation = jsonData.passwordConfirmation;
final lang = req.headers["accept-language"] ?? "en";
final errors:Array<{type:String, error:String}> = [];

if (main.isBadClientName(name)) {
final error = Lang.get(lang, "usernameError")
.replace("$MAX", '${main.config.maxLoginLength}');
errors.push({
type: "name",
error: error
});
}

final min = Main.MIN_PASSWORD_LENGTH;
final max = Main.MAX_PASSWORD_LENGTH;
if (password.length < min || password.length > max) {
final error = Lang.get(lang, "passwordError")
.replace("$MIN", '$min').replace("$MAX", '$max');
errors.push({
type: "password",
error: error
});
}

if (password != passwordConfirmation) {
errors.push({
type: "password",
error: Lang.get(lang, "passwordsMismatchError")
});
}

if (errors.length > 0) {
res.status(400).json({success: false, errors: errors});
return;
}

main.addAdmin(name, password);
res.status(200).json({success: true});
});
}

function getPath(dir:String, url:URL):String {
final filePath = dir.urlDecode() + decodeURIComponent(url.pathname);
if (!FileSystem.isDirectory(filePath)) return filePath;
Expand Down
8 changes: 7 additions & 1 deletion src/server/Main.hx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ private typedef MainOptions = {
}

class Main {
public static inline var MIN_PASSWORD_LENGTH = 4;
public static inline var MAX_PASSWORD_LENGTH = 50;
static inline var VIDEO_START_MAX_DELAY = 3000;
static inline var VIDEO_SKIP_DELAY = 1000;
static inline var FLASHBACKS_COUNT = 50;
Expand All @@ -42,8 +44,8 @@ class Main {
public final userDir:String;
public final logsDir:String;
public final config:Config;
public final isNoState:Bool;

final isNoState:Bool;
final verbose:Bool;
final statePath:String;
var wss:WSServer;
Expand Down Expand Up @@ -370,6 +372,10 @@ class Main {
trace('Admin $name removed.');
}

public function hasAdmins():Bool {
return userList.admins.length > 0;
}

public function replayLog(events:Array<ServerEvent>):Void {
final timer = new Timer(1000);
timer.run = () -> {
Expand Down
3 changes: 3 additions & 0 deletions src/server/import.hx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package server;

using tools.HttpServerTools;
Loading