mirror of
https://github.com/dani-garcia/vaultwarden.git
synced 2024-12-26 07:05:57 -05:00
Removed unsafe-inline JS from CSP and other fixes
- Removed `unsafe-inline` for javascript from CSP. The admin interface now uses files instead of inline javascript. - Modified javascript to work not being inline. - Run eslint over javascript and fixed some items. - Added a `to_json` Handlebars helper. Used at the diagnostics page. - Changed `AdminTemplateData` struct to be smaller. The `config` was always added, but only used at one page. Same goes for `can_backup` and `version`. - Also inlined CSS. We can't remove the `unsafe-inline` from css, because that seems to break the web-vault currently. That might need some further checks. But for now the 404 page and all the admin pages are clear of inline scripts and styles.
This commit is contained in:
parent
7c739dd58e
commit
04e02d7f9f
@ -144,7 +144,6 @@ fn render_admin_login(msg: Option<&str>, redirect: Option<String>) -> ApiResult<
|
||||
let msg = msg.map(|msg| format!("Error: {msg}"));
|
||||
let json = json!({
|
||||
"page_content": "admin/login",
|
||||
"version": VERSION,
|
||||
"error": msg,
|
||||
"redirect": redirect,
|
||||
"urlpath": CONFIG.domain_path()
|
||||
@ -208,34 +207,16 @@ fn _validate_token(token: &str) -> bool {
|
||||
#[derive(Serialize)]
|
||||
struct AdminTemplateData {
|
||||
page_content: String,
|
||||
version: Option<&'static str>,
|
||||
page_data: Option<Value>,
|
||||
config: Value,
|
||||
can_backup: bool,
|
||||
logged_in: bool,
|
||||
urlpath: String,
|
||||
}
|
||||
|
||||
impl AdminTemplateData {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
page_content: String::from("admin/settings"),
|
||||
version: VERSION,
|
||||
config: CONFIG.prepare_json(),
|
||||
can_backup: *CAN_BACKUP,
|
||||
logged_in: true,
|
||||
urlpath: CONFIG.domain_path(),
|
||||
page_data: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn with_data(page_content: &str, page_data: Value) -> Self {
|
||||
fn new(page_content: &str, page_data: Value) -> Self {
|
||||
Self {
|
||||
page_content: String::from(page_content),
|
||||
version: VERSION,
|
||||
page_data: Some(page_data),
|
||||
config: CONFIG.prepare_json(),
|
||||
can_backup: *CAN_BACKUP,
|
||||
logged_in: true,
|
||||
urlpath: CONFIG.domain_path(),
|
||||
}
|
||||
@ -247,7 +228,11 @@ impl AdminTemplateData {
|
||||
}
|
||||
|
||||
fn render_admin_page() -> ApiResult<Html<String>> {
|
||||
let text = AdminTemplateData::new().render()?;
|
||||
let settings_json = json!({
|
||||
"config": CONFIG.prepare_json(),
|
||||
"can_backup": *CAN_BACKUP,
|
||||
});
|
||||
let text = AdminTemplateData::new("admin/settings", settings_json).render()?;
|
||||
Ok(Html(text))
|
||||
}
|
||||
|
||||
@ -342,7 +327,7 @@ async fn users_overview(_token: AdminToken, mut conn: DbConn) -> ApiResult<Html<
|
||||
users_json.push(usr);
|
||||
}
|
||||
|
||||
let text = AdminTemplateData::with_data("admin/users", json!(users_json)).render()?;
|
||||
let text = AdminTemplateData::new("admin/users", json!(users_json)).render()?;
|
||||
Ok(Html(text))
|
||||
}
|
||||
|
||||
@ -442,7 +427,7 @@ async fn update_user_org_type(
|
||||
};
|
||||
|
||||
if user_to_edit.atype == UserOrgType::Owner && new_type != UserOrgType::Owner {
|
||||
// Removing owner permmission, check that there is at least one other confirmed owner
|
||||
// Removing owner permission, check that there is at least one other confirmed owner
|
||||
if UserOrganization::count_confirmed_by_org_and_type(&data.org_uuid, UserOrgType::Owner, &mut conn).await <= 1 {
|
||||
err!("Can't change the type of the last owner")
|
||||
}
|
||||
@ -494,7 +479,7 @@ async fn organizations_overview(_token: AdminToken, mut conn: DbConn) -> ApiResu
|
||||
organizations_json.push(org);
|
||||
}
|
||||
|
||||
let text = AdminTemplateData::with_data("admin/organizations", json!(organizations_json)).render()?;
|
||||
let text = AdminTemplateData::new("admin/organizations", json!(organizations_json)).render()?;
|
||||
Ok(Html(text))
|
||||
}
|
||||
|
||||
@ -617,13 +602,14 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, mut conn: DbConn)
|
||||
|
||||
let diagnostics_json = json!({
|
||||
"dns_resolved": dns_resolved,
|
||||
"current_release": VERSION,
|
||||
"latest_release": latest_release,
|
||||
"latest_commit": latest_commit,
|
||||
"web_vault_enabled": &CONFIG.web_vault_enabled(),
|
||||
"web_vault_version": web_vault_version.version,
|
||||
"latest_web_build": latest_web_build,
|
||||
"running_within_docker": running_within_docker,
|
||||
"docker_base_image": docker_base_image(),
|
||||
"docker_base_image": if running_within_docker { docker_base_image() } else { "Not applicable" },
|
||||
"has_http_access": has_http_access,
|
||||
"ip_header_exists": &ip_header.0.is_some(),
|
||||
"ip_header_match": ip_header_name == CONFIG.ip_header(),
|
||||
@ -634,11 +620,13 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, mut conn: DbConn)
|
||||
"db_version": get_sql_server_version(&mut conn).await,
|
||||
"admin_url": format!("{}/diagnostics", admin_url()),
|
||||
"overrides": &CONFIG.get_overrides().join(", "),
|
||||
"host_arch": std::env::consts::ARCH,
|
||||
"host_os": std::env::consts::OS,
|
||||
"server_time_local": Local::now().format("%Y-%m-%d %H:%M:%S %Z").to_string(),
|
||||
"server_time": Utc::now().format("%Y-%m-%d %H:%M:%S UTC").to_string(), // Run the date/time check as the last item to minimize the difference
|
||||
});
|
||||
|
||||
let text = AdminTemplateData::with_data("admin/diagnostics", diagnostics_json).render()?;
|
||||
let text = AdminTemplateData::new("admin/diagnostics", diagnostics_json).render()?;
|
||||
Ok(Html(text))
|
||||
}
|
||||
|
||||
|
@ -102,6 +102,17 @@ pub fn static_files(filename: String) -> Result<(ContentType, &'static [u8]), Er
|
||||
"hibp.png" => Ok((ContentType::PNG, include_bytes!("../static/images/hibp.png"))),
|
||||
"vaultwarden-icon.png" => Ok((ContentType::PNG, include_bytes!("../static/images/vaultwarden-icon.png"))),
|
||||
"vaultwarden-favicon.png" => Ok((ContentType::PNG, include_bytes!("../static/images/vaultwarden-favicon.png"))),
|
||||
"404.css" => Ok((ContentType::CSS, include_bytes!("../static/scripts/404.css"))),
|
||||
"admin.css" => Ok((ContentType::CSS, include_bytes!("../static/scripts/admin.css"))),
|
||||
"admin.js" => Ok((ContentType::JavaScript, include_bytes!("../static/scripts/admin.js"))),
|
||||
"admin_settings.js" => Ok((ContentType::JavaScript, include_bytes!("../static/scripts/admin_settings.js"))),
|
||||
"admin_users.js" => Ok((ContentType::JavaScript, include_bytes!("../static/scripts/admin_users.js"))),
|
||||
"admin_organizations.js" => {
|
||||
Ok((ContentType::JavaScript, include_bytes!("../static/scripts/admin_organizations.js")))
|
||||
}
|
||||
"admin_diagnostics.js" => {
|
||||
Ok((ContentType::JavaScript, include_bytes!("../static/scripts/admin_diagnostics.js")))
|
||||
}
|
||||
"bootstrap.css" => Ok((ContentType::CSS, include_bytes!("../static/scripts/bootstrap.css"))),
|
||||
"bootstrap-native.js" => Ok((ContentType::JavaScript, include_bytes!("../static/scripts/bootstrap-native.js"))),
|
||||
"jdenticon.js" => Ok((ContentType::JavaScript, include_bytes!("../static/scripts/jdenticon.js"))),
|
||||
|
@ -1099,6 +1099,7 @@ where
|
||||
// Register helpers
|
||||
hb.register_helper("case", Box::new(case_helper));
|
||||
hb.register_helper("jsesc", Box::new(js_escape_helper));
|
||||
hb.register_helper("to_json", Box::new(to_json));
|
||||
|
||||
macro_rules! reg {
|
||||
($name:expr) => {{
|
||||
@ -1196,3 +1197,17 @@ fn js_escape_helper<'reg, 'rc>(
|
||||
out.write(&escaped_value)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn to_json<'reg, 'rc>(
|
||||
h: &Helper<'reg, 'rc>,
|
||||
_r: &'reg Handlebars<'_>,
|
||||
_ctx: &'rc Context,
|
||||
_rc: &mut RenderContext<'reg, 'rc>,
|
||||
out: &mut dyn Output,
|
||||
) -> HelperResult {
|
||||
let param = h.param(0).ok_or_else(|| RenderError::new("Expected 1 parameter for \"to_json\""))?.value();
|
||||
let json = serde_json::to_string(param)
|
||||
.map_err(|e| RenderError::new(format!("Can't serialize parameter to JSON: {}", e)))?;
|
||||
out.write(&json)?;
|
||||
Ok(())
|
||||
}
|
||||
|
26
src/static/scripts/404.css
vendored
Normal file
26
src/static/scripts/404.css
vendored
Normal file
@ -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%;
|
||||
}
|
45
src/static/scripts/admin.css
vendored
Normal file
45
src/static/scripts/admin.css
vendored
Normal file
@ -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;
|
||||
}
|
65
src/static/scripts/admin.js
vendored
Normal file
65
src/static/scripts/admin.js
vendored
Normal file
@ -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");
|
||||
}
|
||||
});
|
219
src/static/scripts/admin_diagnostics.js
vendored
Normal file
219
src/static/scripts/admin_diagnostics.js
vendored
Normal file
@ -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);
|
||||
});
|
54
src/static/scripts/admin_organizations.js
vendored
Normal file
54
src/static/scripts/admin_organizations.js
vendored
Normal file
@ -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);
|
||||
});
|
180
src/static/scripts/admin_settings.js
vendored
Normal file
180
src/static/scripts/admin_settings.js
vendored
Normal file
@ -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);
|
||||
});
|
246
src/static/scripts/admin_users.js
vendored
Normal file
246
src/static/scripts/admin_users.js
vendored
Normal file
@ -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);
|
||||
});
|
2
src/static/scripts/bootstrap.css
vendored
2
src/static/scripts/bootstrap.css
vendored
@ -10874,5 +10874,3 @@ textarea.form-control-lg {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=bootstrap.css.map */
|
@ -7,31 +7,7 @@
|
||||
<link rel="icon" type="image/png" href="{{urlpath}}/vw_static/vaultwarden-favicon.png">
|
||||
<title>Page not found!</title>
|
||||
<link rel="stylesheet" href="{{urlpath}}/vw_static/bootstrap.css" />
|
||||
<style>
|
||||
body {
|
||||
padding-top: 75px;
|
||||
}
|
||||
.vaultwarden-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
height: 32px;
|
||||
width: auto;
|
||||
margin: -5px 0 0 0;
|
||||
}
|
||||
.footer {
|
||||
padding: 40px 0 40px 0;
|
||||
border-top: 1px solid #dee2e6;
|
||||
}
|
||||
.container {
|
||||
max-width: 980px;
|
||||
}
|
||||
.content {
|
||||
padding-top: 20px;
|
||||
padding-bottom: 20px;
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="{{urlpath}}/vw_static/404.css" />
|
||||
</head>
|
||||
|
||||
<body class="bg-light">
|
||||
@ -53,7 +29,7 @@
|
||||
<h2>Page not found!</h2>
|
||||
<p class="lead">Sorry, but the page you were looking for could not be found.</p>
|
||||
<p class="display-6">
|
||||
<a href="{{urlpath}}/"><img style="max-width: 500px; width: 100%;" src="{{urlpath}}/vw_static/404.png" alt="Return to the web vault?"></a></p>
|
||||
<a href="{{urlpath}}/"><img class="vw-404" src="{{urlpath}}/vw_static/404.png" alt="Return to the web vault?"></a></p>
|
||||
<p>You can <a href="{{urlpath}}/">return to the web-vault</a>, or <a href="https://github.com/dani-garcia/vaultwarden">contact us</a>.</p>
|
||||
</main>
|
||||
|
||||
|
@ -7,86 +7,9 @@
|
||||
<link rel="icon" type="image/png" href="{{urlpath}}/vw_static/vaultwarden-favicon.png">
|
||||
<title>Vaultwarden Admin Panel</title>
|
||||
<link rel="stylesheet" href="{{urlpath}}/vw_static/bootstrap.css" />
|
||||
<style>
|
||||
body {
|
||||
padding-top: 75px;
|
||||
}
|
||||
img {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
.vaultwarden-icon {
|
||||
height: 32px;
|
||||
width: auto;
|
||||
margin: -5px 0 0 0;
|
||||
}
|
||||
/* Special alert-row class to use Bootstrap v5.2+ variable colors */
|
||||
.alert-row {
|
||||
--bs-alert-border: 1px solid var(--bs-alert-border-color);
|
||||
color: var(--bs-alert-color);
|
||||
background-color: var(--bs-alert-bg);
|
||||
border: var(--bs-alert-border);
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
'use strict';
|
||||
|
||||
function reload() {
|
||||
// Reload the page by setting the exact same href
|
||||
// Using window.location.reload() could cause a repost.
|
||||
window.location = window.location.href;
|
||||
}
|
||||
function msg(text, reload_page = true) {
|
||||
text && alert(text);
|
||||
reload_page && reload();
|
||||
}
|
||||
async function sha256(message) {
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest
|
||||
const msgUint8 = new TextEncoder().encode(message);
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-256', msgUint8);
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
return hashHex;
|
||||
}
|
||||
function toggleVis(input_id) {
|
||||
const elem = document.getElementById(input_id);
|
||||
const type = elem.getAttribute("type");
|
||||
if (type === "text") {
|
||||
elem.setAttribute("type", "password");
|
||||
} else {
|
||||
elem.setAttribute("type", "text");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function _post(url, successMsg, errMsg, body, reload_page = true) {
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
body: body,
|
||||
mode: "same-origin",
|
||||
credentials: "same-origin",
|
||||
headers: { "Content-Type": "application/json" }
|
||||
}).then( resp => {
|
||||
if (resp.ok) { msg(successMsg, reload_page); return Promise.reject({error: false}); }
|
||||
const respStatus = resp.status;
|
||||
const respStatusText = resp.statusText;
|
||||
return resp.text();
|
||||
}).then( respText => {
|
||||
try {
|
||||
const respJson = JSON.parse(respText);
|
||||
return respJson ? respJson.ErrorModel.Message : "Unknown error";
|
||||
} catch (e) {
|
||||
return Promise.reject({body:respStatus + ' - ' + respStatusText, error: true});
|
||||
}
|
||||
}).then( apiMsg => {
|
||||
msg(errMsg + "\n" + apiMsg, reload_page);
|
||||
}).catch( e => {
|
||||
if (e.error === false) { return true; }
|
||||
else { msg(errMsg + "\n" + e.body, reload_page); }
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<link rel="stylesheet" href="{{urlpath}}/vw_static/admin.css" />
|
||||
<script src="{{urlpath}}/vw_static/admin.js"></script>
|
||||
</head>
|
||||
|
||||
<body class="bg-light">
|
||||
<nav class="navbar navbar-expand-md navbar-dark bg-dark mb-4 shadow fixed-top">
|
||||
<div class="container-xl">
|
||||
@ -126,21 +49,6 @@
|
||||
{{> (lookup this "page_content") }}
|
||||
|
||||
<!-- This script needs to be at the bottom, else it will fail! -->
|
||||
<script>
|
||||
'use strict';
|
||||
|
||||
// get current URL path and assign 'active' class to the correct nav-item
|
||||
(() => {
|
||||
const pathname = window.location.pathname;
|
||||
if (pathname === "") return;
|
||||
let navItem = document.querySelectorAll('.navbar-nav .nav-item a[href="'+pathname+'"]');
|
||||
if (navItem.length === 1) {
|
||||
navItem[0].className = navItem[0].className + ' active';
|
||||
navItem[0].setAttribute('aria-current', 'page');
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<script src="{{urlpath}}/vw_static/jdenticon.js"></script>
|
||||
<script src="{{urlpath}}/vw_static/bootstrap-native.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -12,7 +12,7 @@
|
||||
<span class="badge bg-info d-none" id="server-branch" title="This is a branched version.">Branched</span>
|
||||
</dt>
|
||||
<dd class="col-sm-7">
|
||||
<span id="server-installed">{{version}}</span>
|
||||
<span id="server-installed">{{page_data.current_release}}</span>
|
||||
</dd>
|
||||
<dt class="col-sm-5">Server Latest
|
||||
<span class="badge bg-secondary d-none" id="server-failed" title="Unable to determine latest version.">Unknown</span>
|
||||
@ -55,6 +55,10 @@
|
||||
<div class="row">
|
||||
<div class="col-md">
|
||||
<dl class="row">
|
||||
<dt class="col-sm-5">OS/Arch</dt>
|
||||
<dd class="col-sm-7">
|
||||
<span class="d-block"><b>{{ page_data.host_os }} / {{ page_data.host_arch }}</b></span>
|
||||
</dd>
|
||||
<dt class="col-sm-5">Running within Docker</dt>
|
||||
<dd class="col-sm-7">
|
||||
{{#if page_data.running_within_docker}}
|
||||
@ -140,8 +144,8 @@
|
||||
<span><b>Server:</b> {{page_data.server_time_local}}</span>
|
||||
</dd>
|
||||
<dt class="col-sm-5">Date & Time (UTC)
|
||||
<span class="badge bg-success d-none" id="time-success" title="Server and browser times are within 30 seconds of each other.">Ok</span>
|
||||
<span class="badge bg-danger d-none" id="time-warning" title="Server and browser times are more than 30 seconds apart.">Error</span>
|
||||
<span class="badge bg-success d-none" id="time-success" title="Server and browser times are within 20 seconds of each other.">Ok</span>
|
||||
<span class="badge bg-danger d-none" id="time-warning" title="Server and browser times are more than 20 seconds apart.">Error</span>
|
||||
</dt>
|
||||
<dd class="col-sm-7">
|
||||
<span id="time-server" class="d-block"><b>Server:</b> <span id="time-server-string">{{page_data.server_time}}</span></span>
|
||||
@ -180,10 +184,10 @@
|
||||
</dl>
|
||||
<dl class="row">
|
||||
<dt class="col-sm-3">
|
||||
<button type="button" id="gen-support" class="btn btn-primary" onclick="generateSupportString(); return false;">Generate Support String</button>
|
||||
<button type="button" id="gen-support" class="btn btn-primary">Generate Support String</button>
|
||||
<br><br>
|
||||
<button type="button" id="copy-support" class="btn btn-info mb-3 d-none" onclick="copyToClipboard(); return false;">Copy To Clipboard</button>
|
||||
<div class="toast-container position-absolute float-start" style="width: 15rem;">
|
||||
<button type="button" id="copy-support" class="btn btn-info mb-3 d-none">Copy To Clipboard</button>
|
||||
<div class="toast-container position-absolute float-start vw-copy-toast">
|
||||
<div id="toastClipboardCopy" class="toast fade hide" role="status" aria-live="polite" aria-atomic="true" data-bs-autohide="true" data-bs-delay="1500">
|
||||
<div class="toast-body">
|
||||
Copied to clipboard!
|
||||
@ -192,197 +196,12 @@
|
||||
</div>
|
||||
</dt>
|
||||
<dd class="col-sm-9">
|
||||
<pre id="support-string" class="pre-scrollable d-none w-100 border p-2" style="height: 16rem;"></pre>
|
||||
<pre id="support-string" class="pre-scrollable d-none w-100 border p-2"></pre>
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
'use strict';
|
||||
|
||||
var dnsCheck = false;
|
||||
var timeCheck = false;
|
||||
var domainCheck = false;
|
||||
var httpsCheck = false;
|
||||
|
||||
(() => {
|
||||
// ================================
|
||||
// Date & Time Check
|
||||
const d = new Date();
|
||||
const year = d.getUTCFullYear();
|
||||
const month = String(d.getUTCMonth()+1).padStart(2, '0');
|
||||
const day = String(d.getUTCDate()).padStart(2, '0');
|
||||
const hour = String(d.getUTCHours()).padStart(2, '0');
|
||||
const minute = String(d.getUTCMinutes()).padStart(2, '0');
|
||||
const seconds = String(d.getUTCSeconds()).padStart(2, '0');
|
||||
const browserUTC = `${year}-${month}-${day} ${hour}:${minute}:${seconds} UTC`;
|
||||
document.getElementById("time-browser-string").innerText = browserUTC;
|
||||
|
||||
const serverUTC = document.getElementById("time-server-string").innerText;
|
||||
const timeDrift = (
|
||||
Date.parse(serverUTC.replace(' ', 'T').replace(' UTC', '')) -
|
||||
Date.parse(browserUTC.replace(' ', 'T').replace(' UTC', ''))
|
||||
) / 1000;
|
||||
if (timeDrift > 30 || timeDrift < -30) {
|
||||
document.getElementById('time-warning').classList.remove('d-none');
|
||||
} else {
|
||||
document.getElementById('time-success').classList.remove('d-none');
|
||||
timeCheck = true;
|
||||
}
|
||||
|
||||
// ================================
|
||||
// Check if the output is a valid IP
|
||||
const isValidIp = value => (/^(?:(?:^|\.)(?:2(?:5[0-5]|[0-4]\d)|1?\d?\d)){4}$/.test(value) ? true : false);
|
||||
if (isValidIp(document.getElementById('dns-resolved').innerText)) {
|
||||
document.getElementById('dns-success').classList.remove('d-none');
|
||||
dnsCheck = true;
|
||||
} else {
|
||||
document.getElementById('dns-warning').classList.remove('d-none');
|
||||
}
|
||||
|
||||
// ================================
|
||||
// Version check for both vaultwarden and web-vault
|
||||
let serverInstalled = document.getElementById('server-installed').innerText;
|
||||
let serverLatest = document.getElementById('server-latest').innerText;
|
||||
let serverLatestCommit = document.getElementById('server-latest-commit').innerText.replace('-', '');
|
||||
if (serverInstalled.indexOf('-') !== -1 && serverLatest !== '-' && serverLatestCommit !== '-') {
|
||||
document.getElementById('server-latest-commit').classList.remove('d-none');
|
||||
}
|
||||
|
||||
const webInstalled = document.getElementById('web-installed').innerText;
|
||||
checkVersions('server', serverInstalled, serverLatest, serverLatestCommit);
|
||||
|
||||
{{#unless page_data.running_within_docker}}
|
||||
const webLatest = document.getElementById('web-latest').innerText;
|
||||
checkVersions('web', webInstalled, webLatest);
|
||||
{{/unless}}
|
||||
|
||||
function checkVersions(platform, installed, latest, commit=null) {
|
||||
if (installed === '-' || latest === '-') {
|
||||
document.getElementById(platform + '-failed').classList.remove('d-none');
|
||||
return;
|
||||
}
|
||||
|
||||
// Only check basic versions, no commit revisions
|
||||
if (commit === null || installed.indexOf('-') === -1) {
|
||||
if (installed !== latest) {
|
||||
document.getElementById(platform + '-warning').classList.remove('d-none');
|
||||
} else {
|
||||
document.getElementById(platform + '-success').classList.remove('d-none');
|
||||
}
|
||||
} else {
|
||||
// Check if this is a branched version.
|
||||
const branchRegex = /(?:\s)\((.*?)\)/;
|
||||
const branchMatch = installed.match(branchRegex);
|
||||
if (branchMatch !== null) {
|
||||
document.getElementById(platform + '-branch').classList.remove('d-none');
|
||||
}
|
||||
|
||||
// This will remove branch info and check if there is a commit hash
|
||||
const installedRegex = /(\d+\.\d+\.\d+)-(\w+)/;
|
||||
const instMatch = installed.match(installedRegex);
|
||||
|
||||
// It could be that a new tagged version has the same commit hash.
|
||||
// In this case the version is the same but only the number is different
|
||||
if (instMatch !== null) {
|
||||
if (instMatch[2] === commit) {
|
||||
// The commit hashes are the same, so latest version is installed
|
||||
document.getElementById(platform + '-success').classList.remove('d-none');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (installed === latest) {
|
||||
document.getElementById(platform + '-success').classList.remove('d-none');
|
||||
} else {
|
||||
document.getElementById(platform + '-warning').classList.remove('d-none');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ================================
|
||||
// Check valid DOMAIN configuration
|
||||
document.getElementById('domain-browser-string').innerText = location.href.toLowerCase();
|
||||
if (document.getElementById('domain-server-string').innerText.toLowerCase() == location.href.toLowerCase()) {
|
||||
document.getElementById('domain-success').classList.remove('d-none');
|
||||
domainCheck = true;
|
||||
} else {
|
||||
document.getElementById('domain-warning').classList.remove('d-none');
|
||||
}
|
||||
|
||||
// Check for HTTPS at domain-server-string
|
||||
if (document.getElementById('domain-server-string').innerText.toLowerCase().startsWith('https://') ) {
|
||||
document.getElementById('https-success').classList.remove('d-none');
|
||||
httpsCheck = true;
|
||||
} else {
|
||||
document.getElementById('https-warning').classList.remove('d-none');
|
||||
}
|
||||
})();
|
||||
|
||||
// ================================
|
||||
// Generate support string to be pasted on github or the forum
|
||||
async function generateSupportString() {
|
||||
let supportString = "### Your environment (Generated via diagnostics page)\n";
|
||||
|
||||
supportString += "* Vaultwarden version: v{{ version }}\n";
|
||||
supportString += "* Web-vault version: v{{ page_data.web_vault_version }}\n";
|
||||
supportString += "* Running within Docker: {{ page_data.running_within_docker }} (Base: {{ page_data.docker_base_image }})\n";
|
||||
supportString += "* Environment settings overridden: ";
|
||||
{{#if page_data.overrides}}
|
||||
supportString += "true\n"
|
||||
{{else}}
|
||||
supportString += "false\n"
|
||||
{{/if}}
|
||||
supportString += "* Uses a reverse proxy: {{ page_data.ip_header_exists }}\n";
|
||||
{{#if page_data.ip_header_exists}}
|
||||
supportString += "* IP Header check: {{ page_data.ip_header_match }} ({{ page_data.ip_header_name }})\n";
|
||||
{{/if}}
|
||||
supportString += "* Internet access: {{ page_data.has_http_access }}\n";
|
||||
supportString += "* Internet access via a proxy: {{ page_data.uses_proxy }}\n";
|
||||
supportString += "* DNS Check: " + dnsCheck + "\n";
|
||||
supportString += "* Time Check: " + timeCheck + "\n";
|
||||
supportString += "* Domain Configuration Check: " + domainCheck + "\n";
|
||||
supportString += "* HTTPS Check: " + httpsCheck + "\n";
|
||||
supportString += "* Database type: {{ page_data.db_type }}\n";
|
||||
supportString += "* Database version: {{ page_data.db_version }}\n";
|
||||
supportString += "* Clients used: \n";
|
||||
supportString += "* Reverse proxy and version: \n";
|
||||
supportString += "* Other relevant information: \n";
|
||||
|
||||
let jsonResponse = await fetch('{{urlpath}}/admin/diagnostics/config', {
|
||||
'headers': { 'Accept': 'application/json' }
|
||||
});
|
||||
if (!jsonResponse.ok) {
|
||||
alert("Generation failed: " + jsonResponse.statusText);
|
||||
throw new Error(jsonResponse);
|
||||
}
|
||||
const configJson = await jsonResponse.json();
|
||||
supportString += "\n### Config (Generated via diagnostics page)\n<details><summary>Show Running Config</summary>\n"
|
||||
supportString += "\n**Environment settings which are overridden:** {{page_data.overrides}}\n"
|
||||
supportString += "\n\n```json\n" + JSON.stringify(configJson, undefined, 2) + "\n```\n</details>\n";
|
||||
|
||||
document.getElementById('support-string').innerText = supportString;
|
||||
document.getElementById('support-string').classList.remove('d-none');
|
||||
document.getElementById('copy-support').classList.remove('d-none');
|
||||
}
|
||||
|
||||
function copyToClipboard() {
|
||||
const supportStr = document.getElementById('support-string').innerText;
|
||||
const tmpCopyEl = document.createElement('textarea');
|
||||
|
||||
tmpCopyEl.setAttribute('id', 'copy-support-string');
|
||||
tmpCopyEl.setAttribute('readonly', '');
|
||||
tmpCopyEl.value = supportStr;
|
||||
tmpCopyEl.style.position = 'absolute';
|
||||
tmpCopyEl.style.left = '-9999px';
|
||||
document.body.appendChild(tmpCopyEl);
|
||||
tmpCopyEl.select();
|
||||
document.execCommand('copy');
|
||||
tmpCopyEl.remove();
|
||||
|
||||
new BSN.Toast('#toastClipboardCopy').show();
|
||||
}
|
||||
</script>
|
||||
<script src="{{urlpath}}/vw_static/admin_diagnostics.js"></script>
|
||||
<script type="application/json" id="diagnostics_json">{{to_json page_data}}</script>
|
||||
|
@ -9,7 +9,7 @@
|
||||
<th>Users</th>
|
||||
<th>Items</th>
|
||||
<th>Attachments</th>
|
||||
<th style="width: 130px; min-width: 130px;">Actions</th>
|
||||
<th class="vw-actions">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -21,7 +21,7 @@
|
||||
<strong>{{Name}}</strong>
|
||||
<span class="me-2">({{BillingEmail}})</span>
|
||||
<span class="d-block">
|
||||
<span class="badge bg-success">{{Id}}</span>
|
||||
<span class="badge bg-success font-monospace">{{Id}}</span>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
@ -38,49 +38,22 @@
|
||||
{{/if}}
|
||||
</td>
|
||||
<td class="text-end px-0 small">
|
||||
<button type="button" class="btn btn-sm btn-link p-0 border-0" onclick='deleteOrganization({{jsesc Id}}, {{jsesc Name}}, {{jsesc BillingEmail}})'>Delete Organization</button>
|
||||
<button type="button" class="btn btn-sm btn-link p-0 border-0" vw-delete-organization data-vw-org-uuid="{{jsesc Id no_quote}}" data-vw-org-name="{{jsesc Name no_quote}}" data-vw-billing-email="{{jsesc BillingEmail no_quote}}">Delete Organization</button>
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 clearfix">
|
||||
<button type="button" class="btn btn-sm btn-primary float-end" id="reload">Reload organizations</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<link rel="stylesheet" href="{{urlpath}}/vw_static/datatables.css" />
|
||||
<script src="{{urlpath}}/vw_static/jquery-3.6.2.slim.js"></script>
|
||||
<script src="{{urlpath}}/vw_static/datatables.js"></script>
|
||||
<script>
|
||||
'use strict';
|
||||
|
||||
function deleteOrganization(id, name, billing_email) {
|
||||
// First make sure the user wants to delete this organization
|
||||
var continueDelete = confirm("WARNING: All data of this organization ("+ name +") will be lost!\nMake sure you have a backup, this cannot be undone!");
|
||||
if (continueDelete == true) {
|
||||
var input_org_uuid = prompt("To delete the organization '" + name + " (" + billing_email +")', please type the organization uuid below.")
|
||||
if (input_org_uuid != null) {
|
||||
if (input_org_uuid == id) {
|
||||
_post("{{urlpath}}/admin/organizations/" + id + "/delete",
|
||||
"Organization deleted correctly",
|
||||
"Error deleting organization");
|
||||
} else {
|
||||
alert("Wrong organization uuid, please try again")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
$('#orgs-table').DataTable({
|
||||
"responsive": true,
|
||||
"lengthMenu": [ [-1, 5, 10, 25, 50], ["All", 5, 10, 25, 50] ],
|
||||
"pageLength": -1, // Default show all
|
||||
"columnDefs": [
|
||||
{ "targets": 4, "searchable": false, "orderable": false }
|
||||
]
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<script src="{{urlpath}}/vw_static/admin_organizations.js"></script>
|
||||
<script src="{{urlpath}}/vw_static/jdenticon.js"></script>
|
||||
|
@ -8,8 +8,8 @@
|
||||
Settings which are overridden are shown with <span class="is-overridden-true alert-row px-1">a yellow colored background</span>.
|
||||
</div>
|
||||
|
||||
<form class="form needs-validation" id="config-form" onsubmit="saveConfig(); return false;" novalidate>
|
||||
{{#each config}}
|
||||
<form class="form needs-validation" id="config-form" novalidate>
|
||||
{{#each page_data.config}}
|
||||
{{#if groupdoc}}
|
||||
<div class="card bg-light mb-3">
|
||||
<button id="b_{{group}}" type="button" class="card-header text-start btn btn-link text-decoration-none" aria-expanded="false" aria-controls="g_{{group}}" data-bs-toggle="collapse" data-bs-target="#g_{{group}}">{{groupdoc}}</button>
|
||||
@ -24,7 +24,7 @@
|
||||
<input class="form-control conf-{{type}}" id="input_{{name}}" type="{{type}}"
|
||||
name="{{name}}" value="{{value}}" {{#if default}} placeholder="Default: {{default}}"{{/if}}>
|
||||
{{#case type "password"}}
|
||||
<button class="btn btn-outline-secondary input-group-text" type="button" onclick="toggleVis('input_{{name}}');">Show/hide</button>
|
||||
<button class="btn btn-outline-secondary input-group-text" type="button" data-vw-pw-toggle="input_{{name}}">Show/hide</button>
|
||||
{{/case}}
|
||||
</div>
|
||||
</div>
|
||||
@ -48,7 +48,7 @@
|
||||
<label for="smtp-test-email" class="col-sm-3 col-form-label">Test SMTP</label>
|
||||
<div class="col-sm-8 input-group">
|
||||
<input class="form-control" id="smtp-test-email" type="email" placeholder="Enter test email" required>
|
||||
<button type="button" class="btn btn-outline-primary input-group-text" onclick="smtpTest(); return false;">Send test email</button>
|
||||
<button type="button" class="btn btn-outline-primary input-group-text" id="smtpTest">Send test email</button>
|
||||
<div class="invalid-tooltip">Please provide a valid email address</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -68,7 +68,7 @@
|
||||
launching the server. You can check the variable names in the tooltips of each option.
|
||||
</div>
|
||||
|
||||
{{#each config}}
|
||||
{{#each page_data.config}}
|
||||
{{#each elements}}
|
||||
{{#unless editable}}
|
||||
<div class="row my-2 align-items-center alert-row" title="[{{name}}] {{doc.description}}">
|
||||
@ -83,11 +83,11 @@
|
||||
--}}
|
||||
{{#if (eq name "database_url")}}
|
||||
<input readonly class="form-control" id="input_{{name}}" type="password" value="{{value}}" {{#if default}} placeholder="Default: {{default}}" {{/if}}>
|
||||
<button class="btn btn-outline-secondary" type="button" onclick="toggleVis('input_{{name}}');">Show/hide</button>
|
||||
<button class="btn btn-outline-secondary" type="button" data-vw-pw-toggle="input_{{name}}">Show/hide</button>
|
||||
{{else}}
|
||||
<input readonly class="form-control" id="input_{{name}}" type="{{type}}" value="{{value}}" {{#if default}} placeholder="Default: {{default}}" {{/if}}>
|
||||
{{#case type "password"}}
|
||||
<button class="btn btn-outline-secondary" type="button" onclick="toggleVis('input_{{name}}');">Show/hide</button>
|
||||
<button class="btn btn-outline-secondary" type="button" data-vw-pw-toggle="input_{{name}}">Show/hide</button>
|
||||
{{/case}}
|
||||
{{/if}}
|
||||
</div>
|
||||
@ -112,7 +112,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#if can_backup}}
|
||||
{{#if page_data.can_backup}}
|
||||
<div class="card bg-light mb-3">
|
||||
<button id="b_database" type="button" class="card-header text-start btn btn-link text-decoration-none" aria-expanded="false" aria-controls="g_database"
|
||||
data-bs-toggle="collapse" data-bs-target="#g_database">Backup Database</button>
|
||||
@ -124,18 +124,17 @@
|
||||
how to perform complete backups, refer to the wiki page on
|
||||
<a href="https://github.com/dani-garcia/vaultwarden/wiki/Backing-up-your-vault" target="_blank" rel="noopener noreferrer">backups</a>.
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary" onclick="backupDatabase();">Backup Database</button>
|
||||
<button type="button" class="btn btn-primary" id="backupDatabase">Backup Database</button>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<button type="submit" class="btn btn-primary">Save</button>
|
||||
<button type="button" class="btn btn-danger float-end" onclick="deleteConf();">Reset defaults</button>
|
||||
<button type="button" class="btn btn-danger float-end" id="deleteConf">Reset defaults</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
#config-block ::placeholder {
|
||||
/* Most modern browsers support this now. */
|
||||
@ -148,146 +147,4 @@
|
||||
--bs-alert-border-color: #ffecb5;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
'use strict';
|
||||
|
||||
function smtpTest() {
|
||||
if (formHasChanges(config_form)) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
alert("Config has been changed but not yet saved.\nPlease save the changes first before sending a test email.");
|
||||
return false;
|
||||
}
|
||||
|
||||
let test_email = document.getElementById("smtp-test-email");
|
||||
|
||||
// Do a very very basic email address check.
|
||||
if (test_email.value.match(/\S+@\S+/i) === null) {
|
||||
test_email.parentElement.classList.add('was-validated');
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
|
||||
const data = JSON.stringify({ "email": test_email.value });
|
||||
_post("{{urlpath}}/admin/test/smtp/",
|
||||
"SMTP Test email sent correctly",
|
||||
"Error sending SMTP test email", data, false);
|
||||
return false;
|
||||
}
|
||||
function getFormData() {
|
||||
let data = {};
|
||||
|
||||
document.querySelectorAll(".conf-checkbox").forEach(function (e) {
|
||||
data[e.name] = e.checked;
|
||||
});
|
||||
|
||||
document.querySelectorAll(".conf-number").forEach(function (e) {
|
||||
data[e.name] = e.value ? +e.value : null;
|
||||
});
|
||||
|
||||
document.querySelectorAll(".conf-text, .conf-password").forEach(function (e) {
|
||||
data[e.name] = e.value || null;
|
||||
});
|
||||
return data;
|
||||
}
|
||||
function saveConfig() {
|
||||
const data = JSON.stringify(getFormData());
|
||||
_post("{{urlpath}}/admin/config/", "Config saved correctly",
|
||||
"Error saving config", data);
|
||||
return false;
|
||||
}
|
||||
function deleteConf() {
|
||||
var input = prompt("This will remove all user configurations, and restore the defaults and the " +
|
||||
"values set by the environment. This operation could be dangerous. Type 'DELETE' to proceed:");
|
||||
if (input === "DELETE") {
|
||||
_post("{{urlpath}}/admin/config/delete",
|
||||
"Config deleted correctly",
|
||||
"Error deleting config");
|
||||
} else {
|
||||
alert("Wrong input, please try again")
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
function backupDatabase() {
|
||||
_post("{{urlpath}}/admin/config/backup_db",
|
||||
"Backup created successfully",
|
||||
"Error creating backup", null, false);
|
||||
return false;
|
||||
}
|
||||
function masterCheck(check_id, inputs_query) {
|
||||
function onChanged(checkbox, inputs_query) {
|
||||
return function _fn() {
|
||||
document.querySelectorAll(inputs_query).forEach(function (e) { e.disabled = !checkbox.checked; });
|
||||
checkbox.disabled = false;
|
||||
};
|
||||
}
|
||||
|
||||
const checkbox = document.getElementById(check_id);
|
||||
const onChange = onChanged(checkbox, inputs_query);
|
||||
onChange(); // Trigger the event initially
|
||||
checkbox.addEventListener("change", onChange);
|
||||
}
|
||||
|
||||
{{#each config}} {{#if grouptoggle}}
|
||||
masterCheck("input_{{grouptoggle}}", "#g_{{group}} input");
|
||||
{{/if}} {{/each}}
|
||||
|
||||
// Two functions to help check if there were changes to the form fields
|
||||
// Useful for example during the smtp test to prevent people from clicking save before testing there new settings
|
||||
function initChangeDetection(form) {
|
||||
const ignore_fields = ["smtp-test-email"];
|
||||
Array.from(form).forEach((el) => {
|
||||
if (! ignore_fields.includes(el.id)) {
|
||||
el.dataset.origValue = el.value
|
||||
}
|
||||
});
|
||||
}
|
||||
function formHasChanges(form) {
|
||||
return Array.from(form).some(el => 'origValue' in el.dataset && ( el.dataset.origValue !== el.value));
|
||||
}
|
||||
|
||||
// This function will prevent submitting a from when someone presses enter.
|
||||
function preventFormSubmitOnEnter(form) {
|
||||
form.onkeypress = function(e) {
|
||||
let key = e.charCode || e.keyCode || 0;
|
||||
if (key == 13) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize Form Change Detection
|
||||
const config_form = document.getElementById('config-form');
|
||||
initChangeDetection(config_form);
|
||||
// Prevent enter to submitting the form and save the config.
|
||||
// Users need to really click on save, this also to prevent accidental submits.
|
||||
preventFormSubmitOnEnter(config_form);
|
||||
|
||||
// This function will hook into the smtp-test-email input field and will call the smtpTest() function when enter is pressed.
|
||||
function submitTestEmailOnEnter() {
|
||||
const smtp_test_email_input = document.getElementById('smtp-test-email');
|
||||
smtp_test_email_input.onkeypress = function(e) {
|
||||
let key = e.charCode || e.keyCode || 0;
|
||||
if (key == 13) {
|
||||
e.preventDefault();
|
||||
smtpTest();
|
||||
}
|
||||
}
|
||||
}
|
||||
submitTestEmailOnEnter();
|
||||
|
||||
// Colorize some settings which are high risk
|
||||
function colorRiskSettings() {
|
||||
const risk_items = document.getElementsByClassName('col-form-label');
|
||||
Array.from(risk_items).forEach((el) => {
|
||||
if (el.innerText.toLowerCase().includes('risks') ) {
|
||||
el.parentElement.className += ' alert-danger'
|
||||
}
|
||||
});
|
||||
}
|
||||
colorRiskSettings();
|
||||
|
||||
</script>
|
||||
<script src="{{urlpath}}/vw_static/admin_settings.js"></script>
|
||||
|
@ -1,18 +1,17 @@
|
||||
<main class="container-xl">
|
||||
<div id="users-block" class="my-3 p-3 bg-white rounded shadow">
|
||||
<h6 class="border-bottom pb-2 mb-3">Registered Users</h6>
|
||||
|
||||
<div class="table-responsive-xl small">
|
||||
<table id="users-table" class="table table-sm table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>User</th>
|
||||
<th style="width: 85px; min-width: 70px;">Created at</th>
|
||||
<th style="width: 85px; min-width: 70px;">Last Active</th>
|
||||
<th style="width: 35px; min-width: 35px;">Items</th>
|
||||
<th>Attachments</th>
|
||||
<th style="min-width: 120px;">Organizations</th>
|
||||
<th style="width: 130px; min-width: 130px;">Actions</th>
|
||||
<th class="vw-created-at">Created at</th>
|
||||
<th class="vw-last-active">Last Active</th>
|
||||
<th class="vw-items">Items</th>
|
||||
<th class="vw-attachments">Attachments</th>
|
||||
<th class="vw-organizations">Organizations</th>
|
||||
<th class="vw-actions">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -55,23 +54,25 @@
|
||||
{{/if}}
|
||||
</td>
|
||||
<td>
|
||||
<div class="overflow-auto" style="max-height: 120px;">
|
||||
<div class="overflow-auto vw-org-cell" data-vw-user-email="{{jsesc Email no_quote}}" data-vw-user-uuid="{{jsesc Id no_quote}}">
|
||||
{{#each Organizations}}
|
||||
<button class="badge" data-bs-toggle="modal" data-bs-target="#userOrgTypeDialog" data-orgtype="{{Type}}" data-orguuid="{{jsesc Id no_quote}}" data-orgname="{{jsesc Name no_quote}}" data-useremail="{{jsesc ../Email no_quote}}" data-useruuid="{{jsesc ../Id no_quote}}">{{Name}}</button>
|
||||
<button class="badge" data-bs-toggle="modal" data-bs-target="#userOrgTypeDialog" data-vw-org-type="{{Type}}" data-vw-org-uuid="{{jsesc Id no_quote}}" data-vw-org-name="{{jsesc Name no_quote}}">{{Name}}</button>
|
||||
{{/each}}
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-end px-0 small">
|
||||
{{#if TwoFactorEnabled}}
|
||||
<button type="button" class="btn btn-sm btn-link p-0 border-0" onclick='remove2fa({{jsesc Id}})'>Remove all 2FA</button>
|
||||
{{/if}}
|
||||
<button type="button" class="btn btn-sm btn-link p-0 border-0" onclick='deauthUser({{jsesc Id}})'>Deauthorize sessions</button>
|
||||
<button type="button" class="btn btn-sm btn-link p-0 border-0" onclick='deleteUser({{jsesc Id}}, {{jsesc Email}})'>Delete User</button>
|
||||
{{#if user_enabled}}
|
||||
<button type="button" class="btn btn-sm btn-link p-0 border-0" onclick='disableUser({{jsesc Id}}, {{jsesc Email}})'>Disable User</button>
|
||||
{{else}}
|
||||
<button type="button" class="btn btn-sm btn-link p-0 border-0" onclick='enableUser({{jsesc Id}}, {{jsesc Email}})'>Enable User</button>
|
||||
{{/if}}
|
||||
<span data-vw-user-uuid="{{jsesc Id no_quote}}" data-vw-user-email="{{jsesc Email no_quote}}">
|
||||
{{#if TwoFactorEnabled}}
|
||||
<button type="button" class="btn btn-sm btn-link p-0 border-0" vw-remove2fa>Remove all 2FA</button>
|
||||
{{/if}}
|
||||
<button type="button" class="btn btn-sm btn-link p-0 border-0" vw-deauth-user>Deauthorize sessions</button>
|
||||
<button type="button" class="btn btn-sm btn-link p-0 border-0" vw-delete-user>Delete User</button>
|
||||
{{#if user_enabled}}
|
||||
<button type="button" class="btn btn-sm btn-link p-0 border-0" vw-disable-user>Disable User</button>
|
||||
{{else}}
|
||||
<button type="button" class="btn btn-sm btn-link p-0 border-0" vw-enable-user>Enable User</button>
|
||||
{{/if}}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
@ -79,23 +80,23 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<button type="button" class="btn btn-sm btn-danger" onclick="updateRevisions();"
|
||||
<div class="mt-3 clearfix">
|
||||
<button type="button" class="btn btn-sm btn-danger" id="updateRevisions"
|
||||
title="Force all clients to fetch new data next time they connect. Useful after restoring a backup to remove any stale data.">
|
||||
Force clients to resync
|
||||
</button>
|
||||
|
||||
<button type="button" class="btn btn-sm btn-primary float-end" onclick="reload();">Reload users</button>
|
||||
<button type="button" class="btn btn-sm btn-primary float-end" id="reload">Reload users</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="invite-form-block" class="align-items-center p-3 mb-3 text-white-50 bg-secondary rounded shadow">
|
||||
<div id="inviteUserFormBlock" class="align-items-center p-3 mb-3 text-white-50 bg-secondary rounded shadow">
|
||||
<div>
|
||||
<h6 class="mb-0 text-white">Invite User</h6>
|
||||
<small>Email:</small>
|
||||
|
||||
<form class="form-inline input-group w-50" id="invite-form" onsubmit="inviteUser(); return false;">
|
||||
<input type="email" class="form-control me-2" id="email-invite" placeholder="Enter email" required>
|
||||
<form class="form-inline input-group w-50" id="inviteUserForm">
|
||||
<input type="email" class="form-control me-2" id="inviteEmail" placeholder="Enter email" required>
|
||||
<button type="submit" class="btn btn-primary">Invite</button>
|
||||
</form>
|
||||
</div>
|
||||
@ -108,7 +109,7 @@
|
||||
<h6 class="modal-title" id="userOrgTypeDialogTitle"></h6>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form class="form" id="userOrgTypeForm" onsubmit="updateUserOrgType(); return false;">
|
||||
<form class="form" id="userOrgTypeForm">
|
||||
<input type="hidden" name="user_uuid" id="userOrgTypeUserUuid" value="">
|
||||
<input type="hidden" name="org_uuid" id="userOrgTypeOrgUuid" value="">
|
||||
<div class="modal-body">
|
||||
@ -138,150 +139,5 @@
|
||||
<link rel="stylesheet" href="{{urlpath}}/vw_static/datatables.css" />
|
||||
<script src="{{urlpath}}/vw_static/jquery-3.6.2.slim.js"></script>
|
||||
<script src="{{urlpath}}/vw_static/datatables.js"></script>
|
||||
<script>
|
||||
'use strict';
|
||||
|
||||
function deleteUser(id, mail) {
|
||||
var input_mail = prompt("To delete user '" + mail + "', please type the email below")
|
||||
if (input_mail != null) {
|
||||
if (input_mail == mail) {
|
||||
_post("{{urlpath}}/admin/users/" + id + "/delete",
|
||||
"User deleted correctly",
|
||||
"Error deleting user");
|
||||
} else {
|
||||
alert("Wrong email, please try again")
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function remove2fa(id) {
|
||||
_post("{{urlpath}}/admin/users/" + id + "/remove-2fa",
|
||||
"2FA removed correctly",
|
||||
"Error removing 2FA");
|
||||
return false;
|
||||
}
|
||||
function deauthUser(id) {
|
||||
_post("{{urlpath}}/admin/users/" + id + "/deauth",
|
||||
"Sessions deauthorized correctly",
|
||||
"Error deauthorizing sessions");
|
||||
return false;
|
||||
}
|
||||
function disableUser(id, mail) {
|
||||
var confirmed = confirm("Are you sure you want to disable user '" + mail + "'? This will also deauthorize their sessions.")
|
||||
if (confirmed) {
|
||||
_post("{{urlpath}}/admin/users/" + id + "/disable",
|
||||
"User disabled successfully",
|
||||
"Error disabling user");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function enableUser(id, mail) {
|
||||
var confirmed = confirm("Are you sure you want to enable user '" + mail + "'?")
|
||||
if (confirmed) {
|
||||
_post("{{urlpath}}/admin/users/" + id + "/enable",
|
||||
"User enabled successfully",
|
||||
"Error enabling user");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function updateRevisions() {
|
||||
_post("{{urlpath}}/admin/users/update_revision",
|
||||
"Success, clients will sync next time they connect",
|
||||
"Error forcing clients to sync");
|
||||
return false;
|
||||
}
|
||||
function inviteUser() {
|
||||
const inv = document.getElementById("email-invite");
|
||||
const data = JSON.stringify({ "email": inv.value });
|
||||
inv.value = "";
|
||||
_post("{{urlpath}}/admin/invite/", "User invited correctly",
|
||||
"Error inviting user", data);
|
||||
return false;
|
||||
}
|
||||
|
||||
let OrgTypes = {
|
||||
"0": { "name": "Owner", "color": "orange" },
|
||||
"1": { "name": "Admin", "color": "blueviolet" },
|
||||
"2": { "name": "User", "color": "blue" },
|
||||
"3": { "name": "Manager", "color": "green" },
|
||||
};
|
||||
|
||||
document.querySelectorAll("[data-orgtype]").forEach(function (e) {
|
||||
let orgtype = OrgTypes[e.dataset.orgtype];
|
||||
e.style.backgroundColor = orgtype.color;
|
||||
e.title = orgtype.name;
|
||||
});
|
||||
|
||||
// Special sort function to sort dates in ISO format
|
||||
jQuery.extend( jQuery.fn.dataTableExt.oSort, {
|
||||
"date-iso-pre": function ( a ) {
|
||||
let x;
|
||||
let sortDate = a.replace(/(<([^>]+)>)/gi, "").trim();
|
||||
if ( sortDate !== '' ) {
|
||||
let dtParts = sortDate.split(' ');
|
||||
var timeParts = (undefined != dtParts[1]) ? dtParts[1].split(':') : ['00','00','00'];
|
||||
var dateParts = dtParts[0].split('-');
|
||||
x = (dateParts[0] + dateParts[1] + dateParts[2] + timeParts[0] + timeParts[1] + ((undefined != timeParts[2]) ? timeParts[2] : 0)) * 1;
|
||||
if ( isNaN(x) ) {
|
||||
x = 0;
|
||||
}
|
||||
} else {
|
||||
x = Infinity;
|
||||
}
|
||||
return x;
|
||||
},
|
||||
|
||||
"date-iso-asc": function ( a, b ) {
|
||||
return a - b;
|
||||
},
|
||||
|
||||
"date-iso-desc": function ( a, b ) {
|
||||
return b - a;
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
$('#users-table').DataTable({
|
||||
"responsive": true,
|
||||
"lengthMenu": [ [-1, 5, 10, 25, 50], ["All", 5, 10, 25, 50] ],
|
||||
"pageLength": -1, // Default show all
|
||||
"columnDefs": [
|
||||
{ "targets": [1,2], "type": "date-iso" },
|
||||
{ "targets": 6, "searchable": false, "orderable": false }
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
var userOrgTypeDialog = document.getElementById('userOrgTypeDialog');
|
||||
// Fill the form and title
|
||||
userOrgTypeDialog.addEventListener('show.bs.modal', function(event){
|
||||
let userOrgType = event.relatedTarget.getAttribute("data-orgtype");
|
||||
let userOrgTypeName = OrgTypes[userOrgType]["name"];
|
||||
let orgName = event.relatedTarget.getAttribute("data-orgname");
|
||||
let userEmail = event.relatedTarget.getAttribute("data-useremail");
|
||||
let orgUuid = event.relatedTarget.getAttribute("data-orguuid");
|
||||
let userUuid = event.relatedTarget.getAttribute("data-useruuid");
|
||||
|
||||
document.getElementById("userOrgTypeDialogTitle").innerHTML = "<b>Update User Type:</b><br><b>Organization:</b> " + orgName + "<br><b>User:</b> " + userEmail;
|
||||
document.getElementById("userOrgTypeUserUuid").value = userUuid;
|
||||
document.getElementById("userOrgTypeOrgUuid").value = orgUuid;
|
||||
document.getElementById("userOrgType"+userOrgTypeName).checked = true;
|
||||
}, false);
|
||||
|
||||
// Prevent accidental submission of the form with valid elements after the modal has been hidden.
|
||||
userOrgTypeDialog.addEventListener('hide.bs.modal', function(){
|
||||
document.getElementById("userOrgTypeDialogTitle").innerHTML = '';
|
||||
document.getElementById("userOrgTypeUserUuid").value = '';
|
||||
document.getElementById("userOrgTypeOrgUuid").value = '';
|
||||
}, false);
|
||||
|
||||
function updateUserOrgType() {
|
||||
let orgForm = document.getElementById("userOrgTypeForm");
|
||||
const data = JSON.stringify(Object.fromEntries(new FormData(orgForm).entries()));
|
||||
|
||||
_post("{{urlpath}}/admin/users/org_type",
|
||||
"Updated organization type of the user successfully",
|
||||
"Error updating organization type of the user", data);
|
||||
return false;
|
||||
}
|
||||
</script>
|
||||
<script src="{{urlpath}}/vw_static/admin_users.js"></script>
|
||||
<script src="{{urlpath}}/vw_static/jdenticon.js"></script>
|
||||
|
18
src/util.rs
18
src/util.rs
@ -42,14 +42,6 @@ impl Fairing for AppHeaders {
|
||||
// This can cause issues when some MFA requests needs to open a popup or page within the clients like WebAuthn, or Duo.
|
||||
// This is the same behaviour as upstream Bitwarden.
|
||||
if !req_uri_path.ends_with("connector.html") {
|
||||
// Check if we are requesting an admin page, if so, allow unsafe-inline for scripts.
|
||||
// TODO: In the future maybe we need to see if we can generate a sha256 hash or have no scripts inline at all.
|
||||
let admin_path = format!("{}/admin", CONFIG.domain_path());
|
||||
let mut script_src = "";
|
||||
if req_uri_path.starts_with(admin_path.as_str()) {
|
||||
script_src = " 'unsafe-inline'";
|
||||
}
|
||||
|
||||
// # Frame Ancestors:
|
||||
// Chrome Web Store: https://chrome.google.com/webstore/detail/bitwarden-free-password-m/nngceckbapebfimnlniiiahkandclblb
|
||||
// Edge Add-ons: https://microsoftedge.microsoft.com/addons/detail/bitwarden-free-password/jbkfoedolllekgbhcbcoahefnbanhhlh?hl=en-US
|
||||
@ -66,7 +58,7 @@ impl Fairing for AppHeaders {
|
||||
base-uri 'self'; \
|
||||
form-action 'self'; \
|
||||
object-src 'self' blob:; \
|
||||
script-src 'self'{script_src}; \
|
||||
script-src 'self'; \
|
||||
style-src 'self' 'unsafe-inline'; \
|
||||
child-src 'self' https://*.duosecurity.com https://*.duofederal.com; \
|
||||
frame-src 'self' https://*.duosecurity.com https://*.duofederal.com; \
|
||||
@ -520,13 +512,13 @@ pub fn is_running_in_docker() -> bool {
|
||||
|
||||
/// Simple check to determine on which docker base image vaultwarden is running.
|
||||
/// We build images based upon Debian or Alpine, so these we check here.
|
||||
pub fn docker_base_image() -> String {
|
||||
pub fn docker_base_image() -> &'static str {
|
||||
if Path::new("/etc/debian_version").exists() {
|
||||
"Debian".to_string()
|
||||
"Debian"
|
||||
} else if Path::new("/etc/alpine-release").exists() {
|
||||
"Alpine".to_string()
|
||||
"Alpine"
|
||||
} else {
|
||||
"Unknown".to_string()
|
||||
"Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user