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