Browse Source
			
			
			
			
				
		- Removed `unsafe-inline` for javascript from CSP. The admin interface now uses files instead of inline javascript. - Modified javascript to work not being inline. - Run eslint over javascript and fixed some items. - Added a `to_json` Handlebars helper. Used at the diagnostics page. - Changed `AdminTemplateData` struct to be smaller. The `config` was always added, but only used at one page. Same goes for `can_backup` and `version`. - Also inlined CSS. We can't remove the `unsafe-inline` from css, because that seems to break the web-vault currently. That might need some further checks. But for now the 404 page and all the admin pages are clear of inline scripts and styles.pull/3128/head
							committed by
							
								 Daniel García
								Daniel García
							
						
					
				
				 18 changed files with 946 additions and 718 deletions
			
			
		| @ -0,0 +1,26 @@ | |||||
|  | body { | ||||
|  |     padding-top: 75px; | ||||
|  | } | ||||
|  | .vaultwarden-icon { | ||||
|  |     width: 48px; | ||||
|  |     height: 48px; | ||||
|  |     height: 32px; | ||||
|  |     width: auto; | ||||
|  |     margin: -5px 0 0 0; | ||||
|  | } | ||||
|  | .footer { | ||||
|  |     padding: 40px 0 40px 0; | ||||
|  |     border-top: 1px solid #dee2e6; | ||||
|  | } | ||||
|  | .container { | ||||
|  |     max-width: 980px; | ||||
|  | } | ||||
|  | .content { | ||||
|  |     padding-top: 20px; | ||||
|  |     padding-bottom: 20px; | ||||
|  |     padding-left: 15px; | ||||
|  |     padding-right: 15px; | ||||
|  | } | ||||
|  | .vw-404 { | ||||
|  |     max-width: 500px; width: 100%; | ||||
|  | } | ||||
| @ -0,0 +1,45 @@ | |||||
|  | body { | ||||
|  |     padding-top: 75px; | ||||
|  | } | ||||
|  | img { | ||||
|  |     width: 48px; | ||||
|  |     height: 48px; | ||||
|  | } | ||||
|  | .vaultwarden-icon { | ||||
|  |     height: 32px; | ||||
|  |     width: auto; | ||||
|  |     margin: -5px 0 0 0; | ||||
|  | } | ||||
|  | /* Special alert-row class to use Bootstrap v5.2+ variable colors */ | ||||
|  | .alert-row { | ||||
|  |     --bs-alert-border: 1px solid var(--bs-alert-border-color); | ||||
|  |     color: var(--bs-alert-color); | ||||
|  |     background-color: var(--bs-alert-bg); | ||||
|  |     border: var(--bs-alert-border); | ||||
|  | } | ||||
|  | 
 | ||||
|  | #users-table .vw-created-at, #users-table .vw-last-active { | ||||
|  |     width: 85px; | ||||
|  |     min-width: 70px; | ||||
|  | } | ||||
|  | #users-table .vw-items { | ||||
|  |     width: 35px; | ||||
|  |     min-width: 35px; | ||||
|  | } | ||||
|  | #users-table .vw-organizations { | ||||
|  |     min-width: 120px; | ||||
|  | } | ||||
|  | #users-table .vw-actions, #orgs-table .vw-actions { | ||||
|  |     width: 130px; | ||||
|  |     min-width: 130px; | ||||
|  | } | ||||
|  | #users-table .vw-org-cell { | ||||
|  |     max-height: 120px; | ||||
|  | } | ||||
|  | 
 | ||||
|  | #support-string { | ||||
|  |     height: 16rem; | ||||
|  | } | ||||
|  | .vw-copy-toast { | ||||
|  |     width: 15rem; | ||||
|  | } | ||||
| @ -0,0 +1,65 @@ | |||||
|  | "use strict"; | ||||
|  | 
 | ||||
|  | function getBaseUrl() { | ||||
|  |     // If the base URL is `https://vaultwarden.example.com/base/path/`,
 | ||||
|  |     // `window.location.href` should have one of the following forms:
 | ||||
|  |     //
 | ||||
|  |     // - `https://vaultwarden.example.com/base/path/`
 | ||||
|  |     // - `https://vaultwarden.example.com/base/path/#/some/route[?queryParam=...]`
 | ||||
|  |     //
 | ||||
|  |     // We want to get to just `https://vaultwarden.example.com/base/path`.
 | ||||
|  |     const baseUrl = window.location.href; | ||||
|  |     const adminPos = baseUrl.indexOf("/admin"); | ||||
|  |     return baseUrl.substring(0, adminPos != -1 ? adminPos : baseUrl.length); | ||||
|  | } | ||||
|  | const BASE_URL = getBaseUrl(); | ||||
|  | 
 | ||||
|  | function reload() { | ||||
|  |     // Reload the page by setting the exact same href
 | ||||
|  |     // Using window.location.reload() could cause a repost.
 | ||||
|  |     window.location = window.location.href; | ||||
|  | } | ||||
|  | 
 | ||||
|  | function msg(text, reload_page = true) { | ||||
|  |     text && alert(text); | ||||
|  |     reload_page && reload(); | ||||
|  | } | ||||
|  | 
 | ||||
