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