diff options
author | BlackDex <[email protected]> | 2022-12-28 20:05:10 +0100 |
---|---|---|
committer | Daniel GarcĂa <[email protected]> | 2023-01-09 19:11:26 +0100 |
commit | de26af0c2db3dd7d4a58229c6c5e1d5596f84913 (patch) | |
tree | 5bd32914d4d8aef43dd8b4487af1f4737ebb8454 /src/static | |
parent | 3f223a75141701cb67443542d66888dfe3618e20 (diff) | |
download | vaultwarden-de26af0c2db3dd7d4a58229c6c5e1d5596f84913.tar.gz vaultwarden-de26af0c2db3dd7d4a58229c6c5e1d5596f84913.zip |
Removed unsafe-inline JS from CSP and other fixes
- 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.
Diffstat (limited to 'src/static')
-rw-r--r-- | src/static/scripts/404.css | 26 | ||||
-rw-r--r-- | src/static/scripts/admin.css | 45 | ||||
-rw-r--r-- | src/static/scripts/admin.js | 65 | ||||
-rw-r--r-- | src/static/scripts/admin_diagnostics.js | 219 | ||||
-rw-r--r-- | src/static/scripts/admin_organizations.js | 54 | ||||
-rw-r--r-- | src/static/scripts/admin_settings.js | 180 | ||||
-rw-r--r-- | src/static/scripts/admin_users.js | 246 | ||||
-rw-r--r-- | src/static/scripts/bootstrap.css | 2 | ||||
-rw-r--r-- | src/static/templates/404.hbs | 28 | ||||
-rw-r--r-- | src/static/templates/admin/base.hbs | 96 | ||||
-rw-r--r-- | src/static/templates/admin/diagnostics.hbs | 207 | ||||
-rw-r--r-- | src/static/templates/admin/organizations.hbs | 45 | ||||
-rw-r--r-- | src/static/templates/admin/settings.hbs | 165 | ||||
-rw-r--r-- | src/static/templates/admin/users.hbs | 202 |
14 files changed, 901 insertions, 679 deletions
diff --git a/src/static/scripts/404.css b/src/static/scripts/404.css new file mode 100644 index 00000000..c1024d2b --- /dev/null +++ b/src/static/scripts/404.css @@ -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%; +}
\ No newline at end of file diff --git a/src/static/scripts/admin.css b/src/static/scripts/admin.css new file mode 100644 index 00000000..d77b5372 --- /dev/null +++ b/src/static/scripts/admin.css @@ -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; +}
\ No newline at end of file diff --git a/src/static/scripts/admin.js b/src/static/scripts/admin.js new file mode 100644 index 00000000..7849ac19 --- /dev/null +++ b/src/static/scripts/admin.js @@ -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"); + } +});
\ No newline at end of file diff --git a/src/static/scripts/admin_diagnostics.js b/src/static/scripts/admin_diagnostics.js new file mode 100644 index 00000000..84a7ecc5 --- /dev/null +++ b/src/static/scripts/admin_diagnostics.js @@ -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); +});
\ No newline at end of file diff --git a/src/static/scripts/admin_organizations.js b/src/static/scripts/admin_organizations.js new file mode 100644 index 00000000..ae15e2fd --- /dev/null +++ b/src/static/scripts/admin_organizations.js @@ -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); +});
\ No newline at end of file diff --git a/src/static/scripts/admin_settings.js b/src/static/scripts/admin_settings.js new file mode 100644 index 00000000..4f248cbd --- /dev/null +++ b/src/static/scripts/admin_settings.js @@ -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); +});
\ No newline at end of file diff --git a/src/static/scripts/admin_users.js b/src/static/scripts/admin_users.js new file mode 100644 index 00000000..8f7ddf20 --- /dev/null +++ b/src/static/scripts/admin_users.js @@ -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); +});
\ No newline at end of file diff --git a/src/static/scripts/bootstrap.css b/src/static/scripts/bootstrap.css index fa2da29b..614c226f 100644 --- a/src/static/scripts/bootstrap.css +++ b/src/static/scripts/bootstrap.css @@ -10874,5 +10874,3 @@ textarea.form-control-lg { display: none !important; } } - -/*# sourceMappingURL=bootstrap.css.map */
\ No newline at end of file diff --git a/src/static/templates/404.hbs b/src/static/templates/404.hbs index 230c30ca..064dc5a1 100644 --- a/src/static/templates/404.hbs +++ b/src/static/templates/404.hbs @@ -7,31 +7,7 @@ <link rel="icon" type="image/png" href="{{urlpath}}/vw_static/vaultwarden-favicon.png"> <title>Page not found!</title> <link rel="stylesheet" href="{{urlpath}}/vw_static/bootstrap.css" /> - <style> - 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; - } - </style> + <link rel="stylesheet" href="{{urlpath}}/vw_static/404.css" /> </head> <body class="bg-light"> @@ -53,7 +29,7 @@ <h2>Page not found!</h2> <p class="lead">Sorry, but the page you were looking for could not be found.</p> <p class="display-6"> - <a href="{{urlpath}}/"><img style="max-width: 500px; width: 100%;" src="{{urlpath}}/vw_static/404.png" alt="Return to the web vault?"></a></p> + <a href="{{urlpath}}/"><img class="vw-404" src="{{urlpath}}/vw_static/404.png" alt="Return to the web vault?"></a></p> <p>You can <a href="{{urlpath}}/">return to the web-vault</a>, or <a href="https://github.com/dani-garcia/vaultwarden">contact us</a>.</p> </main> diff --git a/src/static/templates/admin/base.hbs b/src/static/templates/admin/base.hbs index 23317e3c..e296b114 100644 --- a/src/static/templates/admin/base.hbs +++ b/src/static/templates/admin/base.hbs @@ -7,86 +7,9 @@ <link rel="icon" type="image/png" href="{{urlpath}}/vw_static/vaultwarden-favicon.png"> <title>Vaultwarden Admin Panel</title> <link rel="stylesheet" href="{{urlpath}}/vw_static/bootstrap.css" /> - <style> - 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); - } - </style> - <script> - 'use strict'; - - 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(); - } - async function sha256(message) { - // https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest - const msgUint8 = new TextEncoder().encode(message); - const hashBuffer = await crypto.subtle.digest('SHA-256', msgUint8); - const hashArray = Array.from(new Uint8Array(hashBuffer)); - const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); - return hashHex; - } - function toggleVis(input_id) { - const elem = document.getElementById(input_id); - const type = elem.getAttribute("type"); - if (type === "text") { - elem.setAttribute("type", "password"); - } else { - elem.setAttribute("type", "text"); - } - return false; - } - 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); } - }); - } - </script> + <link rel="stylesheet" href="{{urlpath}}/vw_static/admin.css" /> + <script src="{{urlpath}}/vw_static/admin.js"></script> </head> - <body class="bg-light"> <nav class="navbar navbar-expand-md navbar-dark bg-dark mb-4 shadow fixed-top"> <div class="container-xl"> @@ -126,21 +49,6 @@ {{> (lookup this "page_content") }} <!-- This script needs to be at the bottom, else it will fail! --> - <script> - 'use strict'; - - // get current URL path and assign 'active' class to the correct nav-item - (() => { - const pathname = window.location.pathname; - if (pathname === "") return; - let 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'); - } - })(); - </script> - <script src="{{urlpath}}/vw_static/jdenticon.js"></script> <script src="{{urlpath}}/vw_static/bootstrap-native.js"></script> </body> </html> diff --git a/src/static/templates/admin/diagnostics.hbs b/src/static/templates/admin/diagnostics.hbs index cb63eb4c..de83ae11 100644 --- a/src/static/templates/admin/diagnostics.hbs +++ b/src/static/templates/admin/diagnostics.hbs @@ -12,7 +12,7 @@ <span class="badge bg-info d-none" id="server-branch" title="This is a branched version.">Branched</span> </dt> <dd class="col-sm-7"> - <span id="server-installed">{{version}}</span> + <span id="server-installed">{{page_data.current_release}}</span> </dd> <dt class="col-sm-5">Server Latest <span class="badge bg-secondary d-none" id="server-failed" title="Unable to determine latest version.">Unknown</span> @@ -55,6 +55,10 @@ <div class="row"> <div class="col-md"> <dl class="row"> + <dt class="col-sm-5">OS/Arch</dt> + <dd class="col-sm-7"> + <span class="d-block"><b>{{ page_data.host_os }} / {{ page_data.host_arch }}</b></span> + </dd> <dt class="col-sm-5">Running within Docker</dt> <dd class="col-sm-7"> {{#if page_data.running_within_docker}} @@ -140,8 +144,8 @@ <span><b>Server:</b> {{page_data.server_time_local}}</span> </dd> <dt class="col-sm-5">Date & Time (UTC) - <span class="badge bg-success d-none" id="time-success" title="Server and browser times are within 30 seconds of each other.">Ok</span> - <span class="badge bg-danger d-none" id="time-warning" title="Server and browser times are more than 30 seconds apart.">Error</span> + <span class="badge bg-success d-none" id="time-success" title="Server and browser times are within 20 seconds of each other.">Ok</span> + <span class="badge bg-danger d-none" id="time-warning" title="Server and browser times are more than 20 seconds apart.">Error</span> </dt> <dd class="col-sm-7"> <span id="time-server" class="d-block"><b>Server:</b> <span id="time-server-string">{{page_data.server_time}}</span></span> @@ -180,10 +184,10 @@ </dl> <dl class="row"> <dt class="col-sm-3"> - <button type="button" id="gen-support" class="btn btn-primary" onclick="generateSupportString(); return false;">Generate Support String</button> + <button type="button" id="gen-support" class="btn btn-primary">Generate Support String</button> <br><br> - <button type="button" id="copy-support" class="btn btn-info mb-3 d-none" onclick="copyToClipboard(); return false;">Copy To Clipboard</button> - <div class="toast-container position-absolute float-start" style="width: 15rem;"> + <button type="button" id="copy-support" class="btn btn-info mb-3 d-none">Copy To Clipboard</button> + <div class="toast-container position-absolute float-start vw-copy-toast"> <div id="toastClipboardCopy" class="toast fade hide" role="status" aria-live="polite" aria-atomic="true" data-bs-autohide="true" data-bs-delay="1500"> <div class="toast-body"> Copied to clipboard! @@ -192,197 +196,12 @@ </div> </dt> <dd class="col-sm-9"> - <pre id="support-string" class="pre-scrollable d-none w-100 border p-2" style="height: 16rem;"></pre> + <pre id="support-string" class="pre-scrollable d-none w-100 border p-2"></pre> </dd> </dl> </div> </div> </div> </main> - -<script> - '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`; - document.getElementById("time-browser-string").innerText = browserUTC; - - const serverUTC = document.getElementById("time-server-string").innerText; - const timeDrift = ( - Date.parse(serverUTC.replace(' ', 'T').replace(' UTC', '')) - - Date.parse(browserUTC.replace(' ', 'T').replace(' UTC', '')) - ) / 1000; - if (timeDrift > 30 || timeDrift < -30) { - document.getElementById('time-warning').classList.remove('d-none'); - } else { - document.getElementById('time-success').classList.remove('d-none'); - timeCheck = true; - } - - // ================================ - // 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); - if (isValidIp(document.getElementById('dns-resolved').innerText)) { - document.getElementById('dns-success').classList.remove('d-none'); - dnsCheck = true; - } else { - document.getElementById('dns-warning').classList.remove('d-none'); - } - - // ================================ - // Version check for both vaultwarden and web-vault - let serverInstalled = document.getElementById('server-installed').innerText; - let serverLatest = document.getElementById('server-latest').innerText; - let serverLatestCommit = document.getElementById('server-latest-commit').innerText.replace('-', ''); - if (serverInstalled.indexOf('-') !== -1 && serverLatest !== '-' && serverLatestCommit !== '-') { - document.getElementById('server-latest-commit').classList.remove('d-none'); - } - - const webInstalled = document.getElementById('web-installed').innerText; - checkVersions('server', serverInstalled, serverLatest, serverLatestCommit); - - {{#unless page_data.running_within_docker}} - const webLatest = document.getElementById('web-latest').innerText; - checkVersions('web', webInstalled, webLatest); - {{/unless}} - - 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'); - } - } - } - - // ================================ - // Check valid DOMAIN configuration - document.getElementById('domain-browser-string').innerText = location.href.toLowerCase(); - if (document.getElementById('domain-server-string').innerText.toLowerCase() == location.href.toLowerCase()) { - 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 (document.getElementById('domain-server-string').innerText.toLowerCase().startsWith('https://') ) { - document.getElementById('https-success').classList.remove('d-none'); - httpsCheck = true; - } else { - document.getElementById('https-warning').classList.remove('d-none'); - } - })(); - - // ================================ - // Generate support string to be pasted on github or the forum - async function generateSupportString() { - let supportString = "### Your environment (Generated via diagnostics page)\n"; - - supportString += "* Vaultwarden version: v{{ version }}\n"; - supportString += "* Web-vault version: v{{ page_data.web_vault_version }}\n"; - supportString += "* Running within Docker: {{ page_data.running_within_docker }} (Base: {{ page_data.docker_base_image }})\n"; - supportString += "* Environment settings overridden: "; - {{#if page_data.overrides}} - supportString += "true\n" - {{else}} - supportString += "false\n" - {{/if}} - supportString += "* Uses a reverse proxy: {{ page_data.ip_header_exists }}\n"; - {{#if page_data.ip_header_exists}} - supportString += "* IP Header check: {{ page_data.ip_header_match }} ({{ page_data.ip_header_name }})\n"; - {{/if}} - supportString += "* Internet access: {{ page_data.has_http_access }}\n"; - supportString += "* Internet access via a proxy: {{ page_data.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: {{ page_data.db_type }}\n"; - supportString += "* Database version: {{ page_data.db_version }}\n"; - supportString += "* Clients used: \n"; - supportString += "* Reverse proxy and version: \n"; - supportString += "* Other relevant information: \n"; - - let jsonResponse = await fetch('{{urlpath}}/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:** {{page_data.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() { - 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(); - } -</script> +<script src="{{urlpath}}/vw_static/admin_diagnostics.js"></script> +<script type="application/json" id="diagnostics_json">{{to_json page_data}}</script> diff --git a/src/static/templates/admin/organizations.hbs b/src/static/templates/admin/organizations.hbs index 9762b189..eef6ae1a 100644 --- a/src/static/templates/admin/organizations.hbs +++ b/src/static/templates/admin/organizations.hbs @@ -9,7 +9,7 @@ <th>Users</th> <th>Items</th> <th>Attachments</th> - <th style="width: 130px; min-width: 130px;">Actions</th> + <th class="vw-actions">Actions</th> </tr> </thead> <tbody> @@ -21,7 +21,7 @@ <strong>{{Name}}</strong> <span class="me-2">({{BillingEmail}})</span> <span class="d-block"> - <span class="badge bg-success">{{Id}}</span> + <span class="badge bg-success font-monospace">{{Id}}</span> </span> </div> </td> @@ -38,49 +38,22 @@ {{/if}} </td> <td class="text-end px-0 small"> - <button type="button" class="btn btn-sm btn-link p-0 border-0" onclick='deleteOrganization({{jsesc Id}}, {{jsesc Name}}, {{jsesc BillingEmail}})'>Delete Organization</button> + <button type="button" class="btn btn-sm btn-link p-0 border-0" vw-delete-organization data-vw-org-uuid="{{jsesc Id no_quote}}" data-vw-org-name="{{jsesc Name no_quote}}" data-vw-billing-email="{{jsesc BillingEmail no_quote}}">Delete Organization</button> </td> </tr> {{/each}} </tbody> </table> </div> + + <div class="mt-3 clearfix"> + <button type="button" class="btn btn-sm btn-primary float-end" id="reload">Reload organizations</button> + </div> </div> </main> <link rel="stylesheet" href="{{urlpath}}/vw_static/datatables.css" /> <script src="{{urlpath}}/vw_static/jquery-3.6.2.slim.js"></script> <script src="{{urlpath}}/vw_static/datatables.js"></script> -<script> - 'use strict'; - - function deleteOrganization(id, name, billing_email) { - // First make sure the user wants to delete this organization - var continueDelete = confirm("WARNING: All data of this organization ("+ name +") will be lost!\nMake sure you have a backup, this cannot be undone!"); - if (continueDelete == true) { - var input_org_uuid = prompt("To delete the organization '" + name + " (" + billing_email +")', please type the organization uuid below.") - if (input_org_uuid != null) { - if (input_org_uuid == id) { - _post("{{urlpath}}/admin/organizations/" + id + "/delete", - "Organization deleted correctly", - "Error deleting organization"); - } else { - alert("Wrong organization uuid, please try again") - } - } - } - - return false; - } - - document.addEventListener("DOMContentLoaded", function() { - $('#orgs-table').DataTable({ - "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 } - ] - }); - }); -</script> +<script src="{{urlpath}}/vw_static/admin_organizations.js"></script> +<script src="{{urlpath}}/vw_static/jdenticon.js"></script> diff --git a/src/static/templates/admin/settings.hbs b/src/static/templates/admin/settings.hbs index fb9668ad..e3874335 100644 --- a/src/static/templates/admin/settings.hbs +++ b/src/static/templates/admin/settings.hbs @@ -8,8 +8,8 @@ Settings which are overridden are shown with <span class="is-overridden-true alert-row px-1">a yellow colored background</span>. </div> - <form class="form needs-validation" id="config-form" onsubmit="saveConfig(); return false;" novalidate> - {{#each config}} + <form class="form needs-validation" id="config-form" novalidate> + {{#each page_data.config}} {{#if groupdoc}} <div class="card bg-light mb-3"> <button id="b_{{group}}" type="button" class="card-header text-start btn btn-link text-decoration-none" aria-expanded="false" aria-controls="g_{{group}}" data-bs-toggle="collapse" data-bs-target="#g_{{group}}">{{groupdoc}}</button> @@ -24,7 +24,7 @@ <input class="form-control conf-{{type}}" id="input_{{name}}" type="{{type}}" name="{{name}}" value="{{value}}" {{#if default}} placeholder="Default: {{default}}"{{/if}}> {{#case type "password"}} - <button class="btn btn-outline-secondary input-group-text" type="button" onclick="toggleVis('input_{{name}}');">Show/hide</button> + <button class="btn btn-outline-secondary input-group-text" type="button" data-vw-pw-toggle="input_{{name}}">Show/hide</button> {{/case}} </div> </div> @@ -48,7 +48,7 @@ <label for="smtp-test-email" class="col-sm-3 col-form-label">Test SMTP</label> <div class="col-sm-8 input-group"> <input class="form-control" id="smtp-test-email" type="email" placeholder="Enter test email" required> - <button type="button" class="btn btn-outline-primary input-group-text" onclick="smtpTest(); return false;">Send test email</button> + <button type="button" class="btn btn-outline-primary input-group-text" id="smtpTest">Send test email</button> <div class="invalid-tooltip">Please provide a valid email address</div> </div> </div> @@ -68,7 +68,7 @@ launching the server. You can check the variable names in the tooltips of each option. </div> - {{#each config}} + {{#each page_data.config}} {{#each elements}} {{#unless editable}} <div class="row my-2 align-items-center alert-row" title="[{{name}}] {{doc.description}}"> @@ -83,11 +83,11 @@ --}} {{#if (eq name "database_url")}} <input readonly class="form-control" id="input_{{name}}" type="password" value="{{value}}" {{#if default}} placeholder="Default: {{default}}" {{/if}}> - <button class="btn btn-outline-secondary" type="button" onclick="toggleVis('input_{{name}}');">Show/hide</button> + <button class="btn btn-outline-secondary" type="button" data-vw-pw-toggle="input_{{name}}">Show/hide</button> {{else}} <input readonly class="form-control" id="input_{{name}}" type="{{type}}" value="{{value}}" {{#if default}} placeholder="Default: {{default}}" {{/if}}> {{#case type "password"}} - <button class="btn btn-outline-secondary" type="button" onclick="toggleVis('input_{{name}}');">Show/hide</button> + <button class="btn btn-outline-secondary" type="button" data-vw-pw-toggle="input_{{name}}">Show/hide</button> {{/case}} {{/if}} </div> @@ -112,7 +112,7 @@ </div> </div> - {{#if can_backup}} + {{#if page_data.can_backup}} <div class="card bg-light mb-3"> <button id="b_database" type="button" class="card-header text-start btn btn-link text-decoration-none" aria-expanded="false" aria-controls="g_database" data-bs-toggle="collapse" data-bs-target="#g_database">Backup Database</button> @@ -124,18 +124,17 @@ how to perform complete backups, refer to the wiki page on <a href="https://github.com/dani-garcia/vaultwarden/wiki/Backing-up-your-vault" target="_blank" rel="noopener noreferrer">backups</a>. </div> - <button type="button" class="btn btn-primary" onclick="backupDatabase();">Backup Database</button> + <button type="button" class="btn btn-primary" id="backupDatabase">Backup Database</button> </div> </div> {{/if}} <button type="submit" class="btn btn-primary">Save</button> - <button type="button" class="btn btn-danger float-end" onclick="deleteConf();">Reset defaults</button> + <button type="button" class="btn btn-danger float-end" id="deleteConf">Reset defaults</button> </form> </div> </div> </main> - <style> #config-block ::placeholder { /* Most modern browsers support this now. */ @@ -148,146 +147,4 @@ --bs-alert-border-color: #ffecb5; } </style> - -<script> - 'use strict'; - - function smtpTest() { - if (formHasChanges(config_form)) { - event.preventDefault(); - event.stopPropagation(); - alert("Config has been changed but not yet saved.\nPlease save the changes first before sending a test email."); - return false; - } - - let 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'); - event.preventDefault(); - event.stopPropagation(); - return false; - } - - const data = JSON.stringify({ "email": test_email.value }); - _post("{{urlpath}}/admin/test/smtp/", - "SMTP Test email sent correctly", - "Error sending SMTP test email", data, false); - return 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("{{urlpath}}/admin/config/", "Config saved correctly", - "Error saving config", data); - return false; - } - function deleteConf() { - var 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("{{urlpath}}/admin/config/delete", - "Config deleted correctly", - "Error deleting config"); - } else { - alert("Wrong input, please try again") - } - - return false; - } - function backupDatabase() { - _post("{{urlpath}}/admin/config/backup_db", - "Backup created successfully", - "Error creating backup", null, false); - return false; - } - 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); - } - - {{#each config}} {{#if grouptoggle}} - masterCheck("input_{{grouptoggle}}", "#g_{{group}} input"); - {{/if}} {{/each}} - - // 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) { - let key = e.charCode || e.keyCode || 0; - if (key == 13) { - e.preventDefault(); - } - } - } - - // Initialize Form Change Detection - const config_form = document.getElementById('config-form'); - 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); - - // 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) { - let key = e.charCode || e.keyCode || 0; - if (key == 13) { - e.preventDefault(); - smtpTest(); - } - } - } - submitTestEmailOnEnter(); - - // 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' - } - }); - } - colorRiskSettings(); - -</script> +<script src="{{urlpath}}/vw_static/admin_settings.js"></script> diff --git a/src/static/templates/admin/users.hbs b/src/static/templates/admin/users.hbs index 5825720f..3dbee11c 100644 --- a/src/static/templates/admin/users.hbs +++ b/src/static/templates/admin/users.hbs @@ -1,18 +1,17 @@ <main class="container-xl"> <div id="users-block" class="my-3 p-3 bg-white rounded shadow"> <h6 class="border-bottom pb-2 mb-3">Registered Users</h6> - <div class="table-responsive-xl small"> <table id="users-table" class="table table-sm table-striped table-hover"> <thead> <tr> <th>User</th> - <th style="width: 85px; min-width: 70px;">Created at</th> - <th style="width: 85px; min-width: 70px;">Last Active</th> - <th style="width: 35px; min-width: 35px;">Items</th> - <th>Attachments</th> - <th style="min-width: 120px;">Organizations</th> - <th style="width: 130px; min-width: 130px;">Actions</th> + <th class="vw-created-at">Created at</th> + <th class="vw-last-active">Last Active</th> + <th class="vw-items">Items</th> + <th class="vw-attachments">Attachments</th> + <th class="vw-organizations">Organizations</th> + <th class="vw-actions">Actions</th> </tr> </thead> <tbody> @@ -55,23 +54,25 @@ {{/if}} </td> <td> - <div class="overflow-auto" style="max-height: 120px;"> + <div class="overflow-auto vw-org-cell" data-vw-user-email="{{jsesc Email no_quote}}" data-vw-user-uuid="{{jsesc Id no_quote}}"> {{#each Organizations}} - <button class="badge" data-bs-toggle="modal" data-bs-target="#userOrgTypeDialog" data-orgtype="{{Type}}" data-orguuid="{{jsesc Id no_quote}}" data-orgname="{{jsesc Name no_quote}}" data-useremail="{{jsesc ../Email no_quote}}" data-useruuid="{{jsesc ../Id no_quote}}">{{Name}}</button> + <button class="badge" data-bs-toggle="modal" data-bs-target="#userOrgTypeDialog" data-vw-org-type="{{Type}}" data-vw-org-uuid="{{jsesc Id no_quote}}" data-vw-org-name="{{jsesc Name no_quote}}">{{Name}}</button> {{/each}} </div> </td> <td class="text-end px-0 small"> - {{#if TwoFactorEnabled}} - <button type="button" class="btn btn-sm btn-link p-0 border-0" onclick='remove2fa({{jsesc Id}})'>Remove all 2FA</button> - {{/if}} - <button type="button" class="btn btn-sm btn-link p-0 border-0" onclick='deauthUser({{jsesc Id}})'>Deauthorize sessions</button> - <button type="button" class="btn btn-sm btn-link p-0 border-0" onclick='deleteUser({{jsesc Id}}, {{jsesc Email}})'>Delete User</button> - {{#if user_enabled}} - <button type="button" class="btn btn-sm btn-link p-0 border-0" onclick='disableUser({{jsesc Id}}, {{jsesc Email}})'>Disable User</button> - {{else}} - <button type="button" class="btn btn-sm btn-link p-0 border-0" onclick='enableUser({{jsesc Id}}, {{jsesc Email}})'>Enable User</button> - {{/if}} + <span data-vw-user-uuid="{{jsesc Id no_quote}}" data-vw-user-email="{{jsesc Email no_quote}}"> + {{#if TwoFactorEnabled}} + <button type="button" class="btn btn-sm btn-link p-0 border-0" vw-remove2fa>Remove all 2FA</button> + {{/if}} + <button type="button" class="btn btn-sm btn-link p-0 border-0" vw-deauth-user>Deauthorize sessions</button> + <button type="button" class="btn btn-sm btn-link p-0 border-0" vw-delete-user>Delete User</button> + {{#if user_enabled}} + <button type="button" class="btn btn-sm btn-link p-0 border-0" vw-disable-user>Disable User</button> + {{else}} + <button type="button" class="btn btn-sm btn-link p-0 border-0" vw-enable-user>Enable User</button> + {{/if}} + </span> </td> </tr> {{/each}} @@ -79,23 +80,23 @@ </table> </div> - <div class="mt-3"> - <button type="button" class="btn btn-sm btn-danger" onclick="updateRevisions();" + <div class="mt-3 clearfix"> + <button type="button" class="btn btn-sm btn-danger" id="updateRevisions" title="Force all clients to fetch new data next time they connect. Useful after restoring a backup to remove any stale data."> Force clients to resync </button> - <button type="button" class="btn btn-sm btn-primary float-end" onclick="reload();">Reload users</button> + <button type="button" class="btn btn-sm btn-primary float-end" id="reload">Reload users</button> </div> </div> - <div id="invite-form-block" class="align-items-center p-3 mb-3 text-white-50 bg-secondary rounded shadow"> + <div id="inviteUserFormBlock" class="align-items-center p-3 mb-3 text-white-50 bg-secondary rounded shadow"> <div> <h6 class="mb-0 text-white">Invite User</h6> <small>Email:</small> - <form class="form-inline input-group w-50" id="invite-form" onsubmit="inviteUser(); return false;"> - <input type="email" class="form-control me-2" id="email-invite" placeholder="Enter email" required> + <form class="form-inline input-group w-50" id="inviteUserForm"> + <input type="email" class="form-control me-2" id="inviteEmail" placeholder="Enter email" required> <button type="submit" class="btn btn-primary">Invite</button> </form> </div> @@ -108,7 +109,7 @@ <h6 class="modal-title" id="userOrgTypeDialogTitle"></h6> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> </div> - <form class="form" id="userOrgTypeForm" onsubmit="updateUserOrgType(); return false;"> + <form class="form" id="userOrgTypeForm"> <input type="hidden" name="user_uuid" id="userOrgTypeUserUuid" value=""> <input type="hidden" name="org_uuid" id="userOrgTypeOrgUuid" value=""> <div class="modal-body"> @@ -138,150 +139,5 @@ <link rel="stylesheet" href="{{urlpath}}/vw_static/datatables.css" /> <script src="{{urlpath}}/vw_static/jquery-3.6.2.slim.js"></script> <script src="{{urlpath}}/vw_static/datatables.js"></script> -<script> - 'use strict'; - - function deleteUser(id, mail) { - var input_mail = prompt("To delete user '" + mail + "', please type the email below") - if (input_mail != null) { - if (input_mail == mail) { - _post("{{urlpath}}/admin/users/" + id + "/delete", - "User deleted correctly", - "Error deleting user"); - } else { - alert("Wrong email, please try again") - } - } - return false; - } - function remove2fa(id) { - _post("{{urlpath}}/admin/users/" + id + "/remove-2fa", - "2FA removed correctly", - "Error removing 2FA"); - return false; - } - function deauthUser(id) { - _post("{{urlpath}}/admin/users/" + id + "/deauth", - "Sessions deauthorized correctly", - "Error deauthorizing sessions"); - return false; - } - function disableUser(id, mail) { - var confirmed = confirm("Are you sure you want to disable user '" + mail + "'? This will also deauthorize their sessions.") - if (confirmed) { - _post("{{urlpath}}/admin/users/" + id + "/disable", - "User disabled successfully", - "Error disabling user"); - } - return false; - } - function enableUser(id, mail) { - var confirmed = confirm("Are you sure you want to enable user '" + mail + "'?") - if (confirmed) { - _post("{{urlpath}}/admin/users/" + id + "/enable", - "User enabled successfully", - "Error enabling user"); - } - return false; - } - function updateRevisions() { - _post("{{urlpath}}/admin/users/update_revision", - "Success, clients will sync next time they connect", - "Error forcing clients to sync"); - return false; - } - function inviteUser() { - const inv = document.getElementById("email-invite"); - const data = JSON.stringify({ "email": inv.value }); - inv.value = ""; - _post("{{urlpath}}/admin/invite/", "User invited correctly", - "Error inviting user", data); - return false; - } - - let OrgTypes = { - "0": { "name": "Owner", "color": "orange" }, - "1": { "name": "Admin", "color": "blueviolet" }, - "2": { "name": "User", "color": "blue" }, - "3": { "name": "Manager", "color": "green" }, - }; - - document.querySelectorAll("[data-orgtype]").forEach(function (e) { - let orgtype = OrgTypes[e.dataset.orgtype]; - e.style.backgroundColor = orgtype.color; - e.title = orgtype.name; - }); - - // Special sort function to sort dates in ISO format - jQuery.extend( jQuery.fn.dataTableExt.oSort, { - "date-iso-pre": function ( a ) { - let x; - let sortDate = a.replace(/(<([^>]+)>)/gi, "").trim(); - if ( sortDate !== '' ) { - let dtParts = sortDate.split(' '); - var timeParts = (undefined != dtParts[1]) ? dtParts[1].split(':') : ['00','00','00']; - var 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; - } - }); - - document.addEventListener("DOMContentLoaded", function() { - $('#users-table').DataTable({ - "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 } - ] - }); - }); - - var userOrgTypeDialog = document.getElementById('userOrgTypeDialog'); - // Fill the form and title - userOrgTypeDialog.addEventListener('show.bs.modal', function(event){ - let userOrgType = event.relatedTarget.getAttribute("data-orgtype"); - let userOrgTypeName = OrgTypes[userOrgType]["name"]; - let orgName = event.relatedTarget.getAttribute("data-orgname"); - let userEmail = event.relatedTarget.getAttribute("data-useremail"); - let orgUuid = event.relatedTarget.getAttribute("data-orguuid"); - let userUuid = event.relatedTarget.getAttribute("data-useruuid"); - - 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() { - let orgForm = document.getElementById("userOrgTypeForm"); - const data = JSON.stringify(Object.fromEntries(new FormData(orgForm).entries())); - - _post("{{urlpath}}/admin/users/org_type", - "Updated organization type of the user successfully", - "Error updating organization type of the user", data); - return false; - } -</script> +<script src="{{urlpath}}/vw_static/admin_users.js"></script> +<script src="{{urlpath}}/vw_static/jdenticon.js"></script> |