summaryrefslogtreecommitdiff
path: root/src/static
diff options
context:
space:
mode:
authorBlackDex <[email protected]>2022-12-28 20:05:10 +0100
committerDaniel GarcĂ­a <[email protected]>2023-01-09 19:11:26 +0100
commitde26af0c2db3dd7d4a58229c6c5e1d5596f84913 (patch)
tree5bd32914d4d8aef43dd8b4487af1f4737ebb8454 /src/static
parent3f223a75141701cb67443542d66888dfe3618e20 (diff)
downloadvaultwarden-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.css26
-rw-r--r--src/static/scripts/admin.css45
-rw-r--r--src/static/scripts/admin.js65
-rw-r--r--src/static/scripts/admin_diagnostics.js219
-rw-r--r--src/static/scripts/admin_organizations.js54
-rw-r--r--src/static/scripts/admin_settings.js180
-rw-r--r--src/static/scripts/admin_users.js246
-rw-r--r--src/static/scripts/bootstrap.css2
-rw-r--r--src/static/templates/404.hbs28
-rw-r--r--src/static/templates/admin/base.hbs96
-rw-r--r--src/static/templates/admin/diagnostics.hbs207
-rw-r--r--src/static/templates/admin/organizations.hbs45
-rw-r--r--src/static/templates/admin/settings.hbs165
-rw-r--r--src/static/templates/admin/users.hbs202
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>