|  | function _post(url, successMsg, errMsg, body, reload_page = true) { | ||||
|  |     fetch(url, { | ||||
|  |         method: "POST", | ||||
|  |         body: body, | ||||
|  |         mode: "same-origin", | ||||
|  |         credentials: "same-origin", | ||||
|  |         headers: { "Content-Type": "application/json" } | ||||
|  |     }).then( resp => { | ||||
|  |         if (resp.ok) { msg(successMsg, reload_page); return Promise.reject({error: false}); } | ||||
|  |         const respStatus = resp.status; | ||||
|  |         const respStatusText = resp.statusText; | ||||
|  |         return resp.text(); | ||||
|  |     }).then( respText => { | ||||
|  |         try { | ||||
|  |             const respJson = JSON.parse(respText); | ||||
|  |             return respJson ? respJson.ErrorModel.Message : "Unknown error"; | ||||
|  |         } catch (e) { | ||||
|  |             return Promise.reject({body:respStatus + " - " + respStatusText, error: true}); | ||||
|  |         } | ||||
|  |     }).then( apiMsg => { | ||||
|  |         msg(errMsg + "\n" + apiMsg, reload_page); | ||||
|  |     }).catch( e => { | ||||
|  |         if (e.error === false) { return true; } | ||||
|  |         else { msg(errMsg + "\n" + e.body, reload_page); } | ||||
|  |     }); | ||||
|  | } | ||||
|  | 
 | ||||
|  | // onLoad events
 | ||||
|  | document.addEventListener("DOMContentLoaded", (/*event*/) => { | ||||
|  |     // get current URL path and assign "active" class to the correct nav-item
 | ||||
|  |     const pathname = window.location.pathname; | ||||
|  |     if (pathname === "") return; | ||||
|  |     const navItem = document.querySelectorAll(`.navbar-nav .nav-item a[href="${pathname}"]`); | ||||
|  |     if (navItem.length === 1) { | ||||
|  |         navItem[0].className = navItem[0].className + " active"; | ||||
|  |         navItem[0].setAttribute("aria-current", "page"); | ||||
|  |     } | ||||
|  | }); | ||||
| @ -0,0 +1,219 @@ | |||||
|  | "use strict"; | ||||
|  | 
 | ||||
|  | var dnsCheck = false; | ||||
|  | var timeCheck = false; | ||||
|  | var domainCheck = false; | ||||
|  | var httpsCheck = false; | ||||
|  | 
 | ||||
|  | // ================================
 | ||||
|  | // Date & Time Check
 | ||||
|  | const d = new Date(); | ||||
|  | const year = d.getUTCFullYear(); | ||||
|  | const month = String(d.getUTCMonth()+1).padStart(2, "0"); | ||||
|  | const day = String(d.getUTCDate()).padStart(2, "0"); | ||||
|  | const hour = String(d.getUTCHours()).padStart(2, "0"); | ||||
|  | const minute = String(d.getUTCMinutes()).padStart(2, "0"); | ||||
|  | const seconds = String(d.getUTCSeconds()).padStart(2, "0"); | ||||
|  | const browserUTC = `${year}-${month}-${day} ${hour}:${minute}:${seconds} UTC`; | ||||
|  | 
 | ||||
|  | // ================================
 | ||||
|  | // Check if the output is a valid IP
 | ||||
|  | const isValidIp = value => (/^(?:(?:^|\.)(?:2(?:5[0-5]|[0-4]\d)|1?\d?\d)){4}$/.test(value) ? true : false); | ||||
|  | 
 | ||||
