aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.md7
-rw-r--r--default-config.json136
-rw-r--r--res/admin-register.html89
-rw-r--r--res/client.js2
-rw-r--r--res/css/setup.css48
-rw-r--r--res/img/welcome.webpbin0 -> 53258 bytes
-rw-r--r--res/index.html2
-rw-r--r--res/langs/en.json228
-rw-r--r--res/setup.html52
-rw-r--r--src/Types.hx2
-rw-r--r--src/client/Main.hx2
-rw-r--r--src/server/HttpServer.hx138
12 files changed, 433 insertions, 273 deletions
diff --git a/README.md b/README.md
index 6121853..87cf61b 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,10 @@
+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
Synchronized video viewing with chat and other features.
Lightweight modern implementation and a very easy way to run locally.
diff --git a/default-config.json b/default-config.json
index bdfdb4e..e4bb98d 100644
--- a/default-config.json
+++ b/default-config.json
@@ -1,84 +1,56 @@
{
- "port": 4200,
- "channelName": "SyncTube",
- "maxLoginLength": 20,
- "maxMessageLength": 500,
- "serverChatHistory": 50,
- "totalVideoLimit": 0,
- "userVideoLimit": 0,
- "requestLeaderOnPause": false,
- "unpauseWithoutLeader": false,
- "localAdmins": true,
- "allowProxyIps": true,
- "localNetworkOnly": false,
- "sslKeyPemPath": "",
- "sslCertPemPath": "",
- "templateUrl": "https://youtube.com/playlist?list=PL9FiZUDVMu9tc_85frYognMOVFC_-VkSX",
- "youtubeApiKey": "AIzaSyDTk1OPRI9cDkAK_BKsBcv10DQCHse-QaA",
- "youtubePlaylistLimit": 50,
- "cacheStorageLimitGiB": 3.0,
- "permissions": {
- "banned": [],
- "guest": ["writeChat", "addVideo", "removeVideo", "changeOrder", "toggleItemType", "requestLeader", "rewind"],
- "user": ["guest"],
- "leader": ["user"],
- "admin": ["user", "clearChat", "setLeader", "lockPlaylist", "banClient"]
- },
- "ytDlp": {
- "channel": "stable",
- "jsRuntime": "node"
- },
- "emotes": [
- {"name": ":adorable:", "image": "https://i.imgur.com/5GxNwDY.png"},
- {"name": ":angry:", "image": "https://i.imgur.com/Mx9lhMZ.png"},
- {"name": ":artist:", "image": "https://i.imgur.com/8wQxnse.png"},
- {"name": ":back:", "image": "https://i.imgur.com/hafcTP1.png"},
- {"name": ":beaten:", "image": "https://i.imgur.com/QIvPD83.png"},
- {"name": ":cold:", "image": "https://i.imgur.com/fwgJPJ2.png"},
- {"name": ":confusion:", "image": "https://i.imgur.com/rQHCTPI.png"},
- {"name": ":doh:", "image": "https://i.imgur.com/EaFIosT.png"},
- {"name": ":drink:", "image": "https://i.imgur.com/mSeXSIk.png"},
- {"name": ":eh:", "image": "https://i.imgur.com/lrpyuCX.png"},
- {"name": ":ehehe:", "image": "https://i.imgur.com/UrxypoB.png"},
- {"name": ":fallen:", "image": "https://i.imgur.com/ihYXAlM.png"},
- {"name": ":frustrated:", "image": "https://i.imgur.com/pSY67Ja.gif"},
- {"name": ":goodmood:", "image": "https://i.imgur.com/t5WgyL6.gif"},
- {"name": ":happy:", "image": "https://i.imgur.com/2JNBCk7.png"},
- {"name": ":hmm:", "image": "https://i.imgur.com/u0O7xrc.png"},
- {"name": ":honor:", "image": "https://i.imgur.com/Dkn7JfX.png"},
- {"name": ":hot:", "image": "https://i.imgur.com/9IHCj1a.png"},
- {"name": ":hungry:", "image": "https://i.imgur.com/QoVVKYK.png"},
- {"name": ":idea:", "image": "https://i.imgur.com/l3HoqtG.png"},
- {"name": ":ill:", "image": "https://i.imgur.com/EgY26B0.png"},
- {"name": ":awesome:", "image": "https://i.imgur.com/3COHK8w.png"},
- {"name": ":lovebreak:", "image": "https://i.imgur.com/FcTCnCx.png"},
- {"name": ":loveformoney:", "image": "https://i.imgur.com/Z18e5NZ.png"},
- {"name": ":money:", "image": "https://i.imgur.com/1uM0wky.png"},
- {"name": ":nani:", "image": "https://i.imgur.com/bePPNni.png"},
- {"name": ":ok:", "image": "https://i.imgur.com/gub2TDR.png"},
- {"name": ":ontheway:", "image": "https://i.imgur.com/fWWBeBz.png"},
- {"name": ":romantic:", "image": "https://i.imgur.com/sze1iiu.png"},
- {"name": ":sad:", "image": "https://i.imgur.com/yAjo1kC.png"},
- {"name": ":scared:", "image": "https://i.imgur.com/rsqs0fw.png"},
- {"name": ":sleep:", "image": "https://i.imgur.com/UbR9xv3.png"},
- {"name": ":think:", "image": "https://i.imgur.com/nAoKYYi.png"},
- {"name": ":tired:", "image": "https://i.imgur.com/mKCmaIQ.png"},
- {"name": ":tricky:", "image": "https://i.imgur.com/gC7NlqY.png"},
- {"name": ":writer:", "image": "https://i.imgur.com/rJLs4Ig.png"},
- {"name": ":haxe:", "image": "https://i.imgur.com/F9Tllyv.png"}
- ],
- "filters": [
- {
- "name": "image",
- "regex": "(https?:\\/\\/[^']*\\.)(png|jpg|gif|jpeg|webp)([^' ,]*)",
- "flags": "g",
- "replace": "<a href='$1$2$3' target='_blank' rel='noopener noreferrer'><img src='$1$2$3' class='chat-img'/></a>"
- },
- {
- "name": "url",
- "regex": "(^|[^'])(https?:\\/\\/[^' \t]*)",
- "flags": "g",
- "replace": "$1<a href='$2' target='_blank' rel='noopener noreferrer'>$2</a>"
- }
- ]
+ "port": 4200,
+ "channelName": "Dohee Cinema",
+ "gatePassword": "changeme",
+ "adminToken": "admin",
+ "maxLoginLength": 20,
+ "maxMessageLength": 500,
+ "serverChatHistory": 50,
+ "totalVideoLimit": 0,
+ "userVideoLimit": 0,
+ "requestLeaderOnPause": false,
+ "unpauseWithoutLeader": false,
+ "localAdmins": true,
+ "allowProxyIps": true,
+ "localNetworkOnly": false,
+ "sslKeyPemPath": "",
+ "sslCertPemPath": "",
+ "templateUrl": "YouTube Playlist URL here",
+ "youtubeApiKey": "",
+ "youtubePlaylistLimit": 50,
+ "cacheStorageLimitGiB": 3.0,
+ "permissions": {
+ "banned": [],
+ "guest": [
+ "writeChat",
+ "addVideo",
+ "removeVideo",
+ "changeOrder",
+ "toggleItemType",
+ "requestLeader",
+ "rewind"
+ ],
+ "user": ["guest"],
+ "leader": ["user"],
+ "admin": ["user", "clearChat", "setLeader", "lockPlaylist", "banClient"]
+ },
+ "ytDlp": {
+ "channel": "stable",
+ "jsRuntime": "node"
+ },
+ "emotes": [{ "name": ":haxe:", "image": "https://i.imgur.com/F9Tllyv.png" }],
+ "filters": [
+ {
+ "name": "image",
+ "regex": "(https?:\\/\\/[^']*\\.)(png|jpg|gif|jpeg|webp)([^' ,]*)",
+ "flags": "g",
+ "replace": "<a href='$1$2$3' target='_blank' rel='noopener noreferrer'><img src='$1$2$3' class='chat-img'/></a>"
+ },
+ {
+ "name": "url",
+ "regex": "(^|[^'])(https?:\\/\\/[^' \t]*)",
+ "flags": "g",
+ "replace": "$1<a href='$2' target='_blank' rel='noopener noreferrer'>$2</a>"
+ }
+ ]
}
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/client.js b/res/client.js
index eeea7b3..faf13b4 100644
--- a/res/client.js
+++ b/res/client.js
@@ -1392,7 +1392,7 @@ var client_Main = function() {
if(this.host == "") {
this.host = "localhost";
}
- client_Settings.init({ version : 6, uuid : null, name : "", hash : "", chatSize : 300, synchThreshold : 2, isSwapped : false, isUserListHidden : true, latestLinks : [], latestSubs : [], hotkeysEnabled : true, showHintList : true, checkboxes : [], checkedCache : []},$bind(this,this.settingsPatcher));
+ client_Settings.init({ version : 6, uuid : null, name : "", hash : "", chatSize : 300, synchThreshold : 1, isSwapped : false, isUserListHidden : true, latestLinks : [], latestSubs : [], hotkeysEnabled : true, showHintList : true, checkboxes : [], checkedCache : []},$bind(this,this.settingsPatcher));
this.settings = client_Settings.read();
this.initListeners();
this.onTimeGet = new haxe_Timer(this.settings.synchThreshold * 1000);
diff --git a/res/css/setup.css b/res/css/setup.css
index 9755ad3..b42f8a8 100644
--- a/res/css/setup.css
+++ b/res/css/setup.css
@@ -3,23 +3,42 @@ body {
height: 100vh;
align-items: center;
justify-content: center;
+ background-color: var(--background-video);
+ margin: 0;
}
.setup {
margin: auto;
- padding: 2rem;
- width: 320px;
+ padding: 2.5rem 2rem;
+ width: 360px;
display: flex;
flex-direction: column;
align-items: center;
-
- /* debug */
background-color: #1a1a1f;
+ color: var(--foreground);
+ border: 1px solid var(--border);
+ border-radius: 0.5rem;
+ gap: 1rem;
+}
+
+.welcome-image {
+ max-width: 200px;
+ width: 100%;
border-radius: 0.375rem;
+ margin-bottom: 0.5rem;
+}
- & h1 {
- font-size: 1.75rem;
- }
+.setup-title {
+ font-size: 1.75rem;
+ margin: 0;
+ text-align: center;
+}
+
+.setup p {
+ margin: 0;
+ text-align: center;
+ opacity: 0.7;
+ font-size: 0.95rem;
}
.setup-form {
@@ -27,13 +46,23 @@ body {
flex-direction: column;
gap: 1rem;
width: 100%;
+ margin-top: 0.5rem;
+
+ & input {
+ width: 100%;
+ box-sizing: border-box;
+ }
& button {
margin: 0;
- padding: .75rem .5rem;
+ padding: 0.75rem 0.5rem;
justify-content: center;
background-color: var(--accent);
color: #fff;
+ border: none;
+ border-radius: 0.375rem;
+ cursor: pointer;
+ font-size: 0.95rem;
&:hover {
filter: brightness(1.15);
@@ -46,4 +75,5 @@ body {
flex-direction: column;
gap: 0.5rem;
color: var(--error);
-}
+ font-size: 0.9rem;
+} \ No newline at end of file
diff --git a/res/img/welcome.webp b/res/img/welcome.webp
new file mode 100644
index 0000000..b53f48e
--- /dev/null
+++ b/res/img/welcome.webp
Binary files differ
diff --git a/res/index.html b/res/index.html
index 2f620c6..dbdde80 100644
--- a/res/index.html
+++ b/res/index.html
@@ -145,7 +145,7 @@
</section>
<!-- Footer -->
<footer id="footer">
- <p>Powered by <a href="https://github.com/RblSb/SyncTube" target="_blank" rel="noreferrer noopener">SyncTube</a>
+ <p>Dohee Cinema is powered by <a href="https://github.com/RblSb/SyncTube" target="_blank" rel="noreferrer noopener">SyncTube</a>
</p>
</footer>
</main>
diff --git a/res/langs/en.json b/res/langs/en.json
index a7ce267..02f91ea 100644
--- a/res/langs/en.json
+++ b/res/langs/en.json
@@ -1,119 +1,119 @@
{
- "connection": "Connection",
- "msgConnected": "Connected",
- "msgDisconnected": "Disconnected",
- "joined": "joined",
- "online": "online",
- "nothingPlaying": "Nothing Playing",
- "hintListStart": "Welcome to SyncTube! Here you can:",
- "hintListAddVideo": "$addVideos to watch together",
- "hintListRequestLeader": "$requestLeader to pause and rewind videos for everyone",
- "hintListRequestLeaderMouse": "(also use right mouse button for quick pause)",
- "hintListRequestLeaderTouch": "(also use long tap for quick pause)",
- "hintListOpenInApp": "$openInApp this server for better Android experience",
- "hintListHide": "$hideThisMessage and send <code>/help</code> in chat to see it again",
- "addVideos": "Add Videos",
- "requestLeader": "Request Leader",
- "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.",
- "totalVideoLimitError": "Playlist video limit has been reached.",
- "userVideoLimitError": "Playlist video limit per user has been reached.",
- "videoAlreadyExistsError": "The video already exists in playlist.",
- "addVideoError": "Failed to add video.",
- "adminsCannotBeBannedError": "Admins cannot be banned. Remove them first.",
- "caching": "Caching",
- "downloading": "Downloading",
- "uploading": "Uploading",
- "rawVideo": "Raw video",
- "videos": "videos",
- "addedBy": "Added by",
- "play": "Play",
- "setNext": "Next",
- "makePermanent": "Make Permanent",
- "makeTemporary": "Make Temporary",
- "delete": "Delete",
- "account": "Account",
- "exportSettings": "Export Settings",
- "importSettings": "Import Settings",
- "login": "Login",
- "exit": "Exit",
- "settings": "Settings",
- "synchThreshold": "Synch Threshold",
- "general": "General",
- "hotkeys": "Hotkeys",
- "video": "Video",
- "channel": "Channel",
- "layout": "Layout",
- "swapLayout": "Swap Layout",
- "chatOnly": "Chat Only",
- "setVideoUrl": "Set Video URL",
- "setVideoUrlPrompt": "New video URL:\n(Does not affect other users)",
- "selectLocalVideo": "Select Local Video",
- "removePlayer": "Remove Player",
- "restorePlayer": "Restore Player",
- "toggleUserList": "Show/Hide Userlist",
- "leaderDesc": "Request video control permissions",
- "mobileViewBtn": "Mobile View",
- "leader": "Leader",
- "enterAsGuest": "Enter As Guest:",
- "yourName": "Your Name",
- "enterUserPassword": "Enter User Password",
- "yourPassword": "Your Password",
- "emotes": "Emotes",
- "chat": "Chat",
- "kicked": "Kicked",
- "clearChat": "Clear Chat",
- "chatlinePlaceholder": "Send a message...",
- "leaderDisconnectedServerOnPause": "Leader was disconnected, server on pause.",
- "unpause": "Unpause",
- "addVideoFromUrl": "Add video from URL",
- "embedCustomFrame": "Embed a custom frame",
- "clearPlaylist": "Clear playlist",
- "shufflePlaylist": "Shuffle playlist",
- "playlist": "Playlist",
- "playlistOpen": "Playlist open",
- "playlistLocked": "Playlist locked",
- "expandPlayer": "Expand player",
- "toggleVideoSync": "Toggle video synchronization",
- "toggleSynchConfirm": "Are you sure you want to turn off video sync?",
- "refreshPlayer": "Refresh player",
- "fullscreenPlayer": "Fullscreen player",
- "retrievePlaylistLinks": "Retrieve playlist links",
- "voteForSkip": "Vote for skip",
- "addAsTemporary": "Add as temporary",
- "cacheOnServer": "Cache on server",
- "mediaUrl": "Media URL",
- "optionalTitle": "Title (optional)",
- "subtitlesUrlOptional": "Subtitles URL (optional)",
- "voiceOverAudioTrackUrlOptional": "Voice-over audio URL (optional)",
- "addTemplateUrl": "Add template URL",
- "queueNext": "Queue next",
- "queueLast": "Queue last",
- "and": "and",
- "or": "or",
- "to": "to",
- "pasteEmbedCodeAndClick": "Paste the embed code here",
- "acceptableEmbedCodesAre": "Acceptable embed codes are",
- "customEmbedsCannotBeSynchronized": "CUSTOM EMBEDS CANNOT BE SYNCHRONIZED",
- "save": "Save",
- "skipItemConfirm": "Are you sure you want to skip current video?",
- "clearPlaylistConfirm": "Are you sure you want to clear the playlist?",
- "shufflePlaylistConfirm": "Are you sure you want to shuffle the playlist?",
- "lockPlaylistConfirm": "Are you sure you want to lock the playlist?",
+ "connection": "Connection",
+ "msgConnected": "Connected",
+ "msgDisconnected": "Disconnected",
+ "joined": "joined",
+ "online": "online",
+ "nothingPlaying": "Nothing Playing",
+ "hintListStart": "Welcome to Dohee Cinema! Here you can:",
+ "hintListAddVideo": "$addVideos to watch together (press the + button)",
+ "hintListRequestLeader": "$requestLeader to pause and rewind videos for everyone",
+ "hintListRequestLeaderMouse": "(also use right mouse button for quick pause)",
+ "hintListRequestLeaderTouch": "(also use long tap for quick pause)",
+ "hintListOpenInApp": "$openInApp this server for better Android experience",
+ "hintListHide": "$hideThisMessage and send <code>/help</code> in chat to see it again",
+ "addVideos": "Add Videos",
+ "requestLeader": "Request Leader",
+ "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.",
+ "totalVideoLimitError": "Playlist video limit has been reached.",
+ "userVideoLimitError": "Playlist video limit per user has been reached.",
+ "videoAlreadyExistsError": "The video already exists in playlist.",
+ "addVideoError": "Failed to add video.",
+ "adminsCannotBeBannedError": "Admins cannot be banned. Remove them first.",
+ "caching": "Caching",
+ "downloading": "Downloading",
+ "uploading": "Uploading",
+ "rawVideo": "Raw video",
+ "videos": "videos",
+ "addedBy": "Added by",
+ "play": "Play",
+ "setNext": "Next",
+ "makePermanent": "Make Permanent",
+ "makeTemporary": "Make Temporary",
+ "delete": "Delete",
+ "account": "Account",
+ "exportSettings": "Export Settings",
+ "importSettings": "Import Settings",
+ "login": "Login",
+ "exit": "Logout",
+ "settings": "Settings",
+ "synchThreshold": "Sync Threshold",
+ "general": "General",
+ "hotkeys": "Hotkeys",
+ "video": "Video",
+ "channel": "Channel",
+ "layout": "Layout",
+ "swapLayout": "Swap Layout",
+ "chatOnly": "Chat Only",
+ "setVideoUrl": "Set Video URL",
+ "setVideoUrlPrompt": "New video URL:\n(Does not affect other users)",
+ "selectLocalVideo": "Select Local Video",
+ "removePlayer": "Remove Player",
+ "restorePlayer": "Restore Player",
+ "toggleUserList": "Show/Hide Userlist",
+ "leaderDesc": "Request video control permissions",
+ "mobileViewBtn": "Mobile View",
+ "leader": "Leader",
+ "enterAsGuest": "Login/Register",
+ "yourName": "Username",
+ "enterUserPassword": "Welcome back! Enter Password",
+ "yourPassword": "Your Password",
+ "emotes": "Emotes",
+ "chat": "Chat",
+ "kicked": "Kicked",
+ "clearChat": "Clear Chat",
+ "chatlinePlaceholder": "Send a message...",
+ "leaderDisconnectedServerOnPause": "Leader was disconnected, server on pause.",
+ "unpause": "Unpause",
+ "addVideoFromUrl": "Add video from URL",
+ "embedCustomFrame": "Embed a custom frame",
+ "clearPlaylist": "Clear playlist",
+ "shufflePlaylist": "Shuffle playlist",
+ "playlist": "Playlist",
+ "playlistOpen": "Playlist open",
+ "playlistLocked": "Playlist locked",
+ "expandPlayer": "Expand player",
+ "toggleVideoSync": "Toggle video synchronization",
+ "toggleSynchConfirm": "Are you sure you want to turn off video sync?",
+ "refreshPlayer": "Refresh player",
+ "fullscreenPlayer": "Fullscreen player",
+ "retrievePlaylistLinks": "Retrieve playlist links",
+ "voteForSkip": "Vote for skip",
+ "addAsTemporary": "Add as temporary",
+ "cacheOnServer": "Cache on server",
+ "mediaUrl": "Media URL",
+ "optionalTitle": "Title (optional)",
+ "subtitlesUrlOptional": "Subtitles URL (optional)",
+ "voiceOverAudioTrackUrlOptional": "Voice-over audio URL (optional)",
+ "addTemplateUrl": "Add template URL",
+ "queueNext": "Queue next",
+ "queueLast": "Queue last",
+ "and": "and",
+ "or": "or",
+ "to": "to",
+ "pasteEmbedCodeAndClick": "Paste the embed code here",
+ "acceptableEmbedCodesAre": "Acceptable embed codes are",
+ "customEmbedsCannotBeSynchronized": "CUSTOM EMBEDS CANNOT BE SYNCHRONIZED",
+ "save": "Save",
+ "skipItemConfirm": "Are you sure you want to skip current video?",
+ "clearPlaylistConfirm": "Are you sure you want to clear the playlist?",
+ "shufflePlaylistConfirm": "Are you sure you want to shuffle the playlist?",
+ "lockPlaylistConfirm": "Are you sure you want to lock the playlist?",
- "yes": "Yes",
- "no": "No",
- "on": "On",
- "off": "Off",
+ "yes": "Yes",
+ "no": "No",
+ "on": "On",
+ "off": "Off",
- "areYouSure": "Are you sure?",
- "dataWillBeLost": "The data will be lost.",
+ "areYouSure": "Are you sure?",
+ "dataWillBeLost": "The data will be lost.",
- "setupTitle": "Welcome to SyncTube!"
+ "setupTitle": "Welcome to Dohee Cinema!"
}
diff --git a/res/setup.html b/res/setup.html
index 8068a9d..d7a044e 100644
--- a/res/setup.html
+++ b/res/setup.html
@@ -19,17 +19,16 @@
<body>
<main class="setup">
- <h1 class="setup-title">SyncTube</h1>
- <p>Create your admin account</p>
+ <img class="welcome-image" src="img/welcome.webp" alt="Welcome to Dohee Cinema">
+ <h1 class="setup-title">Welcome to the Dohee Cinema</h1>
+ <p>put in the password for entry</p>
- <form id="setup-form" class="setup-form" action="/setup" method="POST">
- <input type="text" name="name" placeholder="Name">
+ <form id="setup-form" class="setup-form">
<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>
+ <button type="submit">Enter</button>
</form>
</main>
@@ -37,33 +36,34 @@
const formElement = document.getElementById("setup-form");
const errorsElement = document.getElementById("form-errors");
+ function submitPassword(pw) {
+ const payload = { password: pw };
+ return fetch("/gate", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) })
+ .then(res => res.json())
+ .then(response => handleResponse(response, pw))
+ .catch(() => handleResponse(null, pw));
+ }
+
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));
+ const { password } = formElement.elements;
+ submitPassword(password.value);
}, true);
+ const savedPassword = localStorage.getItem("gate_password");
+ if (savedPassword) {
+ submitPassword(savedPassword);
+ }
- function handleResponse(response) {
- if (response.success) {
- return window.location.reload();
+ function handleResponse(response, pw) {
+ if (response && response.success === true) {
+ localStorage.setItem("gate_password", pw);
+ window.location.href = "/";
+ return;
}
- const errors = !response
- ? ["Unknown error"]
- : (response.errors ?? []).map(item => item.error);
-
- showErrors(errorsElement, errors);
+ localStorage.removeItem("gate_password");
+ showErrors(errorsElement, ["Incorrect password. Not cool man"]);
}
function showErrors(container, errors) {
diff --git a/src/Types.hx b/src/Types.hx
index 22b6ec2..1b1997a 100644
--- a/src/Types.hx
+++ b/src/Types.hx
@@ -31,6 +31,8 @@ typedef VideoData = {
typedef ServerConfig = Config & {
serverChatHistory:Int,
+ gatePassword:String,
+ adminToken:String,
localAdmins:Bool,
allowProxyIps:Bool,
localNetworkOnly:Bool,
diff --git a/src/client/Main.hx b/src/client/Main.hx
index 5e05690..e23acbe 100644
--- a/src/client/Main.hx
+++ b/src/client/Main.hx
@@ -85,7 +85,7 @@ class Main {
name: "",
hash: "",
chatSize: 300,
- synchThreshold: 2,
+ synchThreshold: 1,
isSwapped: false,
isUserListHidden: true,
latestLinks: [],
diff --git a/src/server/HttpServer.hx b/src/server/HttpServer.hx
index f7c6d8c..f0f565b 100644
--- a/src/server/HttpServer.hx
+++ b/src/server/HttpServer.hx
@@ -1,6 +1,7 @@
package server;
import Types.UploadResponse;
+import haxe.crypto.Sha256;
import haxe.io.Path;
import js.node.Buffer;
import js.node.Fs.Fs;
@@ -24,10 +25,15 @@ private class HttpServerConfig {
public final cache:Cache = null;
}
-typedef SetupAdminRequest = {
+typedef GateRequest = {
+ password:String,
+}
+
+typedef AdminRegisterRequest = {
name:String,
password:String,
passwordConfirmation:String,
+ token:String,
}
class HttpServer {
@@ -100,8 +106,10 @@ class HttpServer {
}
}
switch url.pathname {
- case "/setup":
- finishSetup(req, res);
+ case "/gate":
+ verifyGate(req, res);
+ case "/admin-register":
+ registerAdmin(req, res);
}
return;
}
@@ -121,8 +129,8 @@ class HttpServer {
return;
}
- if (url.pathname == "/setup") {
- if (main.hasAdmins()) {
+ if (url.pathname == "/gate") {
+ if (!hasGatePassword() || hasValidGateCookie(req)) {
res.redirect("/");
return;
}
@@ -135,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;
@@ -158,8 +183,8 @@ class HttpServer {
}
if (ext == "html") {
- if (!main.isNoState && !main.hasAdmins()) {
- res.redirect("/setup");
+ if (hasGatePassword() && !hasValidGateCookie(req)) {
+ res.redirect("/gate");
return;
}
// replace ${textId} to localized strings
@@ -244,8 +269,8 @@ class HttpServer {
});
}
- function finishSetup(req:IncomingMessage, res:ServerResponse) {
- if (main.hasAdmins()) {
+ function verifyGate(req:IncomingMessage, res:ServerResponse) {
+ if (!hasGatePassword()) {
return res.redirect("/");
}
@@ -257,52 +282,87 @@ class HttpServer {
req.on("end", () -> {
final body = Buffer.concat(bodyChunks).toString();
- final jsonParser = new JsonParser<SetupAdminRequest>();
+ final jsonParser = new JsonParser<GateRequest>();
+ final jsonData = jsonParser.fromJson(body);
+ if (jsonParser.errors.length > 0) {
+ res.status(400).json({success: false});
+ return;
+ }
+ final password = jsonData.password;
+ if (password == main.config.gatePassword) {
+ final token = getGateToken();
+ res.setHeader("set-cookie", 'gate_auth=$token; Path=/; HttpOnly; SameSite=Strict');
+ res.status(200).json({success: true});
+ } else {
+ res.status(401).json({success: false});
+ }
+ });
+ }
+
+ function hasGatePassword():Bool {
+ final gp = main.config.gatePassword;
+ return gp != null && gp.length > 0;
+ }
+
+ function hasValidGateCookie(req:IncomingMessage):Bool {
+ final cookieHeader:String = req.headers["cookie"];
+ if (cookieHeader == null) return false;
+ final token = getGateToken();
+ final needle = 'gate_auth=$token';
+ for (cookie in cookieHeader.split(";")) {
+ if (cookie.trim() == needle) return true;
+ }
+ return false;
+ }
+
+ function getGateToken():String {
+ 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) {
- final errors = ErrorUtils.convertErrorArray(jsonParser.errors);
- trace(errors);
- res.status(400).json({success: false, errors: []});
+ res.status(400).json({success: false, error: "Invalid request"});
return;
}
- final name = jsonData.name;
+ final name = jsonData.name.trim();
final password = jsonData.password;
final passwordConfirmation = jsonData.passwordConfirmation;
- final lang = req.headers["accept-language"] ?? "en";
- final errors:Array<{type:String, error:String}> = [];
+ final token = jsonData.token;
+ if (token != main.config.adminToken) {
+ res.status(401).json({success: false, error: "Invalid admin token"});
+ return;
+ }
if (main.isBadClientName(name)) {
- final error = Lang.get(lang, "usernameError")
- .replace("$MAX", '${main.config.maxLoginLength}');
- errors.push({
- type: "name",
- error: error
- });
+ 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) {
- final error = Lang.get(lang, "passwordError")
- .replace("$MIN", '$min').replace("$MAX", '$max');
- errors.push({
- type: "password",
- error: error
- });
+ res.status(400).json({success: false, error: 'Password must be $min-$max characters'});
+ return;
}
-
if (password != passwordConfirmation) {
- errors.push({
- type: "password",
- error: Lang.get(lang, "passwordsMismatchError")
- });
- }
-
- if (errors.length > 0) {
- res.status(400).json({success: false, errors: errors});
+ res.status(400).json({success: false, error: "Passwords do not match"});
return;
}
-
main.addAdmin(name, password);
res.status(200).json({success: true});
});
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage