diff options
| -rw-r--r-- | README.md | 5 | ||||
| -rw-r--r-- | default-config.json | 1 | ||||
| -rw-r--r-- | res/admin-register.html | 89 | ||||
| -rw-r--r-- | res/langs/en.json | 10 | ||||
| -rw-r--r-- | src/Types.hx | 1 | ||||
| -rw-r--r-- | src/server/HttpServer.hx | 75 |
6 files changed, 176 insertions, 5 deletions
@@ -1,3 +1,8 @@ +Fork Key Differences
+- `gatePassword` config option, allowing only people who know the password to enter
+- `adminToken` allows for registering admin on the frontend using a known token
+- `/admin-register` page that is visible only if `adminToken` is set
+
This fork adds basic password based authentication to prevent unwanted users from joining and strips most existing emotes.
# <img src="./res/img/favicon.svg" width="40" height="40" align="top"> SyncTube
diff --git a/default-config.json b/default-config.json index afe50c2..e4bb98d 100644 --- a/default-config.json +++ b/default-config.json @@ -2,6 +2,7 @@ "port": 4200, "channelName": "Dohee Cinema", "gatePassword": "changeme", + "adminToken": "admin", "maxLoginLength": 20, "maxMessageLength": 500, "serverChatHistory": 50, diff --git a/res/admin-register.html b/res/admin-register.html new file mode 100644 index 0000000..4515cfd --- /dev/null +++ b/res/admin-register.html @@ -0,0 +1,89 @@ +<!-- localization-template --> +<!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">Admin Registration</h1> + <p>Enter the admin token to register your account</p> + + <form id="admin-form" class="setup-form"> + <input type="text" name="name" placeholder="Username"> + <input type="password" name="password" placeholder="Password"> + <input type="password" name="confirmation" placeholder="Repeat password"> + <input type="password" name="token" placeholder="Admin Token"> + + <div id="form-errors" class="form-errors"></div> + + <button type="submit">Register</button> + </form> + </main> + + <script> + const formElement = document.getElementById("admin-form"); + const errorsElement = document.getElementById("form-errors"); + + formElement.addEventListener("submit", function (e) { + e.preventDefault(); + const { name, password, confirmation, token } = formElement.elements; + + const payload = { + name: name.value, + password: password.value, + passwordConfirmation: confirmation.value, + token: token.value + }; + + fetch("/admin-register", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload) + }) + .then(res => res.json()) + .then(response => handleResponse(response)) + .catch(() => handleResponse(null)); + }, true); + + function handleResponse(response) { + if (response && response.success === true) { + errorsElement.innerHTML = ""; + const successEl = document.createElement("div"); + successEl.innerText = "Admin account created! You can now log in."; + successEl.style.color = "green"; + errorsElement.appendChild(successEl); + + Array.from(formElement.elements).forEach(el => { el.disabled = true; }); + return; + } + + const message = (response && response.error) ? response.error : "Registration failed"; + showErrors(errorsElement, [message]); + } + + function showErrors(container, errors) { + container.innerHTML = ""; + + errors.forEach(function (message) { + const errorEl = document.createElement("div"); + errorEl.innerText = message; + container.appendChild(errorEl); + }); + } + </script> +</body> + +</html>
\ No newline at end of file diff --git a/res/langs/en.json b/res/langs/en.json index 91d7135..02f91ea 100644 --- a/res/langs/en.json +++ b/res/langs/en.json @@ -42,9 +42,9 @@ "exportSettings": "Export Settings", "importSettings": "Import Settings", "login": "Login", - "exit": "Exit", + "exit": "Logout", "settings": "Settings", - "synchThreshold": "Synch Threshold", + "synchThreshold": "Sync Threshold", "general": "General", "hotkeys": "Hotkeys", "video": "Video", @@ -61,9 +61,9 @@ "leaderDesc": "Request video control permissions", "mobileViewBtn": "Mobile View", "leader": "Leader", - "enterAsGuest": "Enter As Guest:", - "yourName": "Your Name", - "enterUserPassword": "Enter User Password", + "enterAsGuest": "Login/Register", + "yourName": "Username", + "enterUserPassword": "Welcome back! Enter Password", "yourPassword": "Your Password", "emotes": "Emotes", "chat": "Chat", diff --git a/src/Types.hx b/src/Types.hx index 6fbe55b..1b1997a 100644 --- a/src/Types.hx +++ b/src/Types.hx @@ -32,6 +32,7 @@ typedef VideoData = { typedef ServerConfig = Config & { serverChatHistory:Int, gatePassword:String, + adminToken:String, localAdmins:Bool, allowProxyIps:Bool, localNetworkOnly:Bool, diff --git a/src/server/HttpServer.hx b/src/server/HttpServer.hx index 5917ee0..f0f565b 100644 --- a/src/server/HttpServer.hx +++ b/src/server/HttpServer.hx @@ -29,6 +29,13 @@ typedef GateRequest = { password:String, } +typedef AdminRegisterRequest = { + name:String, + password:String, + passwordConfirmation:String, + token:String, +} + class HttpServer { static final mimeTypes = [ "html" => "text/html", @@ -101,6 +108,8 @@ class HttpServer { switch url.pathname { case "/gate": verifyGate(req, res); + case "/admin-register": + registerAdmin(req, res); } return; } @@ -134,6 +143,23 @@ class HttpServer { return; } + if (url.pathname == "/admin-register") { + if (!hasAdminToken()) { + res.redirect("/"); + return; + } + Fs.readFile('$dir/admin-register.html', (err:Dynamic, data:Buffer) -> { + if (err != null) { + readFileError(err, res, '$dir/admin-register.html'); + return; + } + 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; @@ -293,6 +319,55 @@ class HttpServer { return Sha256.encode('gate_${main.config.gatePassword}_${main.config.salt}'); } + function hasAdminToken():Bool { + final t = main.config.adminToken; + return t != null && t.length > 0; + } + + function registerAdmin(req:IncomingMessage, res:ServerResponse) { + if (!hasAdminToken()) { + res.status(403).json({success: false, error: "Admin registration is disabled"}); + return; + } + + final bodyChunks:Array<Buffer> = []; + req.on("data", chunk -> bodyChunks.push(chunk)); + req.on("end", () -> { + final body = Buffer.concat(bodyChunks).toString(); + final jsonParser = new JsonParser<AdminRegisterRequest>(); + final jsonData = jsonParser.fromJson(body); + if (jsonParser.errors.length > 0) { + res.status(400).json({success: false, error: "Invalid request"}); + return; + } + final name = jsonData.name.trim(); + final password = jsonData.password; + final passwordConfirmation = jsonData.passwordConfirmation; + final token = jsonData.token; + + if (token != main.config.adminToken) { + res.status(401).json({success: false, error: "Invalid admin token"}); + return; + } + if (main.isBadClientName(name)) { + res.status(400).json({success: false, error: "Invalid username"}); + return; + } + final min = Main.MIN_PASSWORD_LENGTH; + final max = Main.MAX_PASSWORD_LENGTH; + if (password.length < min || password.length > max) { + res.status(400).json({success: false, error: 'Password must be $min-$max characters'}); + return; + } + if (password != passwordConfirmation) { + res.status(400).json({success: false, error: "Passwords do not match"}); + 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; |
