UNPKG

save-server

Version:

A powerful ShareX image and URL server, with support for multiple users.

734 lines (629 loc) 20.6 kB
// From https://fontawesome.com/icons?d=gallery&q=file&s=solid&m=free const iconMappings = { doc: "fa-file-word", docx: "fa-file-word", md: "fa-file-alt", txt: "fa-file-alt", exe: "fa-asterisk", zip: "fa-archive", "": "fa-file", js: "fa-file-code" }; const extensionMappings = { pdf: "Document", js: "Javascript", py: "Python", md: "Markdown document", docx: "MS Word Document", ts: "Typescript", exe: "Executable program", db: "Database", msi: "Program installer", txt: "Text document", ods: "Document", zip: "File archive", mp3: "MP3 sound", mp4: "Video" }; // source; https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Img const imgExtensions = ["jpeg", "jpg", "gif", "png", "exif", "bmp", "webp", "svg", "apng", "cur", "pjpeg", "pjp", "jfif"]; window.addEventListener("DOMContentLoaded", function () { const Persist = window.Persist; const showError = window.showError; const Api = window.Api; const BaseUrl = window.BaseUrl; const parseDate = window.parseDate; const deleteFile = window.deleteFile; const showMessage = window.showMessage; // Sidebar const helloBox = document.getElementById("hello"); helloBox.innerText = "Hey, " + window.user.username + "!"; const getTarget = () => Persist.get("targetUser"); const getTab = () => Persist.get("tab"); const getFilter = () => Persist.get("filter"); const getPage = () => Persist.get("page"); const setTarget = (value) => Persist.set("targetUser", value); const setTab = (value) => Persist.set("tab", value); const setFilter = (value) => Persist.set("filter", value); const setPage = (value) => Persist.set("page", value); // Persistent values - initial if (!Persist.get("targetUser")) { setTarget(window.user.username); setTab(0); // True = Image only. setFilter(true); setPage(0); } // Not persistent let modalTarget; const tabs = document.getElementById("main-tabs"); function bindControl() { const controllers = tabs.children; if (!controllers) { return showError("Failed to bind control"); } // Set initial showContent(getTab()); setVisibleTab(getTab()); for (let i = 0; i < controllers.length; i++) { controllers[i].onclick = function () { setVisibleTab(i); setTab(i); showContent(i); }; } function setVisibleTab(tabNo) { for (let c = 0; c < controllers.length; c++) { if (c === tabNo) { controllers[c].className = "is-active"; } else { controllers[c].className = ""; } } } } bindControl(); function showContent(pos) { const content = document.getElementsByClassName("page-content")[0]; for (let i = 0; i < content.children.length; i++) { if (i === pos) { content.children[i].style.display = "block"; } else { content.children[i].style.display = "none"; } } } // User list - Only bother if user is admin. const list = document.getElementById("users"); function addBlock(username, me) { const block = document.createElement("a"); block.className = getTarget() === username ? "is-active panel-block" : "panel-block"; const icon = document.createElement("span"); icon.className = "panel-icon"; const iEle = document.createElement("i"); iEle.className = "fa fa-user"; iEle["aria-hidden"] = true; icon.appendChild(iEle); block.appendChild(icon); const name = document.createTextNode(me ? "Me" : username); block.appendChild(name); list.appendChild(block); block.onclick = function () { for (let count = 0; count < block.parentNode.children.length; count++) { block.parentNode.children[count].className = "panel-block"; } block.className = "panel-block is-active"; updateTargetUser(username); }; return block; } addBlock(window.user.username, true); if (window.user.isAdmin) { Api.get("/users/") .then(function (users) { for (let i = 0; i < users.users.length; i++) { const u = users.users[i]; if (u.username === window.user.username) continue; addBlock(u.username); } }) .catch(showError); } // Basically combines all actions that happen when user is updated into 1 function function updateTargetUser(username) { setTarget(username); getFiles(); getLinks(); userManagement(); } const refreshButton = document.getElementById("refresh"); refreshButton.onclick = function () { updateTargetUser(getTarget()); }; // Gallery const gallery = document.getElementById("gallery"); const filterImages = document.getElementsByClassName("filter")[0].children[1]; const filterAll = document.getElementsByClassName("filter")[0].children[0]; const nextPage = document.getElementById("next"); const prevPage = document.getElementById("previous"); const currentPageEle = document.getElementById("current-page"); // Gallery Filter filterImages.onclick = function () { updateFilter(true); setImage(); }; filterAll.onclick = function () { updateFilter(false); setAll(); }; function setImage() { filterImages.classList.add("is-active"); filterAll.classList.remove("is-active"); } function setAll() { filterImages.classList.remove("is-active"); filterAll.classList.add("is-active"); } // Initial - persistent if (getFilter()) { setImage(); } else { setAll(); } // Pagination nextPage.onclick = function () { const currentPage = getPage(); const newPage = currentPage + 1; setPage(newPage); setCurrentPageText(newPage); getFiles(); window.scrollTo(0, 0); }; prevPage.onclick = function () { const currentPage = getPage(); const newPage = currentPage - 1; // Refuse to go below 0 if (newPage >= 0) { setPage(newPage); setCurrentPageText(newPage); getFiles(); window.scrollTo(0, 0); } }; function setCurrentPageText(no) { currentPageEle.innerText = "" + (no + 1); } // Set initial setCurrentPageText(getPage()); function updateFilter(state) { if (state !== getFilter()) { setFilter(state); showFiles(); } } let files = []; function getFiles() { const page = getPage(); const target = Persist.get("targetUser"); Api.get(`/users/${target}/files?page=${page}`) .then(function (f) { if (f.success) { files = f.files; showFiles(); } else { // show error showError(f.error); } }) .catch(showError); } getFiles(); const parent = gallery.getElementsByClassName("columns")[0]; const modal = document.getElementsByClassName("modal")[0]; function showFiles() { clearChildren(parent); if (files.length === 0) { const noContent = document.createElement("h2"); noContent.className = "has-text-centered"; noContent.innerText = "There are no items to display."; parent.appendChild(noContent); } for (let i = 0; i < files.length; i++) { const file = files[i]; if (getFilter()) { // filter if (file.extension) { if (imgExtensions.includes(file.extension.toLowerCase())) { showImage(file); } } } else { if (file.extension && imgExtensions.includes(file.extension.toLowerCase())) { showImage(file); } else { showFile(file); } } } } // Create the gallery boxes - this is a pain in plain JS. function showImage(fileInfo) { const col = document.createElement("div"); col.className = "column is-3 outerBox"; const img = document.createElement("div"); const url = `${BaseUrl}/${fileInfo.id}.${fileInfo.extension}`; img.style.backgroundImage = `url('${url}')`; img.style.backgroundSize = "cover"; img.className = "file-box box"; col.appendChild(img); parent.appendChild(col); col.onclick = function (e) { e.preventDefault(); const img = modal.getElementsByTagName("img")[0]; const box = modal.getElementsByClassName("box")[0]; const meta = document.getElementById("modal-meta-text"); const a = document.getElementById("img-link"); img.style.display = "block"; box.style.display = "none"; img.src = url; a.href = url; const parsed = new Date(fileInfo.created); meta.innerText = `${fileInfo.owner} - ${parseDate(parsed)}`; modal.className = modal.className + " is-active"; modalTarget = fileInfo; }; } function showFile(fileInfo) { const col = document.createElement("div"); col.className = "column is-3 outerBox"; const innerColumns = document.createElement("div"); innerColumns.className = "columns is-centered is-vcentered"; const box = document.createElement("div"); box.className = "box file-box"; col.appendChild(box); box.appendChild(innerColumns); const innerCol = document.createElement("div"); innerCol.className = "column is-6"; innerColumns.appendChild(innerCol); const fileName = `${fileInfo.id}.${fileInfo.extension ? fileInfo.extension : ""}`; const url = `${BaseUrl}/${fileName}`; const icon = document.createElement("i"); let iconClass = "fa-file"; if (fileInfo.extension && iconMappings[fileInfo.extension]) { iconClass = iconMappings[fileInfo.extension]; } icon.className = "icon is-large fas " + iconClass; innerCol.appendChild(icon); const info = document.createElement("p"); info.className = "has-text-centered"; info.innerText = `${fileInfo.id}.${fileInfo.extension ? fileInfo.extension : ""}`; innerCol.appendChild(info); parent.appendChild(col); col.onclick = function (e) { e.preventDefault(); // Modal set-up const meta = document.getElementById("modal-meta-text"); const openButton = document.getElementById("modal-open"); const img = modal.getElementsByTagName("img")[0]; const box = modal.getElementsByClassName("box")[0]; const text = document.getElementById("m-file-text"); const desc = document.getElementById("m-file-desc"); img.style.display = "none"; box.style.display = "block"; text.innerText = fileName; if (fileInfo.extension && extensionMappings[fileInfo.extension.toLowerCase()]) { desc.innerText = extensionMappings[fileInfo.extension.toLowerCase()]; } else { desc.innerText = "Unknown file type"; } openButton.href = url; const parsed = new Date(fileInfo.created); meta.innerText = `${fileInfo.owner} - ${parseDate(parsed)}`; modal.className = modal.className + " is-active"; modalTarget = fileInfo; }; } function clearChildren(ele) { while (ele.firstChild) { ele.removeChild(ele.lastChild); } } // Modal modal.getElementsByClassName("modal-background")[0].onclick = hideModal; modal.getElementsByClassName("modal-close")[0].onclick = hideModal; function hideModal() { modal.classList.remove("is-active"); } const modalDelete = document.getElementById("modal-delete"); modalDelete.onclick = function () { modalDelete.classList.add("is-loading"); deleteFile(modalTarget, function () { modalDelete.classList.remove("is-loading"); getFiles(); hideModal(); }); }; // Links page function getLinks() { const target = Persist.get("targetUser"); Api.get(`/users/${target}/links`) .then(function (f) { if (f.success) { showLinks(f.links); } else { // show error showError(f.error); } }) .catch(showError); } getLinks(); function showLinks(linksList) { const tableParent = document.getElementById("links-parent"); clearChildren(tableParent); for (let counter = 0; counter < linksList.length; counter++) { const current = linksList[counter]; const tr = document.createElement("tr"); const redirect = document.createElement("td"); const redirectLink = document.createElement("a"); redirectLink.href = `${BaseUrl}/u/${current.id}`; redirectLink.innerText = `${BaseUrl}/u/${current.id}`; redirect.appendChild(redirectLink); tr.appendChild(redirect); const link = document.createElement("td"); const a = document.createElement("a"); a.innerText = current.url.substr(0, 50); a.href = current.url; a.target = "_blank"; a.rel = "noopener noreferrer"; link.appendChild(a); tr.appendChild(link); const dateRow = document.createElement("td"); dateRow.innerText = parseDate(new Date(current.created)); tr.appendChild(dateRow); const deleteRow = document.createElement("td"); const deleteButton = document.createElement("button"); deleteButton.className = "button is-danger is-small"; const trash = document.createElement("i"); trash.className = "fas fa-trash ml"; deleteButton.appendChild(trash); deleteButton.appendChild(document.createTextNode("Delete")); deleteRow.appendChild(deleteButton); tr.appendChild(deleteRow); deleteButton.onclick = function () { deleteButton.classList.add("is-loading"); const sure = confirm(`Are you sure you want to delete link ${current.id}?`); if (sure) { Api.delete(`/links/${current.id}`) .then((res) => { if (res.success) { tr.remove(); } else { showError(res.error); deleteButton.classList.remove("is-loading"); } }) .catch(showError); } else { deleteButton.classList.remove("is-loading"); } }; tableParent.appendChild(tr); } } // User management - Every time target updates. function userManagement() { const tar = getTarget(); const usernameBox = document.getElementById("settings-username"); usernameBox.innerText = tar; } userManagement(); const secondPassField = document.getElementById("pass2-field"); if (window.user.isAdmin) { secondPassField.remove(); } (function userCreation() { const createUser = document.getElementById("create-user"); const form = document.getElementById("create-user-form"); const usernameField = document.getElementById("create-username"); const passwordField = document.getElementById("create-password"); const errorBox = document.getElementById("create-user-error"); const submitButton = document.getElementById("create-user-submit"); const showPass = document.getElementById("create-show-pass"); if (window.user.isAdmin) { createUser.onclick = function () { showContent(3); }; } else { createUser.remove(); } showPass.addEventListener("change", function () { const state = showPass.checked; passwordField.type = state ? "text" : "password"; }); form.onsubmit = function (e) { e.preventDefault(); let username = usernameField.value; let password = passwordField.value; if (!username || username.length < 3) { return showError("Username must be filled out and be more than 3 characters."); } if (username.length > 50) { return showError("Username cannot be more than 50 characters... why tho?"); } if (!password || password.length < 3) { return showError("Password must be filled out and be more than 3 characters."); } if (password.length > 50) { return showError("Password cannot be more than 100 characters. Bcrypt only makes use of the first 72 bytes."); } submitButton.classList.add("is-loading"); Api.post("/users/create", { body: { username: username, password: password } }) .then(function (res) { submitButton.classList.remove("is-loading"); if (res.error) { showCreateError(res.error.message); } else { showMessage("User created", `User ${username} has been created, and opened in the settings panel.`, "success", 10); updateTargetUser(res.username); for (let count = 0; count < list.children.length; count++) { list.children[count].className = "panel-block"; } addBlock(res.username); showContent(2); } }) .catch(function (err) { submitButton.classList.remove("is-loading"); console.log(err); showError(err.message); }); return false; }; function showCreateError(text) { errorBox.innerText = text; errorBox.hidden = false; } })(); (function configButtons() { const uploadConfig = document.getElementById("download-upload-config"); const linkConfig = document.getElementById("download-link-config"); uploadConfig.onclick = function () { const target = getTarget(); window.location = `/api/users/${target}/config`; }; linkConfig.onclick = function () { const target = getTarget(); window.location = `/api/users/${target}/config?link=true`; }; })(); (function passwordReset() { // Password reset const form = document.getElementById("password-change-form"); const passwordInput = document.getElementById("password"); const passwordConfirm = document.getElementById("password2"); const passView = document.getElementById("show-pass"); const errorBox = document.getElementById("password-error-box"); const submitButton = document.getElementById("password-reset-submit"); passView.addEventListener("change", function () { const state = passView.checked; passwordInput.type = state ? "text" : "password"; }); // This means that we use built in validation and means password managers etc. generally just work. Much better than binding to click. form.onsubmit = function (e) { e.preventDefault(); const pass = passwordInput.value; if (!window.user.isAdmin) { // Check confirm is there const oldPass = passwordConfirm.value; if (!oldPass || oldPass === "" || oldPass.length <= 3) { showPasswordError("You must provide your current password. If you do not know it, ask the root user to reset it for you."); return false; } } if (pass && pass !== "" && pass.length > 6) { // it's good - we already checked that it matches. submitButton.classList.add("is-loading"); errorBox.hidden = true; const target = getTarget(); Api.patch(`/users/${target}/password`, { body: { newPassword: pass, oldPassword: passwordConfirm ? passwordConfirm.value : undefined } }) .then(function (res) { if (!res.error) { submitButton.classList.remove("is-loading"); showMessage("Password updated", `${target === window.user.username ? "Your" : `${target}'s `} password was successfully updated.`, "success", 10000); } else { showError(res.error); submitButton.classList.remove("is-loading"); } }) .catch(function (e) { submitButton.classList.remove("is-loading"); showError(e); }); } else { showPasswordError("You must fill out the password. It should be more than 6 characters."); return false; } function showPasswordError(text) { errorBox.innerText = text; errorBox.hidden = false; } }; })(); const tokenB = document.getElementById("update-token"); function onTokenClick() { tokenB.classList.add("is-loading"); const tar = getTarget(); const sure = confirm("Are you sure you want to do this? You will need to update the ShareX config for this user."); if (sure) { Api.patch(`/users/${tar}/token`) .then(function (res) { if (!res.error) { tokenB.classList.remove("is-loading"); const tar = getTarget(); const str = tar === window.user.username ? "Your token was successfully reset. Make sure you update your ShareX config!" : `${tar}'s token was reset. Make sure they update their ShareX config!`; showMessage("Token reset", str, "success", 15000); const tokenBox = document.getElementById("reset-box"); tokenBox.innerHTML = `Success! New token: <code>${res.token}</code>.`; tokenBox.hidden = false; } else { showError(res.error); } }) .catch((e) => showError(e)); } else { tokenB.classList.remove("is-loading"); } } tokenB.onclick = onTokenClick; (function deleteUser() { const deleteAll = document.getElementById("delete-all"); const deleteUsr = document.getElementById("delete-user"); deleteAll.onclick = function () { deleteUser(true); }; deleteUsr.onclick = function () { deleteUser(false); }; function deleteUser(deleteFiles) { // we don't want user to click the other one while it's deleting deleteAll.classList.add("is-loading"); deleteUsr.classList.add("is-loading"); const target = getTarget(); const sure = confirm(`Are you sure you want to delete ${target} ${deleteFiles ? "and all of their files" : ""}?`); if (!sure) { doneLoading(); return false; } Api.delete(`/users/${target}?deleteFiles=${deleteFiles}`) .then(function (res) { if (res.error) { showError(res.error); doneLoading(); } else { // it went ok doneLoading(); updateTargetUser(window.user.username); window.location.reload(); } }) .catch(function () { doneLoading(); }); function doneLoading() { deleteAll.classList.remove("is-loading"); deleteUsr.classList.remove("is-loading"); } } })(); });