|  | function checkVersions(platform, installed, latest, commit=null) { | ||||
|  |     if (installed === "-" || latest === "-") { | ||||
|  |         document.getElementById(`${platform}-failed`).classList.remove("d-none"); | ||||
|  |         return; | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     // Only check basic versions, no commit revisions
 | ||||
|  |     if (commit === null || installed.indexOf("-") === -1) { | ||||
|  |         if (installed !== latest) { | ||||
|  |             document.getElementById(`${platform}-warning`).classList.remove("d-none"); | ||||
|  |         } else { | ||||
|  |             document.getElementById(`${platform}-success`).classList.remove("d-none"); | ||||
|  |         } | ||||
|  |     } else { | ||||
|  |         // Check if this is a branched version.
 | ||||
|  |         const branchRegex = /(?:\s)\((.*?)\)/; | ||||
|  |         const branchMatch = installed.match(branchRegex); | ||||
|  |         if (branchMatch !== null) { | ||||
|  |             document.getElementById(`${platform}-branch`).classList.remove("d-none"); | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         // This will remove branch info and check if there is a commit hash
 | ||||
|  |         const installedRegex = /(\d+\.\d+\.\d+)-(\w+)/; | ||||
|  |         const instMatch = installed.match(installedRegex); | ||||
|  | 
 | ||||
|  |         // It could be that a new tagged version has the same commit hash.
 | ||||
|  |         // In this case the version is the same but only the number is different
 | ||||
|  |         if (instMatch !== null) { | ||||
|  |             if (instMatch[2] === commit) { | ||||
|  |                 // The commit hashes are the same, so latest version is installed
 | ||||
|  |                 document.getElementById(`${platform}-success`).classList.remove("d-none"); | ||||
|  |                 return; | ||||
|  |             } | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         if (installed === latest) { | ||||
|  |             document.getElementById(`${platform}-success`).classList.remove("d-none"); | ||||
|  |         } else { | ||||
|  |             document.getElementById(`${platform}-warning`).classList.remove("d-none"); | ||||
|  |         } | ||||
|  |     } | ||||
|  | } | ||||
|  | 
 | ||||
|  | // ================================
 | ||||
|  | // Generate support string to be pasted on github or the forum
 | ||||
|  | async function generateSupportString(dj) { | ||||
|  |     event.preventDefault(); | ||||
|  |     event.stopPropagation(); | ||||
|  | 
 | ||||
|  |     let supportString = "### Your environment (Generated via diagnostics page)\n"; | ||||
|  | 
 | ||||
|  |     supportString += `* Vaultwarden version: v${dj.current_release}\n`; | ||||
|  |     supportString += `* Web-vault version: v${dj.web_vault_version}\n`; | ||||
|  |     supportString += `* OS/Arch: ${dj.host_os}/${dj.host_arch}\n`; | ||||
|  |     supportString += `* Running within Docker: ${dj.running_within_docker} (Base: ${dj.docker_base_image})\n`; | ||||
|  |     supportString += "* Environment settings overridden: "; | ||||
|  |     if (dj.overrides != "") { | ||||
|  |         supportString += "true\n"; | ||||
|  |     } else { | ||||
|  |         supportString += "false\n"; | ||||
|  |     } | ||||
|  |     supportString += `* Uses a reverse proxy: ${dj.ip_header_exists}\n`; | ||||
|  |     if (dj.ip_header_exists) { | ||||
|  |         supportString += `* IP Header check: ${dj.ip_header_match} (${dj.ip_header_name})\n`; | ||||
|  |     } | ||||
|  |     supportString += `* Internet access: ${dj.has_http_access}\n`; | ||||
|  |     supportString += `* Internet access via a proxy: ${dj.uses_proxy}\n`; | ||||
|  |     supportString += `* DNS Check: ${dnsCheck}\n`; | ||||
|  |     supportString += `* Time Check: ${timeCheck}\n`; | ||||
|  |     supportString += `* Domain Configuration Check: ${domainCheck}\n`; | ||||
|  |     supportString += `* HTTPS Check: ${httpsCheck}\n`; | ||||
|  |     supportString += `* Database type: ${dj.db_type}\n`; | ||||
|  |     supportString += `* Database version: ${dj.db_version}\n`; | ||||
|  |     supportString += "* Clients used: \n"; | ||||
|  |     supportString += "* Reverse proxy and version: \n"; | ||||
|  |     supportString += "* Other relevant information: \n"; | ||||
|  | 
 | ||||
|  |     const jsonResponse = await fetch(`${BASE_URL}/admin/diagnostics/config`, { | ||||
|  |         "headers": { "Accept": "application/json" } | ||||
|  |     }); | ||||
|  |     if (!jsonResponse.ok) { | ||||
|  |         alert("Generation failed: " + jsonResponse.statusText); | ||||
|  |         throw new Error(jsonResponse); | ||||
|  |     } | ||||
|  |     const configJson = await jsonResponse.json(); | ||||
|  |     supportString += "\n### Config (Generated via diagnostics page)\n<details><summary>Show Running Config</summary>\n"; | ||||
|  |     supportString += `\n**Environment settings which are overridden:** ${dj.overrides}\n`; | ||||
|  |     supportString += "\n\n```json\n" + JSON.stringify(configJson, undefined, 2) + "\n```\n</details>\n"; | ||||
|  | 
 | ||||
|  |     document.getElementById("support-string").innerText = supportString; | ||||
|  |     document.getElementById("support-string").classList.remove("d-none"); | ||||
|  |     document.getElementById("copy-support").classList.remove("d-none"); | ||||
|  | } | ||||
|  | 
 | ||||
|  | function copyToClipboard() { | ||||
|  |     event.preventDefault(); | ||||
|  |     event.stopPropagation(); | ||||
|  | 
 | ||||
|  |     const supportStr = document.getElementById("support-string").innerText; | ||||
|  |     const tmpCopyEl = document.createElement("textarea"); | ||||
|  | 
 | ||||
|  |     tmpCopyEl.setAttribute("id", "copy-support-string"); | ||||
|  |     tmpCopyEl.setAttribute("readonly", ""); | ||||
|  |     tmpCopyEl.value = supportStr; | ||||
|  |     tmpCopyEl.style.position = "absolute"; | ||||
|  |     tmpCopyEl.style.left = "-9999px"; | ||||
|  |     document.body.appendChild(tmpCopyEl); | ||||
|  |     tmpCopyEl.select(); | ||||
|  |     document.execCommand("copy"); | ||||
|  |     tmpCopyEl.remove(); | ||||
|  | 
 | ||||
|  |     new BSN.Toast("#toastClipboardCopy").show(); | ||||
|  | } | ||||
|  | 
 | ||||
|  | function checkTimeDrift(browserUTC, serverUTC) { | ||||
|  |     const timeDrift = ( | ||||
|  |         Date.parse(serverUTC.replace(" ", "T").replace(" UTC", "")) - | ||||
|  |         Date.parse(browserUTC.replace(" ", "T").replace(" UTC", "")) | ||||
|  |     ) / 1000; | ||||
|  |     if (timeDrift > 20 || timeDrift < -20) { | ||||
|  |         document.getElementById("time-warning").classList.remove("d-none"); | ||||
|  |     } else { | ||||
|  |         document.getElementById("time-success").classList.remove("d-none"); | ||||
|  |         timeCheck = true; | ||||
|  |     } | ||||
|  | } | ||||
|  | 
 | ||||
|  | function checkDomain(browserURL, serverURL) { | ||||
|  |     if (serverURL == browserURL) { | ||||
|  |         document.getElementById("domain-success").classList.remove("d-none"); | ||||
|  |         domainCheck = true; | ||||
|  |     } else { | ||||
|  |         document.getElementById("domain-warning").classList.remove("d-none"); | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     // Check for HTTPS at domain-server-string
 | ||||
|  |     if (serverURL.startsWith("https://") ) { | ||||
|  |         document.getElementById("https-success").classList.remove("d-none"); | ||||
|  |         httpsCheck = true; | ||||
|  |     } else { | ||||
|  |         document.getElementById("https-warning").classList.remove("d-none"); | ||||
|  |     } | ||||
|  | } | ||||
|  | 
 | ||||
|  | function initVersionCheck(dj) { | ||||
|  |     const serverInstalled = dj.current_release; | ||||
|  |     const serverLatest = dj.latest_release; | ||||
|  |     const serverLatestCommit = dj.latest_commit; | ||||
|  | 
 | ||||
|  |     if (serverInstalled.indexOf("-") !== -1 && serverLatest !== "-" && serverLatestCommit !== "-") { | ||||
|  |         document.getElementById("server-latest-commit").classList.remove("d-none"); | ||||
|  |     } | ||||
|  |     checkVersions("server", serverInstalled, serverLatest, serverLatestCommit); | ||||
|  | 
 | ||||
|  |     if (!dj.running_within_docker) { | ||||
|  |         const webInstalled = dj.web_vault_version; | ||||
|  |         const webLatest = dj.latest_web_build; | ||||
|  |         checkVersions("web", webInstalled, webLatest); | ||||
|  |     } | ||||
|  | } | ||||
|  | 
 | ||||
|  | function checkDns(dns_resolved) { | ||||
|  |     if (isValidIp(dns_resolved)) { | ||||
|  |         document.getElementById("dns-success").classList.remove("d-none"); | ||||
|  |         dnsCheck = true; | ||||
|  |     } else { | ||||
|  |         document.getElementById("dns-warning").classList.remove("d-none"); | ||||
|  |     } | ||||
|  | } | ||||
|  | 
 | ||||
|  | function init(dj) { | ||||
|  |     // Time check
 | ||||
|  |     document.getElementById("time-browser-string").innerText = browserUTC; | ||||
|  |     checkTimeDrift(browserUTC, dj.server_time); | ||||
|  | 
 | ||||
|  |     // Domain check
 | ||||
|  |     const browserURL = location.href.toLowerCase(); | ||||
|  |     document.getElementById("domain-browser-string").innerText = browserURL; | ||||
|  |     checkDomain(browserURL, dj.admin_url.toLowerCase()); | ||||
|  | 
 | ||||
|  |     // Version check
 | ||||
|  |     initVersionCheck(dj); | ||||
|  | 
 | ||||
|  |     // DNS Check
 | ||||
|  |     checkDns(dj.dns_resolved); | ||||
|  | } | ||||
|  | 
 | ||||
|  | // onLoad events
 | ||||
|  | document.addEventListener("DOMContentLoaded", (/*event*/) => { | ||||
|  |     const diag_json = JSON.parse(document.getElementById("diagnostics_json").innerText); | ||||
|  |     init(diag_json); | ||||
|  | 
 | ||||
|  |     document.getElementById("gen-support").addEventListener("click", () => { | ||||
|  |         generateSupportString(diag_json); | ||||
|  |     }); | ||||
|  |     document.getElementById("copy-support").addEventListener("click", copyToClipboard); | ||||
|  | }); | ||||
| @ -0,0 +1,54 @@ | |||||
|  | "use strict"; | ||||
|  | 
 | ||||
|  | function deleteOrganization() { | ||||
|  |     event.preventDefault(); | ||||
|  |     event.stopPropagation(); | ||||
|  |     const org_uuid = event.target.dataset.vwOrgUuid; | ||||
|  |     const org_name = event.target.dataset.vwOrgName; | ||||
|  |     const billing_email = event.target.dataset.vwBillingEmail; | ||||
|  |     if (!org_uuid) { | ||||
|  |         alert("Required parameters not found!"); | ||||
|  |         return false; | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     // First make sure the user wants to delete this organization
 | ||||
|  |     const continueDelete = confirm(`WARNING: All data of this organization (${org_name}) will be lost!\nMake sure you have a backup, this cannot be undone!`); | ||||
|  |     if (continueDelete == true) { | ||||
|  |         const input_org_uuid = prompt(`To delete the organization "${org_name} (${billing_email})", please type the organization uuid below.`); | ||||
|  |         if (input_org_uuid != null) { | ||||
|  |             if (input_org_uuid == org_uuid) { | ||||
|  |                 _post(`${BASE_URL}/admin/organizations/${org_uuid}/delete`, | ||||
|  |                     "Organization deleted correctly", | ||||
|  |                     "Error deleting organization" | ||||
|  |                 ); | ||||
|  |             } else { | ||||
|  |                 alert("Wrong organization uuid, please try again"); | ||||
|  |             } | ||||
|  |         } | ||||
|  |     } | ||||
|  | } | ||||
|  | 
 | ||||
|  | // onLoad events
 | ||||
|  | document.addEventListener("DOMContentLoaded", (/*event*/) => { | ||||
|  |     jQuery("#orgs-table").DataTable({ | ||||
|  |         "stateSave": true, | ||||
|  |         "responsive": true, | ||||
|  |         "lengthMenu": [ | ||||
|  |             [-1, 5, 10, 25, 50], | ||||
|  |             ["All", 5, 10, 25, 50] | ||||
|  |         ], | ||||
|  |         "pageLength": -1, // Default show all
 | ||||
|  |         "columnDefs": [{ | ||||
|  |             "targets": 4, | ||||
|  |             "searchable": false, | ||||
|  |             "orderable": false | ||||
|  |         }] | ||||
|  |     }); | ||||
|  | 
 | ||||
|  |     // Add click events for organization actions
 | ||||
|  |     document.querySelectorAll("button[vw-delete-organization]").forEach(btn => { | ||||
|  |         btn.addEventListener("click", deleteOrganization); | ||||
|  |     }); | ||||
|  | 
 | ||||
|  |     document.getElementById("reload").addEventListener("click", reload); | ||||
|  | }); | ||||
| @ -0,0 +1,180 @@ | |||||
|  | "use strict"; | ||||
|  | 
 | ||||
|  | function smtpTest() { | ||||
|  |     event.preventDefault(); | ||||
|  |     event.stopPropagation(); | ||||
|  |     if (formHasChanges(config_form)) { | ||||
|  |         alert("Config has been changed but not yet saved.\nPlease save the changes first before sending a test email."); | ||||
|  |         return false; | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     const test_email = document.getElementById("smtp-test-email"); | ||||
|  | 
 | ||||
|  |     // Do a very very basic email address check.
 | ||||
|  |     if (test_email.value.match(/\S+@\S+/i) === null) { | ||||
|  |         test_email.parentElement.classList.add("was-validated"); | ||||
|  |         return false; | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     const data = JSON.stringify({ "email": test_email.value }); | ||||
|  |     _post(`${BASE_URL}/admin/test/smtp/`, | ||||
|  |         "SMTP Test email sent correctly", | ||||
|  |         "Error sending SMTP test email", | ||||
|  |         data, false | ||||
|  |     ); | ||||
|  | } | ||||
|  | 
 | ||||
|  | function getFormData() { | ||||
|  |     let data = {}; | ||||
|  | 
 | ||||
|  |     document.querySelectorAll(".conf-checkbox").forEach(function (e) { | ||||
|  |         data[e.name] = e.checked; | ||||
|  |     }); | ||||
|  | 
 | ||||
|  |     document.querySelectorAll(".conf-number").forEach(function (e) { | ||||
|  |         data[e.name] = e.value ? +e.value : null; | ||||
|  |     }); | ||||
|  | 
 | ||||
|  |     document.querySelectorAll(".conf-text, .conf-password").forEach(function (e) { | ||||
|  |         data[e.name] = e.value || null; | ||||
|  |     }); | ||||
|  |     return data; | ||||
|  | } | ||||
|  | 
 | ||||
|  | function saveConfig() { | ||||
|  |     const data = JSON.stringify(getFormData()); | ||||
|  |     _post(`${BASE_URL}/admin/config/`, | ||||
|  |         "Config saved correctly", | ||||
|  |         "Error saving config", | ||||
|  |         data | ||||
|  |     ); | ||||
|  |     event.preventDefault(); | ||||
|  | } | ||||
|  | 
 | ||||
|  | function deleteConf() { | ||||
|  |     event.preventDefault(); | ||||
|  |     event.stopPropagation(); | ||||
|  |     const input = prompt( | ||||
|  |         "This will remove all user configurations, and restore the defaults and the " + | ||||
|  |         "values set by the environment. This operation could be dangerous. Type 'DELETE' to proceed:" | ||||
|  |     ); | ||||
|  |     if (input === "DELETE") { | ||||
|  |         _post(`${BASE_URL}/admin/config/delete`, | ||||
|  |             "Config deleted correctly", | ||||
|  |             "Error deleting config" | ||||
|  |         ); | ||||
|  |     } else { | ||||
|  |         alert("Wrong input, please try again"); | ||||
|  |     } | ||||
|  | } | ||||
|  | 
 | ||||
|  | function backupDatabase() { | ||||
|  |     event.preventDefault(); | ||||
|  |     event.stopPropagation(); | ||||
|  |     _post(`${BASE_URL}/admin/config/backup_db`, | ||||
|  |         "Backup created successfully", | ||||
|  |         "Error creating backup", null, false | ||||
|  |     ); | ||||
|  | } | ||||
|  | 
 | ||||
|  | // Two functions to help check if there were changes to the form fields
 | ||||
|  | // Useful for example during the smtp test to prevent people from clicking save before testing there new settings
 | ||||
|  | function initChangeDetection(form) { | ||||
|  |     const ignore_fields = ["smtp-test-email"]; | ||||
|  |     Array.from(form).forEach((el) => { | ||||
|  |         if (! ignore_fields.includes(el.id)) { | ||||
|  |             el.dataset.origValue = el.value; | ||||
|  |         } | ||||
|  |     }); | ||||
|  | } | ||||
|  | 
 | ||||
|  | function formHasChanges(form) { | ||||
|  |     return Array.from(form).some(el => "origValue" in el.dataset && ( el.dataset.origValue !== el.value)); | ||||
|  | } | ||||
|  | 
 | ||||
|  | // This function will prevent submitting a from when someone presses enter.
 | ||||
|  | function preventFormSubmitOnEnter(form) { | ||||
|  |     form.onkeypress = function(e) { | ||||
|  |         const key = e.charCode || e.keyCode || 0; | ||||
|  |         if (key == 13) { | ||||
|  |             e.preventDefault(); | ||||
|  |         } | ||||
|  |     }; | ||||
|  | } | ||||
|  | 
 | ||||
|  | // This function will hook into the smtp-test-email input field and will call the smtpTest() function when enter is pressed.
 | ||||
|  | function submitTestEmailOnEnter() { | ||||
|  |     const smtp_test_email_input = document.getElementById("smtp-test-email"); | ||||
|  |     smtp_test_email_input.onkeypress = function(e) { | ||||
|  |         const key = e.charCode || e.keyCode || 0; | ||||
|  |         if (key == 13) { | ||||
|  |             e.preventDefault(); | ||||
|  |             smtpTest(); | ||||
|  |         } | ||||
|  |     }; | ||||
|  | } | ||||
|  | 
 | ||||
|  | // Colorize some settings which are high risk
 | ||||
|  | function colorRiskSettings() { | ||||
|  |     const risk_items = document.getElementsByClassName("col-form-label"); | ||||
|  |     Array.from(risk_items).forEach((el) => { | ||||
|  |         if (el.innerText.toLowerCase().includes("risks") ) { | ||||
|  |             el.parentElement.className += " alert-danger"; | ||||
|  |         } | ||||
|  |     }); | ||||
|  | } | ||||
|  | 
 | ||||
|  | function toggleVis(evt) { | ||||
|  |     event.preventDefault(); | ||||
|  |     event.stopPropagation(); | ||||
|  | 
 | ||||
|  |     const elem = document.getElementById(evt.target.dataset.vwPwToggle); | ||||
|  |     const type = elem.getAttribute("type"); | ||||
|  |     if (type === "text") { | ||||
|  |         elem.setAttribute("type", "password"); | ||||
|  |     } else { | ||||
|  |         elem.setAttribute("type", "text"); | ||||
|  |     } | ||||
|  | } | ||||
|  | 
 | ||||
|  | function masterCheck(check_id, inputs_query) { | ||||
|  |     function onChanged(checkbox, inputs_query) { | ||||
|  |         return function _fn() { | ||||
|  |             document.querySelectorAll(inputs_query).forEach(function (e) { e.disabled = !checkbox.checked; }); | ||||
|  |             checkbox.disabled = false; | ||||
|  |         }; | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     const checkbox = document.getElementById(check_id); | ||||
|  |     const onChange = onChanged(checkbox, inputs_query); | ||||
|  |     onChange(); // Trigger the event initially
 | ||||
|  |     checkbox.addEventListener("change", onChange); | ||||
|  | } | ||||
|  | 
 | ||||
|  | const config_form = document.getElementById("config-form"); | ||||
|  | 
 | ||||
|  | // onLoad events
 | ||||
|  | document.addEventListener("DOMContentLoaded", (/*event*/) => { | ||||
|  |     initChangeDetection(config_form); | ||||
|  |     // Prevent enter to submitting the form and save the config.
 | ||||
|  |     // Users need to really click on save, this also to prevent accidental submits.
 | ||||
|  |     preventFormSubmitOnEnter(config_form); | ||||
|  | 
 | ||||
|  |     submitTestEmailOnEnter(); | ||||
|  |     colorRiskSettings(); | ||||
|  | 
 | ||||
|  |     document.querySelectorAll("input[id^='input__enable_']").forEach(group_toggle => { | ||||
|  |         const input_id = group_toggle.id.replace("input__enable_", "#g_"); | ||||
|  |         masterCheck(group_toggle.id, `${input_id} input`); | ||||
|  |     }); | ||||
|  | 
 | ||||
|  |     document.querySelectorAll("button[data-vw-pw-toggle]").forEach(password_toggle_btn => { | ||||
|  |         password_toggle_btn.addEventListener("click", toggleVis); | ||||
|  |     }); | ||||
|  | 
 | ||||
|  |     document.getElementById("backupDatabase").addEventListener("click", backupDatabase); | ||||
|  |     document.getElementById("deleteConf").addEventListener("click", deleteConf); | ||||
|  |     document.getElementById("smtpTest").addEventListener("click", smtpTest); | ||||
|  | 
 | ||||
|  |     config_form.addEventListener("submit", saveConfig); | ||||
|  | }); | ||||
| @ -0,0 +1,246 @@ | |||||
|  | "use strict"; | ||||
|  | 
 | ||||
|  | function deleteUser() { | ||||
|  |     event.preventDefault(); | ||||
|  |     event.stopPropagation(); | ||||
|  |     const id = event.target.parentNode.dataset.vwUserUuid; | ||||
|  |     const email = event.target.parentNode.dataset.vwUserEmail; | ||||
|  |     if (!id || !email) { | ||||
|  |         alert("Required parameters not found!"); | ||||
|  |         return false; | ||||
|  |     } | ||||
|  |     const input_email = prompt(`To delete user "${email}", please type the email below`); | ||||
|  |     if (input_email != null) { | ||||
|  |         if (input_email == email) { | ||||
|  |             _post(`${BASE_URL}/admin/users/${id}/delete`, | ||||
|  |                 "User deleted correctly", | ||||
|  |                 "Error deleting user" | ||||
|  |             ); | ||||
|  |         } else { | ||||
|  |             alert("Wrong email, please try again"); | ||||
|  |         } | ||||
|  |     } | ||||
|  | } | ||||
|  | 
 | ||||
|  | function remove2fa() { | ||||
|  |     event.preventDefault(); | ||||
|  |     event.stopPropagation(); | ||||
|  |     const id = event.target.parentNode.dataset.vwUserUuid; | ||||
|  |     if (!id) { | ||||
|  |         alert("Required parameters not found!"); | ||||
|  |         return false; | ||||
|  |     } | ||||
|  |     _post(`${BASE_URL}/admin/users/${id}/remove-2fa`, | ||||
|  |         "2FA removed correctly", | ||||
|  |         "Error removing 2FA" | ||||
|  |     ); | ||||
|  | } | ||||
|  | 
 | ||||
|  | function deauthUser() { | ||||
|  |     event.preventDefault(); | ||||
|  |     event.stopPropagation(); | ||||
|  |     const id = event.target.parentNode.dataset.vwUserUuid; | ||||
|  |     if (!id) { | ||||
|  |         alert("Required parameters not found!"); | ||||
|  |         return false; | ||||
|  |     } | ||||
|  |     _post(`${BASE_URL}/admin/users/${id}/deauth`, | ||||
|  |         "Sessions deauthorized correctly", | ||||
|  |         "Error deauthorizing sessions" | ||||
|  |     ); | ||||
|  | } | ||||
|  | 
 | ||||
|  | function disableUser() { | ||||
|  |     event.preventDefault(); | ||||
|  |     event.stopPropagation(); | ||||
|  |     const id = event.target.parentNode.dataset.vwUserUuid; | ||||
|  |     const email = event.target.parentNode.dataset.vwUserEmail; | ||||
|  |     if (!id || !email) { | ||||
|  |         alert("Required parameters not found!"); | ||||
|  |         return false; | ||||
|  |     } | ||||
|  |     const confirmed = confirm(`Are you sure you want to disable user "${email}"? This will also deauthorize their sessions.`); | ||||
|  |     if (confirmed) { | ||||
|  |         _post(`${BASE_URL}/admin/users/${id}/disable`, | ||||
|  |             "User disabled successfully", | ||||
|  |             "Error disabling user" | ||||
|  |         ); | ||||
|  |     } | ||||
|  | } | ||||
|  | 
 | ||||
|  | function enableUser() { | ||||
|  |     event.preventDefault(); | ||||
|  |     event.stopPropagation(); | ||||
|  |     const id = event.target.parentNode.dataset.vwUserUuid; | ||||
|  |     const email = event.target.parentNode.dataset.vwUserEmail; | ||||
|  |     if (!id || !email) { | ||||
|  |         alert("Required parameters not found!"); | ||||
|  |         return false; | ||||
|  |     } | ||||
|  |     const confirmed = confirm(`Are you sure you want to enable user "${email}"?`); | ||||
|  |     if (confirmed) { | ||||
|  |         _post(`${BASE_URL}/admin/users/${id}/enable`, | ||||
|  |             "User enabled successfully", | ||||
|  |             "Error enabling user" | ||||
|  |         ); | ||||
|  |     } | ||||
|  | } | ||||
|  | 
 | ||||
|  | function updateRevisions() { | ||||
|  |     event.preventDefault(); | ||||
|  |     event.stopPropagation(); | ||||
|  |     _post(`${BASE_URL}/admin/users/update_revision`, | ||||
|  |         "Success, clients will sync next time they connect", | ||||
|  |         "Error forcing clients to sync" | ||||
|  |     ); | ||||
|  | } | ||||
|  | 
 | ||||
|  | function inviteUser() { | ||||
|  |     event.preventDefault(); | ||||
|  |     event.stopPropagation(); | ||||
|  |     const email = document.getElementById("inviteEmail"); | ||||
|  |     const data = JSON.stringify({ | ||||
|  |         "email": email.value | ||||
|  |     }); | ||||
|  |     email.value = ""; | ||||
|  |     _post(`${BASE_URL}/admin/invite/`, | ||||
|  |         "User invited correctly", | ||||
|  |         "Error inviting user", | ||||
|  |         data | ||||
|  |     ); | ||||
|  | } | ||||
|  | 
 | ||||
|  | const ORG_TYPES = { | ||||
|  |     "0": { | ||||
|  |         "name": "Owner", | ||||
|  |         "color": "orange" | ||||
|  |     }, | ||||
|  |     "1": { | ||||
|  |         "name": "Admin", | ||||
|  |         "color": "blueviolet" | ||||
|  |     }, | ||||
|  |     "2": { | ||||
|  |         "name": "User", | ||||
|  |         "color": "blue" | ||||
|  |     }, | ||||
|  |     "3": { | ||||
|  |         "name": "Manager", | ||||
|  |         "color": "green" | ||||
|  |     }, | ||||
|  | }; | ||||
|  | 
 | ||||
|  | // Special sort function to sort dates in ISO format
 | ||||
|  | jQuery.extend(jQuery.fn.dataTableExt.oSort, { | ||||
|  |     "date-iso-pre": function(a) { | ||||
|  |         let x; | ||||
|  |         const sortDate = a.replace(/(<([^>]+)>)/gi, "").trim(); | ||||
|  |         if (sortDate !== "") { | ||||
|  |             const dtParts = sortDate.split(" "); | ||||
|  |             const timeParts = (undefined != dtParts[1]) ? dtParts[1].split(":") : ["00", "00", "00"]; | ||||
|  |             const dateParts = dtParts[0].split("-"); | ||||
|  |             x = (dateParts[0] + dateParts[1] + dateParts[2] + timeParts[0] + timeParts[1] + ((undefined != timeParts[2]) ? timeParts[2] : 0)) * 1; | ||||
|  |             if (isNaN(x)) { | ||||
|  |                 x = 0; | ||||
|  |             } | ||||
|  |         } else { | ||||
|  |             x = Infinity; | ||||
|  |         } | ||||
|  |         return x; | ||||
|  |     }, | ||||
|  | 
 | ||||
|  |     "date-iso-asc": function(a, b) { | ||||
|  |         return a - b; | ||||
|  |     }, | ||||
|  | 
 | ||||
|  |     "date-iso-desc": function(a, b) { | ||||
|  |         return b - a; | ||||
|  |     } | ||||
|  | }); | ||||
|  | 
 | ||||
|  | const userOrgTypeDialog = document.getElementById("userOrgTypeDialog"); | ||||
|  | // Fill the form and title
 | ||||
|  | userOrgTypeDialog.addEventListener("show.bs.modal", function(event) { | ||||
|  |     // Get shared values
 | ||||
|  |     const userEmail = event.relatedTarget.parentNode.dataset.vwUserEmail; | ||||
|  |     const userUuid = event.relatedTarget.parentNode.dataset.vwUserUuid; | ||||
|  |     // Get org specific values
 | ||||
|  |     const userOrgType = event.relatedTarget.dataset.vwOrgType; | ||||
|  |     const userOrgTypeName = ORG_TYPES[userOrgType]["name"]; | ||||
|  |     const orgName = event.relatedTarget.dataset.vwOrgName; | ||||
|  |     const orgUuid = event.relatedTarget.dataset.vwOrgUuid; | ||||
|  | 
 | ||||
|  |     document.getElementById("userOrgTypeDialogTitle").innerHTML = `<b>Update User Type:</b><br><b>Organization:</b> ${orgName}<br><b>User:</b> ${userEmail}`; | ||||
|  |     document.getElementById("userOrgTypeUserUuid").value = userUuid; | ||||
|  |     document.getElementById("userOrgTypeOrgUuid").value = orgUuid; | ||||
|  |     document.getElementById(`userOrgType${userOrgTypeName}`).checked = true; | ||||
|  | }, false); | ||||
|  | 
 | ||||
|  | // Prevent accidental submission of the form with valid elements after the modal has been hidden.
 | ||||
|  | userOrgTypeDialog.addEventListener("hide.bs.modal", function() { | ||||
|  |     document.getElementById("userOrgTypeDialogTitle").innerHTML = ""; | ||||
|  |     document.getElementById("userOrgTypeUserUuid").value = ""; | ||||
|  |     document.getElementById("userOrgTypeOrgUuid").value = ""; | ||||
|  | }, false); | ||||
|  | 
 | ||||
|  | function updateUserOrgType() { | ||||
|  |     event.preventDefault(); | ||||
|  |     event.stopPropagation(); | ||||
|  | 
 | ||||
|  |     const data = JSON.stringify(Object.fromEntries(new FormData(event.target).entries())); | ||||
|  | 
 | ||||
|  |     _post(`${BASE_URL}/admin/users/org_type`, | ||||
|  |         "Updated organization type of the user successfully", | ||||
|  |         "Error updating organization type of the user", | ||||
|  |         data | ||||
|  |     ); | ||||
|  | } | ||||
|  | 
 | ||||
|  | // onLoad events
 | ||||
|  | document.addEventListener("DOMContentLoaded", (/*event*/) => { | ||||
|  |     jQuery("#users-table").DataTable({ | ||||
|  |         "stateSave": true, | ||||
|  |         "responsive": true, | ||||
|  |         "lengthMenu": [ | ||||
|  |             [-1, 5, 10, 25, 50], | ||||
|  |             ["All", 5, 10, 25, 50] | ||||
|  |         ], | ||||
|  |         "pageLength": -1, // Default show all
 | ||||
|  |         "columnDefs": [{ | ||||
|  |             "targets": [1, 2], | ||||
|  |             "type": "date-iso" | ||||
|  |         }, { | ||||
|  |             "targets": 6, | ||||
|  |             "searchable": false, | ||||
|  |             "orderable": false | ||||
|  |         }] | ||||
|  |     }); | ||||
|  | 
 | ||||
|  |     // Color all the org buttons per type
 | ||||
|  |     document.querySelectorAll("button[data-vw-org-type]").forEach(function(e) { | ||||
|  |         const orgType = ORG_TYPES[e.dataset.vwOrgType]; | ||||
|  |         e.style.backgroundColor = orgType.color; | ||||
|  |         e.title = orgType.name; | ||||
|  |     }); | ||||
|  | 
 | ||||
|  |     // Add click events for user actions
 | ||||
|  |     document.querySelectorAll("button[vw-remove2fa]").forEach(btn => { | ||||
|  |         btn.addEventListener("click", remove2fa); | ||||
|  |     }); | ||||
|  |     document.querySelectorAll("button[vw-deauth-user]").forEach(btn => { | ||||
|  |         btn.addEventListener("click", deauthUser); | ||||
|  |     }); | ||||
|  |     document.querySelectorAll("button[vw-delete-user]").forEach(btn => { | ||||
|  |         btn.addEventListener("click", deleteUser); | ||||
|  |     }); | ||||
|  |     document.querySelectorAll("button[vw-disable-user]").forEach(btn => { | ||||
|  |         btn.addEventListener("click", disableUser); | ||||
|  |     }); | ||||
|  |     document.querySelectorAll("button[vw-enable-user]").forEach(btn => { | ||||
|  |         btn.addEventListener("click", enableUser); | ||||
|  |     }); | ||||
|  | 
 | ||||
|  |     document.getElementById("updateRevisions").addEventListener("click", updateRevisions); | ||||
|  |     document.getElementById("reload").addEventListener("click", reload); | ||||
|  |     document.getElementById("userOrgTypeForm").addEventListener("submit", updateUserOrgType); | ||||
|  |     document.getElementById("inviteUserForm").addEventListener("submit", inviteUser); | ||||
|  | }); | ||||
					Loading…
					
					
				
		Reference in new issue