Spiegel von
https://github.com/dani-garcia/vaultwarden.git
synchronisiert 2024-11-26 05:50:29 +01:00
Merge branch 'BlackDex-remove-inline-js'
Dieser Commit ist enthalten in:
Commit
f108349547
18 geänderte Dateien mit 946 neuen und 718 gelöschten Zeilen
|
@ -144,7 +144,6 @@ fn render_admin_login(msg: Option<&str>, redirect: Option<String>) -> ApiResult<
|
||||||
let msg = msg.map(|msg| format!("Error: {msg}"));
|
let msg = msg.map(|msg| format!("Error: {msg}"));
|
||||||
let json = json!({
|
let json = json!({
|
||||||
"page_content": "admin/login",
|
"page_content": "admin/login",
|
||||||
"version": VERSION,
|
|
||||||
"error": msg,
|
"error": msg,
|
||||||
"redirect": redirect,
|
"redirect": redirect,
|
||||||
"urlpath": CONFIG.domain_path()
|
"urlpath": CONFIG.domain_path()
|
||||||
|
@ -208,34 +207,16 @@ fn _validate_token(token: &str) -> bool {
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct AdminTemplateData {
|
struct AdminTemplateData {
|
||||||
page_content: String,
|
page_content: String,
|
||||||
version: Option<&'static str>,
|
|
||||||
page_data: Option<Value>,
|
page_data: Option<Value>,
|
||||||
config: Value,
|
|
||||||
can_backup: bool,
|
|
||||||
logged_in: bool,
|
logged_in: bool,
|
||||||
urlpath: String,
|
urlpath: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AdminTemplateData {
|
impl AdminTemplateData {
|
||||||
fn new() -> Self {
|
fn new(page_content: &str, page_data: Value) -> 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 {
|
|
||||||
Self {
|
Self {
|
||||||
page_content: String::from(page_content),
|
page_content: String::from(page_content),
|
||||||
version: VERSION,
|
|
||||||
page_data: Some(page_data),
|
page_data: Some(page_data),
|
||||||
config: CONFIG.prepare_json(),
|
|
||||||
can_backup: *CAN_BACKUP,
|
|
||||||
logged_in: true,
|
logged_in: true,
|
||||||
urlpath: CONFIG.domain_path(),
|
urlpath: CONFIG.domain_path(),
|
||||||
}
|
}
|
||||||
|
@ -247,7 +228,11 @@ impl AdminTemplateData {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_admin_page() -> ApiResult<Html<String>> {
|
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))
|
Ok(Html(text))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -342,7 +327,7 @@ async fn users_overview(_token: AdminToken, mut conn: DbConn) -> ApiResult<Html<
|
||||||
users_json.push(usr);
|
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))
|
Ok(Html(text))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -442,7 +427,7 @@ async fn update_user_org_type(
|
||||||
};
|
};
|
||||||
|
|
||||||
if user_to_edit.atype == UserOrgType::Owner && new_type != UserOrgType::Owner {
|
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 {
|
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")
|
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);
|
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))
|
Ok(Html(text))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -617,13 +602,14 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, mut conn: DbConn)
|
||||||
|
|
||||||
let diagnostics_json = json!({
|
let diagnostics_json = json!({
|
||||||
"dns_resolved": dns_resolved,
|
"dns_resolved": dns_resolved,
|
||||||
|
"current_release": VERSION,
|
||||||
"latest_release": latest_release,
|
"latest_release": latest_release,
|
||||||
"latest_commit": latest_commit,
|
"latest_commit": latest_commit,
|
||||||
"web_vault_enabled": &CONFIG.web_vault_enabled(),
|
"web_vault_enabled": &CONFIG.web_vault_enabled(),
|
||||||
"web_vault_version": web_vault_version.version,
|
"web_vault_version": web_vault_version.version,
|
||||||
"latest_web_build": latest_web_build,
|
"latest_web_build": latest_web_build,
|
||||||
"running_within_docker": running_within_docker,
|
"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,
|
"has_http_access": has_http_access,
|
||||||
"ip_header_exists": &ip_header.0.is_some(),
|
"ip_header_exists": &ip_header.0.is_some(),
|
||||||
"ip_header_match": ip_header_name == CONFIG.ip_header(),
|
"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,
|
"db_version": get_sql_server_version(&mut conn).await,
|
||||||
"admin_url": format!("{}/diagnostics", admin_url()),
|
"admin_url": format!("{}/diagnostics", admin_url()),
|
||||||
"overrides": &CONFIG.get_overrides().join(", "),
|
"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_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
|
"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))
|
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"))),
|
"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-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"))),
|
"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.css" => Ok((ContentType::CSS, include_bytes!("../static/scripts/bootstrap.css"))),
|
||||||
"bootstrap-native.js" => Ok((ContentType::JavaScript, include_bytes!("../static/scripts/bootstrap-native.js"))),
|
"bootstrap-native.js" => Ok((ContentType::JavaScript, include_bytes!("../static/scripts/bootstrap-native.js"))),
|
||||||
"jdenticon.js" => Ok((ContentType::JavaScript, include_bytes!("../static/scripts/jdenticon.js"))),
|
"jdenticon.js" => Ok((ContentType::JavaScript, include_bytes!("../static/scripts/jdenticon.js"))),
|
||||||
|
|
|
@ -1086,6 +1086,7 @@ where
|
||||||
// Register helpers
|
// Register helpers
|
||||||
hb.register_helper("case", Box::new(case_helper));
|
hb.register_helper("case", Box::new(case_helper));
|
||||||
hb.register_helper("jsesc", Box::new(js_escape_helper));
|
hb.register_helper("jsesc", Box::new(js_escape_helper));
|
||||||
|
hb.register_helper("to_json", Box::new(to_json));
|
||||||
|
|
||||||
macro_rules! reg {
|
macro_rules! reg {
|
||||||
($name:expr) => {{
|
($name:expr) => {{
|
||||||
|
@ -1183,3 +1184,17 @@ fn js_escape_helper<'reg, 'rc>(
|
||||||
out.write(&escaped_value)?;
|
out.write(&escaped_value)?;
|
||||||
Ok(())
|
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
gevendort
Normale Datei
26
src/static/scripts/404.css
gevendort
Normale Datei
|
@ -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
gevendort
Normale Datei
45
src/static/scripts/admin.css
gevendort
Normale Datei
|
@ -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
gevendort
Normale Datei
65
src/static/scripts/admin.js
gevendort
Normale Datei
|
@ -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
gevendort
Normale Datei
219
src/static/scripts/admin_diagnostics.js
gevendort
Normale Datei
|
@ -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
gevendort
Normale Datei
54
src/static/scripts/admin_organizations.js
gevendort
Normale Datei
|
@ -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
gevendort
Normale Datei
180
src/static/scripts/admin_settings.js
gevendort
Normale Datei
|
@ -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
gevendort
Normale Datei
246
src/static/scripts/admin_users.js
gevendort
Normale Datei
|
@ -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
gevendort
2
src/static/scripts/bootstrap.css
gevendort
|
@ -10874,5 +10874,3 @@ textarea.form-control-lg {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*# sourceMappingURL=bootstrap.css.map */
|
|
|
@ -7,31 +7,7 @@
|
||||||
<link rel="icon" type="image/png" href="{{urlpath}}/vw_static/vaultwarden-favicon.png">
|
<link rel="icon" type="image/png" href="{{urlpath}}/vw_static/vaultwarden-favicon.png">
|
||||||
<title>Page not found!</title>
|
<title>Page not found!</title>
|
||||||
<link rel="stylesheet" href="{{urlpath}}/vw_static/bootstrap.css" />
|
<link rel="stylesheet" href="{{urlpath}}/vw_static/bootstrap.css" />
|
||||||
<style>
|
<link rel="stylesheet" href="{{urlpath}}/vw_static/404.css" />
|
||||||
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>
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="bg-light">
|
<body class="bg-light">
|
||||||
|
@ -53,7 +29,7 @@
|
||||||
<h2>Page not found!</h2>
|
<h2>Page not found!</h2>
|
||||||
<p class="lead">Sorry, but the page you were looking for could not be found.</p>
|
<p class="lead">Sorry, but the page you were looking for could not be found.</p>
|
||||||
<p class="display-6">
|
<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>
|
<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>
|
</main>
|
||||||
|
|
||||||
|
|
|
@ -7,86 +7,9 @@
|
||||||
<link rel="icon" type="image/png" href="{{urlpath}}/vw_static/vaultwarden-favicon.png">
|
<link rel="icon" type="image/png" href="{{urlpath}}/vw_static/vaultwarden-favicon.png">
|
||||||
<title>Vaultwarden Admin Panel</title>
|
<title>Vaultwarden Admin Panel</title>
|
||||||
<link rel="stylesheet" href="{{urlpath}}/vw_static/bootstrap.css" />
|
<link rel="stylesheet" href="{{urlpath}}/vw_static/bootstrap.css" />
|
||||||
<style>
|
<link rel="stylesheet" href="{{urlpath}}/vw_static/admin.css" />
|
||||||
body {
|
<script src="{{urlpath}}/vw_static/admin.js"></script>
|
||||||
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>
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="bg-light">
|
<body class="bg-light">
|
||||||
<nav class="navbar navbar-expand-md navbar-dark bg-dark mb-4 shadow fixed-top">
|
<nav class="navbar navbar-expand-md navbar-dark bg-dark mb-4 shadow fixed-top">
|
||||||
<div class="container-xl">
|
<div class="container-xl">
|
||||||
|
@ -126,21 +49,6 @@
|
||||||
{{> (lookup this "page_content") }}
|
{{> (lookup this "page_content") }}
|
||||||
|
|
||||||
<!-- This script needs to be at the bottom, else it will fail! -->
|
<!-- 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>
|
<script src="{{urlpath}}/vw_static/bootstrap-native.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
<span class="badge bg-info d-none" id="server-branch" title="This is a branched version.">Branched</span>
|
<span class="badge bg-info d-none" id="server-branch" title="This is a branched version.">Branched</span>
|
||||||
</dt>
|
</dt>
|
||||||
<dd class="col-sm-7">
|
<dd class="col-sm-7">
|
||||||
<span id="server-installed">{{version}}</span>
|
<span id="server-installed">{{page_data.current_release}}</span>
|
||||||
</dd>
|
</dd>
|
||||||
<dt class="col-sm-5">Server Latest
|
<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>
|
<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="row">
|
||||||
<div class="col-md">
|
<div class="col-md">
|
||||||
<dl class="row">
|
<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>
|
<dt class="col-sm-5">Running within Docker</dt>
|
||||||
<dd class="col-sm-7">
|
<dd class="col-sm-7">
|
||||||
{{#if page_data.running_within_docker}}
|
{{#if page_data.running_within_docker}}
|
||||||
|
@ -140,8 +144,8 @@
|
||||||
<span><b>Server:</b> {{page_data.server_time_local}}</span>
|
<span><b>Server:</b> {{page_data.server_time_local}}</span>
|
||||||
</dd>
|
</dd>
|
||||||
<dt class="col-sm-5">Date & Time (UTC)
|
<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-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 30 seconds apart.">Error</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>
|
</dt>
|
||||||
<dd class="col-sm-7">
|
<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>
|
<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>
|
||||||
<dl class="row">
|
<dl class="row">
|
||||||
<dt class="col-sm-3">
|
<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>
|
<br><br>
|
||||||
<button type="button" id="copy-support" class="btn btn-info mb-3 d-none" onclick="copyToClipboard(); return false;">Copy To Clipboard</button>
|
<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" style="width: 15rem;">
|
<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 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">
|
<div class="toast-body">
|
||||||
Copied to clipboard!
|
Copied to clipboard!
|
||||||
|
@ -192,197 +196,12 @@
|
||||||
</div>
|
</div>
|
||||||
</dt>
|
</dt>
|
||||||
<dd class="col-sm-9">
|
<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>
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
<script src="{{urlpath}}/vw_static/admin_diagnostics.js"></script>
|
||||||
<script>
|
<script type="application/json" id="diagnostics_json">{{to_json page_data}}</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>
|
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
<th>Users</th>
|
<th>Users</th>
|
||||||
<th>Items</th>
|
<th>Items</th>
|
||||||
<th>Attachments</th>
|
<th>Attachments</th>
|
||||||
<th style="width: 130px; min-width: 130px;">Actions</th>
|
<th class="vw-actions">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
@ -21,7 +21,7 @@
|
||||||
<strong>{{Name}}</strong>
|
<strong>{{Name}}</strong>
|
||||||
<span class="me-2">({{BillingEmail}})</span>
|
<span class="me-2">({{BillingEmail}})</span>
|
||||||
<span class="d-block">
|
<span class="d-block">
|
||||||
<span class="badge bg-success">{{Id}}</span>
|
<span class="badge bg-success font-monospace">{{Id}}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
@ -38,49 +38,22 @@
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</td>
|
</td>
|
||||||
<td class="text-end px-0 small">
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3 clearfix">
|
||||||
|
<button type="button" class="btn btn-sm btn-primary float-end" id="reload">Reload organizations</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<link rel="stylesheet" href="{{urlpath}}/vw_static/datatables.css" />
|
<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/jquery-3.6.2.slim.js"></script>
|
||||||
<script src="{{urlpath}}/vw_static/datatables.js"></script>
|
<script src="{{urlpath}}/vw_static/datatables.js"></script>
|
||||||
<script>
|
<script src="{{urlpath}}/vw_static/admin_organizations.js"></script>
|
||||||
'use strict';
|
<script src="{{urlpath}}/vw_static/jdenticon.js"></script>
|
||||||
|
|
||||||
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>
|
|
||||||
|
|
|
@ -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>.
|
Settings which are overridden are shown with <span class="is-overridden-true alert-row px-1">a yellow colored background</span>.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form class="form needs-validation" id="config-form" onsubmit="saveConfig(); return false;" novalidate>
|
<form class="form needs-validation" id="config-form" novalidate>
|
||||||
{{#each config}}
|
{{#each page_data.config}}
|
||||||
{{#if groupdoc}}
|
{{#if groupdoc}}
|
||||||
<div class="card bg-light mb-3">
|
<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>
|
<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}}"
|
<input class="form-control conf-{{type}}" id="input_{{name}}" type="{{type}}"
|
||||||
name="{{name}}" value="{{value}}" {{#if default}} placeholder="Default: {{default}}"{{/if}}>
|
name="{{name}}" value="{{value}}" {{#if default}} placeholder="Default: {{default}}"{{/if}}>
|
||||||
{{#case type "password"}}
|
{{#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}}
|
{{/case}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -48,7 +48,7 @@
|
||||||
<label for="smtp-test-email" class="col-sm-3 col-form-label">Test SMTP</label>
|
<label for="smtp-test-email" class="col-sm-3 col-form-label">Test SMTP</label>
|
||||||
<div class="col-sm-8 input-group">
|
<div class="col-sm-8 input-group">
|
||||||
<input class="form-control" id="smtp-test-email" type="email" placeholder="Enter test email" required>
|
<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 class="invalid-tooltip">Please provide a valid email address</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -68,7 +68,7 @@
|
||||||
launching the server. You can check the variable names in the tooltips of each option.
|
launching the server. You can check the variable names in the tooltips of each option.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{#each config}}
|
{{#each page_data.config}}
|
||||||
{{#each elements}}
|
{{#each elements}}
|
||||||
{{#unless editable}}
|
{{#unless editable}}
|
||||||
<div class="row my-2 align-items-center alert-row" title="[{{name}}] {{doc.description}}">
|
<div class="row my-2 align-items-center alert-row" title="[{{name}}] {{doc.description}}">
|
||||||
|
@ -83,11 +83,11 @@
|
||||||
--}}
|
--}}
|
||||||
{{#if (eq name "database_url")}}
|
{{#if (eq name "database_url")}}
|
||||||
<input readonly class="form-control" id="input_{{name}}" type="password" value="{{value}}" {{#if default}} placeholder="Default: {{default}}" {{/if}}>
|
<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}}
|
{{else}}
|
||||||
<input readonly class="form-control" id="input_{{name}}" type="{{type}}" value="{{value}}" {{#if default}} placeholder="Default: {{default}}" {{/if}}>
|
<input readonly class="form-control" id="input_{{name}}" type="{{type}}" value="{{value}}" {{#if default}} placeholder="Default: {{default}}" {{/if}}>
|
||||||
{{#case type "password"}}
|
{{#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}}
|
{{/case}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
|
@ -112,7 +112,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{#if can_backup}}
|
{{#if page_data.can_backup}}
|
||||||
<div class="card bg-light mb-3">
|
<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"
|
<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>
|
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
|
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>.
|
<a href="https://github.com/dani-garcia/vaultwarden/wiki/Backing-up-your-vault" target="_blank" rel="noopener noreferrer">backups</a>.
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary">Save</button>
|
<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>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
#config-block ::placeholder {
|
#config-block ::placeholder {
|
||||||
/* Most modern browsers support this now. */
|
/* Most modern browsers support this now. */
|
||||||
|
@ -148,146 +147,4 @@
|
||||||
--bs-alert-border-color: #ffecb5;
|
--bs-alert-border-color: #ffecb5;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
<script src="{{urlpath}}/vw_static/admin_settings.js"></script>
|
||||||
<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>
|
|
||||||
|
|
|
@ -1,18 +1,17 @@
|
||||||
<main class="container-xl">
|
<main class="container-xl">
|
||||||
<div id="users-block" class="my-3 p-3 bg-white rounded shadow">
|
<div id="users-block" class="my-3 p-3 bg-white rounded shadow">
|
||||||
<h6 class="border-bottom pb-2 mb-3">Registered Users</h6>
|
<h6 class="border-bottom pb-2 mb-3">Registered Users</h6>
|
||||||
|
|
||||||
<div class="table-responsive-xl small">
|
<div class="table-responsive-xl small">
|
||||||
<table id="users-table" class="table table-sm table-striped table-hover">
|
<table id="users-table" class="table table-sm table-striped table-hover">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>User</th>
|
<th>User</th>
|
||||||
<th style="width: 85px; min-width: 70px;">Created at</th>
|
<th class="vw-created-at">Created at</th>
|
||||||
<th style="width: 85px; min-width: 70px;">Last Active</th>
|
<th class="vw-last-active">Last Active</th>
|
||||||
<th style="width: 35px; min-width: 35px;">Items</th>
|
<th class="vw-items">Items</th>
|
||||||
<th>Attachments</th>
|
<th class="vw-attachments">Attachments</th>
|
||||||
<th style="min-width: 120px;">Organizations</th>
|
<th class="vw-organizations">Organizations</th>
|
||||||
<th style="width: 130px; min-width: 130px;">Actions</th>
|
<th class="vw-actions">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
@ -55,23 +54,25 @@
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</td>
|
</td>
|
||||||
<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}}
|
{{#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}}
|
{{/each}}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-end px-0 small">
|
<td class="text-end px-0 small">
|
||||||
|
<span data-vw-user-uuid="{{jsesc Id no_quote}}" data-vw-user-email="{{jsesc Email no_quote}}">
|
||||||
{{#if TwoFactorEnabled}}
|
{{#if TwoFactorEnabled}}
|
||||||
<button type="button" class="btn btn-sm btn-link p-0 border-0" onclick='remove2fa({{jsesc Id}})'>Remove all 2FA</button>
|
<button type="button" class="btn btn-sm btn-link p-0 border-0" vw-remove2fa>Remove all 2FA</button>
|
||||||
{{/if}}
|
{{/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" vw-deauth-user>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>
|
<button type="button" class="btn btn-sm btn-link p-0 border-0" vw-delete-user>Delete User</button>
|
||||||
{{#if user_enabled}}
|
{{#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>
|
<button type="button" class="btn btn-sm btn-link p-0 border-0" vw-disable-user>Disable User</button>
|
||||||
{{else}}
|
{{else}}
|
||||||
<button type="button" class="btn btn-sm btn-link p-0 border-0" onclick='enableUser({{jsesc Id}}, {{jsesc Email}})'>Enable User</button>
|
<button type="button" class="btn btn-sm btn-link p-0 border-0" vw-enable-user>Enable User</button>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
|
@ -79,23 +80,23 @@
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-3">
|
<div class="mt-3 clearfix">
|
||||||
<button type="button" class="btn btn-sm btn-danger" onclick="updateRevisions();"
|
<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.">
|
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
|
Force clients to resync
|
||||||
</button>
|
</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>
|
</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>
|
<div>
|
||||||
<h6 class="mb-0 text-white">Invite User</h6>
|
<h6 class="mb-0 text-white">Invite User</h6>
|
||||||
<small>Email:</small>
|
<small>Email:</small>
|
||||||
|
|
||||||
<form class="form-inline input-group w-50" id="invite-form" onsubmit="inviteUser(); return false;">
|
<form class="form-inline input-group w-50" id="inviteUserForm">
|
||||||
<input type="email" class="form-control me-2" id="email-invite" placeholder="Enter email" required>
|
<input type="email" class="form-control me-2" id="inviteEmail" placeholder="Enter email" required>
|
||||||
<button type="submit" class="btn btn-primary">Invite</button>
|
<button type="submit" class="btn btn-primary">Invite</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -108,7 +109,7 @@
|
||||||
<h6 class="modal-title" id="userOrgTypeDialogTitle"></h6>
|
<h6 class="modal-title" id="userOrgTypeDialogTitle"></h6>
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
</div>
|
</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="user_uuid" id="userOrgTypeUserUuid" value="">
|
||||||
<input type="hidden" name="org_uuid" id="userOrgTypeOrgUuid" value="">
|
<input type="hidden" name="org_uuid" id="userOrgTypeOrgUuid" value="">
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
|
@ -138,150 +139,5 @@
|
||||||
<link rel="stylesheet" href="{{urlpath}}/vw_static/datatables.css" />
|
<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/jquery-3.6.2.slim.js"></script>
|
||||||
<script src="{{urlpath}}/vw_static/datatables.js"></script>
|
<script src="{{urlpath}}/vw_static/datatables.js"></script>
|
||||||
<script>
|
<script src="{{urlpath}}/vw_static/admin_users.js"></script>
|
||||||
'use strict';
|
<script src="{{urlpath}}/vw_static/jdenticon.js"></script>
|
||||||
|
|
||||||
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>
|
|
||||||
|
|
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 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.
|
// This is the same behaviour as upstream Bitwarden.
|
||||||
if !req_uri_path.ends_with("connector.html") {
|
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:
|
// # Frame Ancestors:
|
||||||
// Chrome Web Store: https://chrome.google.com/webstore/detail/bitwarden-free-password-m/nngceckbapebfimnlniiiahkandclblb
|
// 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
|
// 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'; \
|
base-uri 'self'; \
|
||||||
form-action 'self'; \
|
form-action 'self'; \
|
||||||
object-src 'self' blob:; \
|
object-src 'self' blob:; \
|
||||||
script-src 'self'{script_src}; \
|
script-src 'self'; \
|
||||||
style-src 'self' 'unsafe-inline'; \
|
style-src 'self' 'unsafe-inline'; \
|
||||||
child-src 'self' https://*.duosecurity.com https://*.duofederal.com; \
|
child-src 'self' https://*.duosecurity.com https://*.duofederal.com; \
|
||||||
frame-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.
|
/// 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.
|
/// 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() {
|
if Path::new("/etc/debian_version").exists() {
|
||||||
"Debian".to_string()
|
"Debian"
|
||||||
} else if Path::new("/etc/alpine-release").exists() {
|
} else if Path::new("/etc/alpine-release").exists() {
|
||||||
"Alpine".to_string()
|
"Alpine"
|
||||||
} else {
|
} else {
|
||||||
"Unknown".to_string()
|
"Unknown"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Laden …
In neuem Issue referenzieren