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