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