Updated the admin interface
Mostly updated the admin interface, also some small other items. - Added more diagnostic information to (hopefully) decrease issue reporting, or at least solve them quicker. - Added an option to generate a support string which can be used to copy/paste on the forum or during the creation of an issue. It will try to hide the sensitive information automatically. - Changed the `Created At` and `Last Active` info to be in a column and able to sort them in the users overview. - Some small layout changes. - Updated javascript and css files to the latest versions available. - Decreased the png file sizes using `oxipng` - Updated target='_blank' links to have rel='noreferrer' to prevent javascript window.opener modifications.
							
								
								
									
										100
									
								
								src/api/admin.rs
									
									
									
									
									
								
							
							
						
						| @@ -1,8 +1,9 @@ | ||||
| use once_cell::sync::Lazy; | ||||
| use serde::de::DeserializeOwned; | ||||
| use serde_json::Value; | ||||
| use std::process::Command; | ||||
| use std::{env, process::Command, time::Duration}; | ||||
|  | ||||
| use reqwest::{blocking::Client, header::USER_AGENT}; | ||||
| use rocket::{ | ||||
|     http::{Cookie, Cookies, SameSite}, | ||||
|     request::{self, FlashMessage, Form, FromRequest, Outcome, Request}, | ||||
| @@ -18,7 +19,7 @@ use crate::{ | ||||
|     db::{backup_database, models::*, DbConn, DbConnType}, | ||||
|     error::{Error, MapResult}, | ||||
|     mail, | ||||
|     util::{get_display_size, format_naive_datetime_local}, | ||||
|     util::{format_naive_datetime_local, get_display_size}, | ||||
|     CONFIG, | ||||
| }; | ||||
|  | ||||
| @@ -47,9 +48,20 @@ pub fn routes() -> Vec<Route> { | ||||
|         users_overview, | ||||
|         organizations_overview, | ||||
|         diagnostics, | ||||
|         get_diagnostics_config | ||||
|     ] | ||||
| } | ||||
|  | ||||
| static DB_TYPE: Lazy<&str> = Lazy::new(|| { | ||||
|     DbConnType::from_url(&CONFIG.database_url()) | ||||
|         .map(|t| match t { | ||||
|             DbConnType::sqlite => "SQLite", | ||||
|             DbConnType::mysql => "MySQL", | ||||
|             DbConnType::postgresql => "PostgreSQL", | ||||
|         }) | ||||
|         .unwrap_or("Unknown") | ||||
| }); | ||||
|  | ||||
| static CAN_BACKUP: Lazy<bool> = Lazy::new(|| { | ||||
|     DbConnType::from_url(&CONFIG.database_url()) | ||||
|         .map(|t| t == DbConnType::sqlite) | ||||
| @@ -307,7 +319,8 @@ fn users_overview(_token: AdminToken, conn: DbConn) -> ApiResult<Html<String>> { | ||||
|                 None => json!("Never") | ||||
|             }; | ||||
|             usr | ||||
|     }).collect(); | ||||
|         }) | ||||
|         .collect(); | ||||
|  | ||||
|     let text = AdminTemplateData::users(users_json).render()?; | ||||
|     Ok(Html(text)) | ||||
| @@ -362,14 +375,16 @@ fn update_revision_users(_token: AdminToken, conn: DbConn) -> EmptyResult { | ||||
| #[get("/organizations/overview")] | ||||
| fn organizations_overview(_token: AdminToken, conn: DbConn) -> ApiResult<Html<String>> { | ||||
|     let organizations = Organization::get_all(&conn); | ||||
|     let organizations_json: Vec<Value> = organizations.iter().map(|o| { | ||||
|     let organizations_json: Vec<Value> = organizations.iter() | ||||
|         .map(|o| { | ||||
|             let mut org = o.to_json(); | ||||
|             org["user_count"] = json!(UserOrganization::count_by_org(&o.uuid, &conn)); | ||||
|             org["cipher_count"] = json!(Cipher::count_by_org(&o.uuid, &conn)); | ||||
|             org["attachment_count"] = json!(Attachment::count_by_org(&o.uuid, &conn)); | ||||
|             org["attachment_size"] = json!(get_display_size(Attachment::size_by_org(&o.uuid, &conn) as i32)); | ||||
|             org | ||||
|     }).collect(); | ||||
|         }) | ||||
|         .collect(); | ||||
|  | ||||
|     let text = AdminTemplateData::organizations(organizations_json).render()?; | ||||
|     Ok(Html(text)) | ||||
| @@ -391,77 +406,104 @@ struct GitCommit { | ||||
| } | ||||
|  | ||||
| fn get_github_api<T: DeserializeOwned>(url: &str) -> Result<T, Error> { | ||||
|     use reqwest::{blocking::Client, header::USER_AGENT}; | ||||
|     use std::time::Duration; | ||||
|     let github_api = Client::builder().build()?; | ||||
|  | ||||
|     Ok( | ||||
|         github_api.get(url) | ||||
|     Ok(github_api | ||||
|         .get(url) | ||||
|         .timeout(Duration::from_secs(10)) | ||||
|         .header(USER_AGENT, "Bitwarden_RS") | ||||
|         .send()? | ||||
|         .error_for_status()? | ||||
|         .json::<T>()? | ||||
|     ) | ||||
|         .json::<T>()?) | ||||
| } | ||||
|  | ||||
| fn has_http_access() -> bool { | ||||
|     let http_access = Client::builder().build().unwrap(); | ||||
|  | ||||
|     match http_access | ||||
|         .head("https://github.com/dani-garcia/bitwarden_rs") | ||||
|         .timeout(Duration::from_secs(10)) | ||||
|         .header(USER_AGENT, "Bitwarden_RS") | ||||
|         .send() | ||||
|     { | ||||
|         Ok(r) => r.status().is_success(), | ||||
|         _ => false, | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[get("/diagnostics")] | ||||
| fn diagnostics(_token: AdminToken, _conn: DbConn) -> ApiResult<Html<String>> { | ||||
|     use std::net::ToSocketAddrs; | ||||
|     use chrono::prelude::*; | ||||
|     use crate::util::read_file_string; | ||||
|     use chrono::prelude::*; | ||||
|     use std::net::ToSocketAddrs; | ||||
|  | ||||
|     // Get current running versions | ||||
|     let vault_version_path = format!("{}/{}", CONFIG.web_vault_folder(), "version.json"); | ||||
|     let vault_version_str = read_file_string(&vault_version_path)?; | ||||
|     let web_vault_version: WebVaultVersion = serde_json::from_str(&vault_version_str)?; | ||||
|  | ||||
|     let github_ips = ("github.com", 0).to_socket_addrs().map(|mut i| i.next()); | ||||
|     let (dns_resolved, dns_ok) = match github_ips { | ||||
|         Ok(Some(a)) => (a.ip().to_string(), true), | ||||
|         _ => ("Could not resolve domain name.".to_string(), false), | ||||
|     // Execute some environment checks | ||||
|     let running_within_docker = std::path::Path::new("/.dockerenv").exists(); | ||||
|     let has_http_access = has_http_access(); | ||||
|     let uses_proxy = env::var_os("HTTP_PROXY").is_some() | ||||
|         || env::var_os("http_proxy").is_some() | ||||
|         || env::var_os("HTTPS_PROXY").is_some() | ||||
|         || env::var_os("https_proxy").is_some(); | ||||
|  | ||||
|     // Check if we are able to resolve DNS entries | ||||
|     let dns_resolved = match ("github.com", 0).to_socket_addrs().map(|mut i| i.next()) { | ||||
|         Ok(Some(a)) => a.ip().to_string(), | ||||
|         _ => "Could not resolve domain name.".to_string(), | ||||
|     }; | ||||
|  | ||||
|     // If the DNS Check failed, do not even attempt to check for new versions since we were not able to resolve github.com | ||||
|     let (latest_release, latest_commit, latest_web_build) = if dns_ok { | ||||
|     // If the HTTP Check failed, do not even attempt to check for new versions since we were not able to connect with github.com anyway. | ||||
|     // TODO: Maybe we need to cache this using a LazyStatic or something. Github only allows 60 requests per hour, and we use 3 here already. | ||||
|     let (latest_release, latest_commit, latest_web_build) = if has_http_access { | ||||
|         ( | ||||
|             match get_github_api::<GitRelease>("https://api.github.com/repos/dani-garcia/bitwarden_rs/releases/latest") { | ||||
|                 Ok(r) => r.tag_name, | ||||
|                 _ => "-".to_string() | ||||
|                 _ => "-".to_string(), | ||||
|             }, | ||||
|             match get_github_api::<GitCommit>("https://api.github.com/repos/dani-garcia/bitwarden_rs/commits/master") { | ||||
|                 Ok(mut c) => { | ||||
|                     c.sha.truncate(8); | ||||
|                     c.sha | ||||
|             }, | ||||
|                 _ => "-".to_string() | ||||
|                 } | ||||
|                 _ => "-".to_string(), | ||||
|             }, | ||||
|             match get_github_api::<GitRelease>("https://api.github.com/repos/dani-garcia/bw_web_builds/releases/latest") { | ||||
|                 Ok(r) => r.tag_name.trim_start_matches('v').to_string(), | ||||
|                 _ => "-".to_string() | ||||
|                 _ => "-".to_string(), | ||||
|             }, | ||||
|         ) | ||||
|     } else { | ||||
|         ("-".to_string(), "-".to_string(), "-".to_string()) | ||||
|     }; | ||||
|  | ||||
|     // Run the date check as the last item right before filling the json. | ||||
|     // This should ensure that the time difference between the browser and the server is as minimal as possible. | ||||
|     let dt = Utc::now(); | ||||
|     let server_time = dt.format("%Y-%m-%d %H:%M:%S UTC").to_string(); | ||||
|  | ||||
|     let diagnostics_json = json!({ | ||||
|         "dns_resolved": dns_resolved, | ||||
|         "server_time": server_time, | ||||
|         "web_vault_version": web_vault_version.version, | ||||
|         "latest_release": latest_release, | ||||
|         "latest_commit": latest_commit, | ||||
|         "latest_web_build": latest_web_build, | ||||
|         "running_within_docker": running_within_docker, | ||||
|         "has_http_access": has_http_access, | ||||
|         "uses_proxy": uses_proxy, | ||||
|         "db_type": *DB_TYPE, | ||||
|         "admin_url": format!("{}/diagnostics", admin_url(Referer(None))), | ||||
|         "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::diagnostics(diagnostics_json).render()?; | ||||
|     Ok(Html(text)) | ||||
| } | ||||
|  | ||||
| #[get("/diagnostics/config")] | ||||
| fn get_diagnostics_config(_token: AdminToken) -> JsonResult { | ||||
|     let support_json = CONFIG.get_support_json(); | ||||
|     Ok(Json(support_json)) | ||||
| } | ||||
|  | ||||
| #[post("/config", data = "<data>")] | ||||
| fn post_config(data: Json<ConfigBuilder>, _token: AdminToken) -> EmptyResult { | ||||
|     let data: ConfigBuilder = data.into_inner(); | ||||
|   | ||||
| @@ -172,7 +172,7 @@ fn hibp_breach(username: String) -> JsonResult { | ||||
|             "Domain": "haveibeenpwned.com", | ||||
|             "BreachDate": "2019-08-18T00:00:00Z", | ||||
|             "AddedDate": "2019-08-18T00:00:00Z", | ||||
|             "Description": format!("Go to: <a href=\"https://haveibeenpwned.com/account/{account}\" target=\"_blank\" rel=\"noopener\">https://haveibeenpwned.com/account/{account}</a> for a manual check.<br/><br/>HaveIBeenPwned API key not set!<br/>Go to <a href=\"https://haveibeenpwned.com/API/Key\" target=\"_blank\" rel=\"noopener\">https://haveibeenpwned.com/API/Key</a> to purchase an API key from HaveIBeenPwned.<br/><br/>", account=username), | ||||
|             "Description": format!("Go to: <a href=\"https://haveibeenpwned.com/account/{account}\" target=\"_blank\" rel=\"noreferrer\">https://haveibeenpwned.com/account/{account}</a> for a manual check.<br/><br/>HaveIBeenPwned API key not set!<br/>Go to <a href=\"https://haveibeenpwned.com/API/Key\" target=\"_blank\" rel=\"noreferrer\">https://haveibeenpwned.com/API/Key</a> to purchase an API key from HaveIBeenPwned.<br/><br/>", account=username), | ||||
|             "LogoPath": "bwrs_static/hibp.png", | ||||
|             "PwnCount": 0, | ||||
|             "DataClasses": [ | ||||
|   | ||||
| @@ -2,6 +2,7 @@ use std::process::exit; | ||||
| use std::sync::RwLock; | ||||
|  | ||||
| use once_cell::sync::Lazy; | ||||
| use regex::Regex; | ||||
| use reqwest::Url; | ||||
|  | ||||
| use crate::{ | ||||
| @@ -22,6 +23,21 @@ pub static CONFIG: Lazy<Config> = Lazy::new(|| { | ||||
|     }) | ||||
| }); | ||||
|  | ||||
| static PRIVACY_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"[\w]").unwrap()); | ||||
| const PRIVACY_CONFIG: &[&str] = &[ | ||||
|     "allowed_iframe_ancestors", | ||||
|     "database_url", | ||||
|     "domain_origin", | ||||
|     "domain_path", | ||||
|     "domain", | ||||
|     "helo_name", | ||||
|     "org_creation_users", | ||||
|     "signups_domains_whitelist", | ||||
|     "smtp_from", | ||||
|     "smtp_host", | ||||
|     "smtp_username", | ||||
| ]; | ||||
|  | ||||
| pub type Pass = String; | ||||
|  | ||||
| macro_rules! make_config { | ||||
| @@ -52,6 +68,7 @@ macro_rules! make_config { | ||||
|         } | ||||
|  | ||||
|         impl ConfigBuilder { | ||||
|             #[allow(clippy::field_reassign_with_default)] | ||||
|             fn from_env() -> Self { | ||||
|                 match dotenv::from_path(".env") { | ||||
|                     Ok(_) => (), | ||||
| @@ -196,9 +213,38 @@ macro_rules! make_config { | ||||
|                     }, )+ | ||||
|                     ]}, )+ ]) | ||||
|             } | ||||
|  | ||||
|             pub fn get_support_json(&self) -> serde_json::Value { | ||||
|                 let cfg = { | ||||
|                     let inner = &self.inner.read().unwrap(); | ||||
|                     inner.config.clone() | ||||
|                 }; | ||||
|  | ||||
|                 json!({ $($( | ||||
|                     stringify!($name): make_config!{ @supportstr $name, cfg.$name, $ty, $none_action }, | ||||
|                 )+)+ }) | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     // Support string print | ||||
|     ( @supportstr $name:ident, $value:expr, Pass, option ) => { $value.as_ref().map(|_| String::from("***")) }; // Optional pass, we map to an Option<String> with "***" | ||||
|     ( @supportstr $name:ident, $value:expr, Pass, $none_action:ident ) => { String::from("***") }; // Required pass, we return "***" | ||||
|     ( @supportstr $name:ident, $value:expr, $ty:ty, option ) => { // Optional other value, we return as is or convert to string to apply the privacy config | ||||
|         if PRIVACY_CONFIG.contains(&stringify!($name)) { | ||||
|             json!($value.as_ref().map(|x| PRIVACY_REGEX.replace_all(&x.to_string(), "${1}*").to_string())) | ||||
|         } else { | ||||
|             json!($value) | ||||
|         } | ||||
|     }; | ||||
|     ( @supportstr $name:ident, $value:expr, $ty:ty, $none_action:ident ) => { // Required other value, we return as is or convert to string to apply the privacy config | ||||
|         if PRIVACY_CONFIG.contains(&stringify!($name)) { | ||||
|              json!(PRIVACY_REGEX.replace_all(&$value.to_string(), "${1}*").to_string()) | ||||
|          } else { | ||||
|              json!($value) | ||||
|          } | ||||
|     }; | ||||
|  | ||||
|     // Group or empty string | ||||
|     ( @show ) => { "" }; | ||||
|     ( @show $lit:literal ) => { $lit }; | ||||
| @@ -458,7 +504,6 @@ make_config! { | ||||
| } | ||||
|  | ||||
| fn validate_config(cfg: &ConfigItems) -> Result<(), Error> { | ||||
|  | ||||
|     // Validate connection URL is valid and DB feature is enabled | ||||
|     DbConnType::from_url(&cfg.database_url)?; | ||||
|  | ||||
| @@ -472,7 +517,9 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> { | ||||
|  | ||||
|     let dom = cfg.domain.to_lowercase(); | ||||
|     if !dom.starts_with("http://") && !dom.starts_with("https://") { | ||||
|         err!("DOMAIN variable needs to contain the protocol (http, https). Use 'http[s]://bw.example.com' instead of 'bw.example.com'"); | ||||
|         err!( | ||||
|             "DOMAIN variable needs to contain the protocol (http, https). Use 'http[s]://bw.example.com' instead of 'bw.example.com'" | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     let whitelist = &cfg.signups_domains_whitelist; | ||||
| @@ -481,10 +528,10 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> { | ||||
|     } | ||||
|  | ||||
|     let org_creation_users = cfg.org_creation_users.trim().to_lowercase(); | ||||
|     if !(org_creation_users.is_empty() || org_creation_users == "all" || org_creation_users == "none") { | ||||
|         if org_creation_users.split(',').any(|u| !u.contains('@')) { | ||||
|             err!("`ORG_CREATION_USERS` contains invalid email addresses"); | ||||
|         } | ||||
|     if !(org_creation_users.is_empty() || org_creation_users == "all" || org_creation_users == "none") | ||||
|         && org_creation_users.split(',').any(|u| !u.contains('@')) | ||||
|     { | ||||
|         err!("`ORG_CREATION_USERS` contains invalid email addresses"); | ||||
|     } | ||||
|  | ||||
|     if let Some(ref token) = cfg.admin_token { | ||||
| @@ -529,7 +576,6 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> { | ||||
|  | ||||
|     // Check if the icon blacklist regex is valid | ||||
|     if let Some(ref r) = cfg.icon_blacklist_regex { | ||||
|         use regex::Regex; | ||||
|         let validate_regex = Regex::new(&r); | ||||
|         match validate_regex { | ||||
|             Ok(_) => (), | ||||
| @@ -577,7 +623,12 @@ impl Config { | ||||
|         validate_config(&config)?; | ||||
|  | ||||
|         Ok(Config { | ||||
|             inner: RwLock::new(Inner { templates: load_templates(&config.templates_folder), config, _env, _usr }), | ||||
|             inner: RwLock::new(Inner { | ||||
|                 templates: load_templates(&config.templates_folder), | ||||
|                 config, | ||||
|                 _env, | ||||
|                 _usr, | ||||
|             }), | ||||
|         }) | ||||
|     } | ||||
|  | ||||
| @@ -650,7 +701,7 @@ impl Config { | ||||
|     /// Tests whether the specified user is allowed to create an organization. | ||||
|     pub fn is_org_creation_allowed(&self, email: &str) -> bool { | ||||
|         let users = self.org_creation_users(); | ||||
|         if users == "" || users == "all" { | ||||
|         if users.is_empty() || users == "all" { | ||||
|             true | ||||
|         } else if users == "none" { | ||||
|             false | ||||
| @@ -704,8 +755,10 @@ impl Config { | ||||
|             let akey_s = data_encoding::BASE64.encode(&akey); | ||||
|  | ||||
|             // Save the new value | ||||
|             let mut builder = ConfigBuilder::default(); | ||||
|             builder._duo_akey = Some(akey_s.clone()); | ||||
|             let builder = ConfigBuilder { | ||||
|                 _duo_akey: Some(akey_s.clone()), | ||||
|                 ..Default::default() | ||||
|             }; | ||||
|             self.update_config_partial(builder).ok(); | ||||
|  | ||||
|             akey_s | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| #![forbid(unsafe_code)] | ||||
| #![cfg_attr(feature = "unstable", feature(ip))] | ||||
| #![recursion_limit = "256"] | ||||
| #![recursion_limit = "512"] | ||||
|  | ||||
| extern crate openssl; | ||||
| #[macro_use] | ||||
|   | ||||
| Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 9.2 KiB | 
| Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 5.3 KiB | 
| Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB | 
| Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.8 KiB | 
							
								
								
									
										55
									
								
								src/static/scripts/bootstrap-native.js
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,6 +1,6 @@ | ||||
| /*! | ||||
|   * Native JavaScript for Bootstrap v3.0.10 (https://thednp.github.io/bootstrap.native/) | ||||
|   * Copyright 2015-2020 © dnp_theme | ||||
|   * Native JavaScript for Bootstrap v3.0.15 (https://thednp.github.io/bootstrap.native/) | ||||
|   * Copyright 2015-2021 © dnp_theme | ||||
|   * Licensed under MIT (https://github.com/thednp/bootstrap.native/blob/master/LICENSE) | ||||
|   */ | ||||
|  (function (global, factory) { | ||||
| @@ -15,10 +15,14 @@ | ||||
|  | ||||
|   var transitionDuration = 'webkitTransition' in document.head.style ? 'webkitTransitionDuration' : 'transitionDuration'; | ||||
|  | ||||
|   var transitionProperty = 'webkitTransition' in document.head.style ? 'webkitTransitionProperty' : 'transitionProperty'; | ||||
|  | ||||
|   function getElementTransitionDuration(element) { | ||||
|     var duration = supportTransition ? parseFloat(getComputedStyle(element)[transitionDuration]) : 0; | ||||
|     duration = typeof duration === 'number' && !isNaN(duration) ? duration * 1000 : 0; | ||||
|     return duration; | ||||
|     var computedStyle = getComputedStyle(element), | ||||
|         property = computedStyle[transitionProperty], | ||||
|         duration = supportTransition && property && property !== 'none' | ||||
|                  ? parseFloat(computedStyle[transitionDuration]) : 0; | ||||
|     return !isNaN(duration) ? duration * 1000 : 0; | ||||
|   } | ||||
|  | ||||
|   function emulateTransitionEnd(element,handler){ | ||||
| @@ -35,9 +39,15 @@ | ||||
|     return selector instanceof Element ? selector : lookUp.querySelector(selector); | ||||
|   } | ||||
|  | ||||
|   function bootstrapCustomEvent(eventName, componentName, related) { | ||||
|   function bootstrapCustomEvent(eventName, componentName, eventProperties) { | ||||
|     var OriginalCustomEvent = new CustomEvent( eventName + '.bs.' + componentName, {cancelable: true}); | ||||
|     OriginalCustomEvent.relatedTarget = related; | ||||
|     if (typeof eventProperties !== 'undefined') { | ||||
|       Object.keys(eventProperties).forEach(function (key) { | ||||
|         Object.defineProperty(OriginalCustomEvent, key, { | ||||
|           value: eventProperties[key] | ||||
|         }); | ||||
|       }); | ||||
|     } | ||||
|     return OriginalCustomEvent; | ||||
|   } | ||||
|  | ||||
| @@ -352,7 +362,7 @@ | ||||
|     }; | ||||
|     self.slideTo = function (next) { | ||||
|       if (vars.isSliding) { return; } | ||||
|       var activeItem = self.getActiveIndex(), orientation; | ||||
|       var activeItem = self.getActiveIndex(), orientation, eventProperties; | ||||
|       if ( activeItem === next ) { | ||||
|         return; | ||||
|       } else if  ( (activeItem < next ) || (activeItem === 0 && next === slides.length -1 ) ) { | ||||
| @@ -363,8 +373,9 @@ | ||||
|       if ( next < 0 ) { next = slides.length - 1; } | ||||
|       else if ( next >= slides.length ){ next = 0; } | ||||
|       orientation = vars.direction === 'left' ? 'next' : 'prev'; | ||||
|       slideCustomEvent = bootstrapCustomEvent('slide', 'carousel', slides[next]); | ||||
|       slidCustomEvent = bootstrapCustomEvent('slid', 'carousel', slides[next]); | ||||
|       eventProperties = { relatedTarget: slides[next], direction: vars.direction, from: activeItem, to: next }; | ||||
|       slideCustomEvent = bootstrapCustomEvent('slide', 'carousel', eventProperties); | ||||
|       slidCustomEvent = bootstrapCustomEvent('slid', 'carousel', eventProperties); | ||||
|       dispatchCustomEvent.call(element, slideCustomEvent); | ||||
|       if (slideCustomEvent.defaultPrevented) { return; } | ||||
|       vars.index = next; | ||||
| @@ -615,7 +626,7 @@ | ||||
|       } | ||||
|     } | ||||
|     self.show = function () { | ||||
|       showCustomEvent = bootstrapCustomEvent('show', 'dropdown', relatedTarget); | ||||
|       showCustomEvent = bootstrapCustomEvent('show', 'dropdown', { relatedTarget: relatedTarget }); | ||||
|       dispatchCustomEvent.call(parent, showCustomEvent); | ||||
|       if ( showCustomEvent.defaultPrevented ) { return; } | ||||
|       menu.classList.add('show'); | ||||
| @@ -626,12 +637,12 @@ | ||||
|       setTimeout(function () { | ||||
|         setFocus( menu.getElementsByTagName('INPUT')[0] || element ); | ||||
|         toggleDismiss(); | ||||
|         shownCustomEvent = bootstrapCustomEvent( 'shown', 'dropdown', relatedTarget); | ||||
|         shownCustomEvent = bootstrapCustomEvent('shown', 'dropdown', { relatedTarget: relatedTarget }); | ||||
|         dispatchCustomEvent.call(parent, shownCustomEvent); | ||||
|       },1); | ||||
|     }; | ||||
|     self.hide = function () { | ||||
|       hideCustomEvent = bootstrapCustomEvent('hide', 'dropdown', relatedTarget); | ||||
|       hideCustomEvent = bootstrapCustomEvent('hide', 'dropdown', { relatedTarget: relatedTarget }); | ||||
|       dispatchCustomEvent.call(parent, hideCustomEvent); | ||||
|       if ( hideCustomEvent.defaultPrevented ) { return; } | ||||
|       menu.classList.remove('show'); | ||||
| @@ -643,7 +654,7 @@ | ||||
|       setTimeout(function () { | ||||
|         element.Dropdown && element.addEventListener('click',clickHandler,false); | ||||
|       },1); | ||||
|       hiddenCustomEvent = bootstrapCustomEvent('hidden', 'dropdown', relatedTarget); | ||||
|       hiddenCustomEvent = bootstrapCustomEvent('hidden', 'dropdown', { relatedTarget: relatedTarget }); | ||||
|       dispatchCustomEvent.call(parent, hiddenCustomEvent); | ||||
|     }; | ||||
|     self.toggle = function () { | ||||
| @@ -749,7 +760,7 @@ | ||||
|       setFocus(modal); | ||||
|       modal.isAnimating = false; | ||||
|       toggleEvents(1); | ||||
|       shownCustomEvent = bootstrapCustomEvent('shown', 'modal', relatedTarget); | ||||
|       shownCustomEvent = bootstrapCustomEvent('shown', 'modal', { relatedTarget: relatedTarget }); | ||||
|       dispatchCustomEvent.call(modal, shownCustomEvent); | ||||
|     } | ||||
|     function triggerHide(force) { | ||||
| @@ -804,7 +815,7 @@ | ||||
|     }; | ||||
|     self.show = function () { | ||||
|       if (modal.classList.contains('show') && !!modal.isAnimating ) {return} | ||||
|       showCustomEvent = bootstrapCustomEvent('show', 'modal', relatedTarget); | ||||
|       showCustomEvent = bootstrapCustomEvent('show', 'modal', { relatedTarget: relatedTarget }); | ||||
|       dispatchCustomEvent.call(modal, showCustomEvent); | ||||
|       if ( showCustomEvent.defaultPrevented ) { return; } | ||||
|       modal.isAnimating = true; | ||||
| @@ -1193,7 +1204,7 @@ | ||||
|         if (dropLink && !dropLink.classList.contains('active') ) { | ||||
|           dropLink.classList.add('active'); | ||||
|         } | ||||
|         dispatchCustomEvent.call(element, bootstrapCustomEvent( 'activate', 'scrollspy', vars.items[index])); | ||||
|         dispatchCustomEvent.call(element, bootstrapCustomEvent( 'activate', 'scrollspy', { relatedTarget: vars.items[index] })); | ||||
|       } else if ( isActive && !inside ) { | ||||
|         item.classList.remove('active'); | ||||
|         if (dropLink && dropLink.classList.contains('active') && !item.parentNode.getElementsByClassName('active').length ) { | ||||
| @@ -1278,7 +1289,7 @@ | ||||
|       } else { | ||||
|         tabs.isAnimating = false; | ||||
|       } | ||||
|       shownCustomEvent = bootstrapCustomEvent('shown', 'tab', activeTab); | ||||
|       shownCustomEvent = bootstrapCustomEvent('shown', 'tab', { relatedTarget: activeTab }); | ||||
|       dispatchCustomEvent.call(next, shownCustomEvent); | ||||
|     } | ||||
|     function triggerHide() { | ||||
| @@ -1287,8 +1298,8 @@ | ||||
|         nextContent.style.float = 'left'; | ||||
|         containerHeight = activeContent.scrollHeight; | ||||
|       } | ||||
|       showCustomEvent = bootstrapCustomEvent('show', 'tab', activeTab); | ||||
|       hiddenCustomEvent = bootstrapCustomEvent('hidden', 'tab', next); | ||||
|       showCustomEvent = bootstrapCustomEvent('show', 'tab', { relatedTarget: activeTab }); | ||||
|       hiddenCustomEvent = bootstrapCustomEvent('hidden', 'tab', { relatedTarget: next }); | ||||
|       dispatchCustomEvent.call(next, showCustomEvent); | ||||
|       if ( showCustomEvent.defaultPrevented ) { return; } | ||||
|       nextContent.classList.add('active'); | ||||
| @@ -1331,7 +1342,7 @@ | ||||
|         nextContent = queryElement(next.getAttribute('href')); | ||||
|         activeTab = getActiveTab(); | ||||
|         activeContent = getActiveContent(); | ||||
|         hideCustomEvent = bootstrapCustomEvent( 'hide', 'tab', next); | ||||
|         hideCustomEvent = bootstrapCustomEvent( 'hide', 'tab', { relatedTarget: next }); | ||||
|         dispatchCustomEvent.call(activeTab, hideCustomEvent); | ||||
|         if (hideCustomEvent.defaultPrevented) { return; } | ||||
|         tabs.isAnimating = true; | ||||
| @@ -1637,7 +1648,7 @@ | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   var version = "3.0.10"; | ||||
|   var version = "3.0.15"; | ||||
|  | ||||
|   var index = { | ||||
|     Alert: Alert, | ||||
|   | ||||
							
								
								
									
										11
									
								
								src/static/scripts/bootstrap.css
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,10 +1,10 @@ | ||||
| /*! | ||||
|  * Bootstrap v4.5.2 (https://getbootstrap.com/) | ||||
|  * Bootstrap v4.5.3 (https://getbootstrap.com/) | ||||
|  * Copyright 2011-2020 The Bootstrap Authors | ||||
|  * Copyright 2011-2020 Twitter, Inc. | ||||
|  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) | ||||
|  */ | ||||
|  :root { | ||||
| :root { | ||||
|   --blue: #007bff; | ||||
|   --indigo: #6610f2; | ||||
|   --purple: #6f42c1; | ||||
| @@ -216,6 +216,7 @@ caption { | ||||
|  | ||||
| th { | ||||
|   text-align: inherit; | ||||
|   text-align: -webkit-match-parent; | ||||
| } | ||||
|  | ||||
| label { | ||||
| @@ -3750,6 +3751,8 @@ input[type="button"].btn-block { | ||||
|   display: block; | ||||
|   min-height: 1.5rem; | ||||
|   padding-left: 1.5rem; | ||||
|   -webkit-print-color-adjust: exact; | ||||
|   color-adjust: exact; | ||||
| } | ||||
|  | ||||
| .custom-control-inline { | ||||
| @@ -5289,6 +5292,7 @@ a.badge-dark:focus, a.badge-dark.focus { | ||||
|   position: absolute; | ||||
|   top: 0; | ||||
|   right: 0; | ||||
|   z-index: 2; | ||||
|   padding: 0.75rem 1.25rem; | ||||
|   color: inherit; | ||||
| } | ||||
| @@ -10163,7 +10167,7 @@ a.text-dark:hover, a.text-dark:focus { | ||||
|  | ||||
| .text-break { | ||||
|   word-break: break-word !important; | ||||
|   overflow-wrap: break-word !important; | ||||
|   word-wrap: break-word !important; | ||||
| } | ||||
|  | ||||
| .text-reset { | ||||
| @@ -10256,3 +10260,4 @@ a.text-dark:hover, a.text-dark:focus { | ||||
|     border-color: #dee2e6; | ||||
|   } | ||||
| } | ||||
| /*# sourceMappingURL=bootstrap.css.map */ | ||||
| @@ -4,12 +4,13 @@ | ||||
|  * | ||||
|  * To rebuild or modify this file with the latest versions of the included | ||||
|  * software please visit: | ||||
|  *   https://datatables.net/download/#bs4/dt-1.10.22 | ||||
|  *   https://datatables.net/download/#bs4/dt-1.10.23 | ||||
|  * | ||||
|  * Included libraries: | ||||
|  *   DataTables 1.10.22 | ||||
|  *   DataTables 1.10.23 | ||||
|  */ | ||||
|  | ||||
| @charset "UTF-8"; | ||||
| table.dataTable { | ||||
|   clear: both; | ||||
|   margin-top: 6px !important; | ||||
| @@ -114,7 +115,7 @@ table.dataTable > thead .sorting_desc:before, | ||||
| table.dataTable > thead .sorting_asc_disabled:before, | ||||
| table.dataTable > thead .sorting_desc_disabled:before { | ||||
|   right: 1em; | ||||
|   content: "\2191"; | ||||
|   content: "↑"; | ||||
| } | ||||
| table.dataTable > thead .sorting:after, | ||||
| table.dataTable > thead .sorting_asc:after, | ||||
| @@ -122,7 +123,7 @@ table.dataTable > thead .sorting_desc:after, | ||||
| table.dataTable > thead .sorting_asc_disabled:after, | ||||
| table.dataTable > thead .sorting_desc_disabled:after { | ||||
|   right: 0.5em; | ||||
|   content: "\2193"; | ||||
|   content: "↓"; | ||||
| } | ||||
| table.dataTable > thead .sorting_asc:before, | ||||
| table.dataTable > thead .sorting_desc:after { | ||||
| @@ -165,9 +166,9 @@ div.dataTables_scrollFoot > .dataTables_scrollFootInner > table { | ||||
|  | ||||
| @media screen and (max-width: 767px) { | ||||
|   div.dataTables_wrapper div.dataTables_length, | ||||
|   div.dataTables_wrapper div.dataTables_filter, | ||||
|   div.dataTables_wrapper div.dataTables_info, | ||||
|   div.dataTables_wrapper div.dataTables_paginate { | ||||
| div.dataTables_wrapper div.dataTables_filter, | ||||
| div.dataTables_wrapper div.dataTables_info, | ||||
| div.dataTables_wrapper div.dataTables_paginate { | ||||
|     text-align: center; | ||||
|   } | ||||
|   div.dataTables_wrapper div.dataTables_paginate ul.pagination { | ||||
| @@ -213,10 +214,10 @@ div.dataTables_scrollHead table.table-bordered { | ||||
| div.table-responsive > div.dataTables_wrapper > div.row { | ||||
|   margin: 0; | ||||
| } | ||||
| div.table-responsive > div.dataTables_wrapper > div.row > div[class^="col-"]:first-child { | ||||
| div.table-responsive > div.dataTables_wrapper > div.row > div[class^=col-]:first-child { | ||||
|   padding-left: 0; | ||||
| } | ||||
| div.table-responsive > div.dataTables_wrapper > div.row > div[class^="col-"]:last-child { | ||||
| div.table-responsive > div.dataTables_wrapper > div.row > div[class^=col-]:last-child { | ||||
|   padding-right: 0; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -4,20 +4,20 @@ | ||||
|  * | ||||
|  * To rebuild or modify this file with the latest versions of the included | ||||
|  * software please visit: | ||||
|  *   https://datatables.net/download/#bs4/dt-1.10.22 | ||||
|  *   https://datatables.net/download/#bs4/dt-1.10.23 | ||||
|  * | ||||
|  * Included libraries: | ||||
|  *   DataTables 1.10.22 | ||||
|  *   DataTables 1.10.23 | ||||
|  */ | ||||
|  | ||||
| /*! DataTables 1.10.22 | ||||
| /*! DataTables 1.10.23 | ||||
|  * ©2008-2020 SpryMedia Ltd - datatables.net/license | ||||
|  */ | ||||
|  | ||||
| /** | ||||
|  * @summary     DataTables | ||||
|  * @description Paginate, search and order HTML tables | ||||
|  * @version     1.10.22 | ||||
|  * @version     1.10.23 | ||||
|  * @file        jquery.dataTables.js | ||||
|  * @author      SpryMedia Ltd | ||||
|  * @contact     www.datatables.net | ||||
| @@ -2775,7 +2775,7 @@ | ||||
| 				for ( var i=0, iLen=a.length-1 ; i<iLen ; i++ ) | ||||
| 				{ | ||||
| 					// Protect against prototype pollution | ||||
| 					if (a[i] === '__proto__') { | ||||
| 					if (a[i] === '__proto__' || a[i] === 'constructor') { | ||||
| 						throw new Error('Cannot set prototype values'); | ||||
| 					} | ||||
| 	 | ||||
| @@ -3157,7 +3157,7 @@ | ||||
| 				cells.push( nTd ); | ||||
| 	 | ||||
| 				// Need to create the HTML if new, or if a rendering function is defined | ||||
| 				if ( create || ((!nTrIn || oCol.mRender || oCol.mData !== i) && | ||||
| 				if ( create || ((oCol.mRender || oCol.mData !== i) && | ||||
| 					 (!$.isPlainObject(oCol.mData) || oCol.mData._ !== i+'.display') | ||||
| 				)) { | ||||
| 					nTd.innerHTML = _fnGetCellData( oSettings, iRow, i, 'display' ); | ||||
| @@ -3189,10 +3189,6 @@ | ||||
| 	 | ||||
| 			_fnCallbackFire( oSettings, 'aoRowCreatedCallback', null, [nTr, rowData, iRow, cells] ); | ||||
| 		} | ||||
| 	 | ||||
| 		// Remove once webkit bug 131819 and Chromium bug 365619 have been resolved | ||||
| 		// and deployed | ||||
| 		row.nTr.setAttribute( 'role', 'row' ); | ||||
| 	} | ||||
| 	 | ||||
| 	 | ||||
| @@ -9546,7 +9542,7 @@ | ||||
| 	 *  @type string | ||||
| 	 *  @default Version number | ||||
| 	 */ | ||||
| 	DataTable.version = "1.10.22"; | ||||
| 	DataTable.version = "1.10.23"; | ||||
|  | ||||
| 	/** | ||||
| 	 * Private data store, containing all of the settings objects that are | ||||
| @@ -13970,7 +13966,7 @@ | ||||
| 		 * | ||||
| 		 *  @type string | ||||
| 		 */ | ||||
| 		build:"bs4/dt-1.10.22", | ||||
| 		build:"bs4/dt-1.10.23", | ||||
| 	 | ||||
| 	 | ||||
| 		/** | ||||
|   | ||||
| @@ -4,6 +4,7 @@ | ||||
|     <meta http-equiv="content-type" content="text/html; charset=UTF-8" /> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" /> | ||||
|     <meta name="robots" content="noindex,nofollow" /> | ||||
|     <link rel="icon" type="image/png" href="{{urlpath}}/bwrs_static/shield-white.png"> | ||||
|     <title>Bitwarden_rs Admin Panel</title> | ||||
|     <link rel="stylesheet" href="{{urlpath}}/bwrs_static/bootstrap.css" /> | ||||
|     <style> | ||||
| @@ -73,7 +74,7 @@ | ||||
|  | ||||
| <body class="bg-light"> | ||||
|     <nav class="navbar navbar-expand-md navbar-dark bg-dark mb-4 shadow fixed-top"> | ||||
|         <div class="container"> | ||||
|         <div class="container-xl"> | ||||
|             <a class="navbar-brand" href="{{urlpath}}/admin"><img class="pr-1" src="{{urlpath}}/bwrs_static/shield-white.png">Bitwarden_rs Admin</a> | ||||
|             <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarCollapse" | ||||
|                     aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation"> | ||||
| @@ -96,7 +97,7 @@ | ||||
|                     </li> | ||||
|                 {{/if}} | ||||
|                     <li class="nav-item"> | ||||
|                         <a class="nav-link" href="{{urlpath}}/">Vault</a> | ||||
|                         <a class="nav-link" href="{{urlpath}}/" target="_blank" rel="noreferrer">Vault</a> | ||||
|                     </li> | ||||
|                 </ul> | ||||
|  | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| <main class="container"> | ||||
| <main class="container-xl"> | ||||
|     <div id="diagnostics-block" class="my-3 p-3 bg-white rounded shadow"> | ||||
|         <h6 class="border-bottom pb-2 mb-2">Diagnostics</h6> | ||||
|  | ||||
| @@ -15,7 +15,7 @@ | ||||
|                         <span id="server-installed">{{version}}</span> | ||||
|                     </dd> | ||||
|                     <dt class="col-sm-5">Server Latest | ||||
|                         <span class="badge badge-danger d-none" id="server-failed" title="Unable to determine latest version.">Unknown</span> | ||||
|                         <span class="badge badge-secondary d-none" id="server-failed" title="Unable to determine latest version.">Unknown</span> | ||||
|                     </dt> | ||||
|                     <dd class="col-sm-7"> | ||||
|                         <span id="server-latest">{{diagnostics.latest_release}}<span id="server-latest-commit" class="d-none">-{{diagnostics.latest_commit}}</span></span> | ||||
| @@ -28,7 +28,7 @@ | ||||
|                         <span id="web-installed">{{diagnostics.web_vault_version}}</span> | ||||
|                     </dd> | ||||
|                     <dt class="col-sm-5">Web Latest | ||||
|                         <span class="badge badge-danger d-none" id="web-failed" title="Unable to determine latest version.">Unknown</span> | ||||
|                         <span class="badge badge-secondary d-none" id="web-failed" title="Unable to determine latest version.">Unknown</span> | ||||
|                     </dt> | ||||
|                     <dd class="col-sm-7"> | ||||
|                         <span id="web-latest">{{diagnostics.latest_web_build}}</span> | ||||
| @@ -41,6 +41,40 @@ | ||||
|         <div class="row"> | ||||
|             <div class="col-md"> | ||||
|                 <dl class="row"> | ||||
|                     <dt class="col-sm-5">Running within Docker</dt> | ||||
|                     <dd class="col-sm-7"> | ||||
|                     {{#if diagnostics.running_within_docker}} | ||||
|                         <span id="running-docker" class="d-block"><b>Yes</b></span> | ||||
|                     {{/if}} | ||||
|                     {{#unless diagnostics.running_within_docker}} | ||||
|                         <span id="running-docker" class="d-block"><b>No</b></span> | ||||
|                     {{/unless}} | ||||
|                     </dd> | ||||
|                     <dt class="col-sm-5">Uses a proxy</dt> | ||||
|                     <dd class="col-sm-7"> | ||||
|                     {{#if diagnostics.uses_proxy}} | ||||
|                         <span id="running-docker" class="d-block"><b>Yes</b></span> | ||||
|                     {{/if}} | ||||
|                     {{#unless diagnostics.uses_proxy}} | ||||
|                         <span id="running-docker" class="d-block"><b>No</b></span> | ||||
|                     {{/unless}} | ||||
|                     </dd> | ||||
|                     <dt class="col-sm-5">Internet access | ||||
|                     {{#if diagnostics.has_http_access}} | ||||
|                         <span class="badge badge-success" id="internet-success" title="We have internet access!">Ok</span> | ||||
|                     {{/if}} | ||||
|                     {{#unless diagnostics.has_http_access}} | ||||
|                         <span class="badge badge-danger" id="internet-warning" title="There seems to be no internet access. Please fix.">Error</span> | ||||
|                     {{/unless}} | ||||
|                     </dt> | ||||
|                     <dd class="col-sm-7"> | ||||
|                     {{#if diagnostics.has_http_access}} | ||||
|                         <span id="running-docker" class="d-block"><b>Yes</b></span> | ||||
|                     {{/if}} | ||||
|                     {{#unless diagnostics.has_http_access}} | ||||
|                         <span id="running-docker" class="d-block"><b>No</b></span> | ||||
|                     {{/unless}} | ||||
|                     </dd> | ||||
|                     <dt class="col-sm-5">DNS (github.com) | ||||
|                         <span class="badge badge-success d-none" id="dns-success" title="DNS Resolving works!">Ok</span> | ||||
|                         <span class="badge badge-danger d-none" id="dns-warning" title="DNS Resolving failed. Please fix.">Error</span> | ||||
| @@ -57,6 +91,44 @@ | ||||
|                         <span id="time-server" class="d-block"><b>Server:</b> <span id="time-server-string">{{diagnostics.server_time}}</span></span> | ||||
|                         <span id="time-browser" class="d-block"><b>Browser:</b> <span id="time-browser-string"></span></span> | ||||
|                     </dd> | ||||
|  | ||||
|                     <dt class="col-sm-5">Domain configuration | ||||
|                         <span class="badge badge-success d-none" id="domain-success" title="Domain variable seems to be correct.">Ok</span> | ||||
|                         <span class="badge badge-danger d-none" id="domain-warning" title="Domain variable is not configured correctly.
Some features may not work as expected!">Error</span> | ||||
|                     </dt> | ||||
|                     <dd class="col-sm-7"> | ||||
|                         <span id="domain-server" class="d-block"><b>Server:</b> <span id="domain-server-string">{{diagnostics.admin_url}}</span></span> | ||||
|                         <span id="domain-browser" class="d-block"><b>Browser:</b> <span id="domain-browser-string"></span></span> | ||||
|                     </dd> | ||||
|                 </dl> | ||||
|             </div> | ||||
|         </div> | ||||
|  | ||||
|         <h3>Support</h3> | ||||
|         <div class="row"> | ||||
|             <div class="col-md"> | ||||
|                 <dl class="row"> | ||||
|                     <dd class="col-sm-12"> | ||||
|                         If you need support please check the following links first before you create a new issue: | ||||
|                          <a href="https://bitwardenrs.discourse.group/" target="_blank" rel="noreferrer">Bitwarden_RS Forum</a> | ||||
|                          | <a href="https://github.com/dani-garcia/bitwarden_rs/discussions" target="_blank" rel="noreferrer">Github Discussions</a> | ||||
|                     </dd> | ||||
|                 </dl> | ||||
|                 <dl class="row"> | ||||
|                     <dd class="col-sm-12"> | ||||
|                         You can use the button below to pre-generate a string which you can copy/paste on either the Forum or when Creating a new issue at Github.<br> | ||||
|                         We try to hide the most sensitive values from the generated support string by default, but please verify if there is nothing in there which you want to hide!<br> | ||||
|                     </dd> | ||||
|                 </dl> | ||||
|                 <dl class="row"> | ||||
|                     <dt class="col-sm-3"> | ||||
|                         <button type="button" id="gen-support" class="btn btn-primary" onclick="generateSupportString(); return false;">Generate Support String</button> | ||||
|                         <br><br> | ||||
|                         <button type="button" id="copy-support" class="btn btn-info d-none" onclick="copyToClipboard(); return false;">Copy To Clipboard</button> | ||||
|                     </dt> | ||||
|                     <dd class="col-sm-9"> | ||||
|                         <pre id="support-string" class="pre-scrollable d-none" style="width: 100%; height: 16em; size: 0.6em; border: 1px solid; padding: 4px;"></pre> | ||||
|                     </dd> | ||||
|                 </dl> | ||||
|             </div> | ||||
|         </div> | ||||
| @@ -64,7 +136,12 @@ | ||||
| </main> | ||||
|  | ||||
| <script> | ||||
|     dnsCheck = false; | ||||
|     timeCheck = false; | ||||
|     domainCheck = false; | ||||
|     (() => { | ||||
|         // ================================ | ||||
|         // Date & Time Check | ||||
|         const d = new Date(); | ||||
|         const year = d.getUTCFullYear(); | ||||
|         const month = String(d.getUTCMonth()+1).padStart(2, '0'); | ||||
| @@ -81,16 +158,21 @@ | ||||
|             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 bitwarden_rs 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('-', ''); | ||||
| @@ -146,5 +228,56 @@ | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // ================================ | ||||
|         // 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'); | ||||
|         } | ||||
|     })(); | ||||
|  | ||||
|     // ================================ | ||||
|     // Generate support string to be pasted on github or the forum | ||||
|     async function generateSupportString() { | ||||
|         supportString = "### Your environment (Generated via diagnostics page)\n"; | ||||
|  | ||||
|         supportString += "* Bitwarden_rs version: v{{ version }}\n"; | ||||
|         supportString += "* Web-vault version: v{{ diagnostics.web_vault_version }}\n"; | ||||
|         supportString += "* Running within Docker: {{ diagnostics.running_within_docker }}\n"; | ||||
|         supportString += "* Internet access: {{ diagnostics.has_http_access }}\n"; | ||||
|         supportString += "* Uses a proxy: {{ diagnostics.uses_proxy }}\n"; | ||||
|         supportString += "* DNS Check: " + dnsCheck + "\n"; | ||||
|         supportString += "* Time Check: " + timeCheck + "\n"; | ||||
|         supportString += "* Domain Configuration Check: " + domainCheck + "\n"; | ||||
|         supportString += "* Database type: {{ diagnostics.db_type }}\n"; | ||||
|         {{#case diagnostics.db_type "MySQL" "PostgreSQL"}} | ||||
|         supportString += "* Database version: [PLEASE PROVIDE DATABASE VERSION]\n"; | ||||
|         {{/case}} | ||||
|  | ||||
|         jsonResponse = await fetch('{{urlpath}}/admin/diagnostics/config'); | ||||
|         configJson = await jsonResponse.json(); | ||||
|         supportString += "\n### Config (Generated via diagnostics page)\n```json\n" + JSON.stringify(configJson, undefined, 2) + "\n```\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 str = document.getElementById('support-string').innerText; | ||||
|         const el = document.createElement('textarea'); | ||||
|         el.value = str; | ||||
|         el.setAttribute('readonly', ''); | ||||
|         el.style.position = 'absolute'; | ||||
|         el.style.left = '-9999px'; | ||||
|         document.body.appendChild(el); | ||||
|         el.select(); | ||||
|         document.execCommand('copy'); | ||||
|         document.body.removeChild(el); | ||||
|     } | ||||
|  | ||||
| </script> | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| <main class="container"> | ||||
| <main class="container-xl"> | ||||
|     {{#if error}} | ||||
|     <div class="align-items-center p-3 mb-3 text-white-50 bg-warning rounded shadow"> | ||||
|         <div> | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| <main class="container"> | ||||
| <main class="container-xl"> | ||||
|     <div id="organizations-block" class="my-3 p-3 bg-white rounded shadow"> | ||||
|         <h6 class="border-bottom pb-2 mb-3">Organizations</h6> | ||||
|  | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| <main class="container"> | ||||
| <main class="container-xl"> | ||||
|     <div id="config-block" class="align-items-center p-3 mb-3 bg-secondary rounded shadow"> | ||||
|         <div> | ||||
|             <h6 class="text-white mb-3">Configuration</h6> | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| <main class="container"> | ||||
| <main class="container-xl"> | ||||
|     <div id="users-block" class="my-3 p-3 bg-white rounded shadow"> | ||||
|         <h6 class="border-bottom pb-2 mb-3">Registered Users</h6> | ||||
|  | ||||
| @@ -7,10 +7,12 @@ | ||||
|                 <thead> | ||||
|                     <tr> | ||||
|                         <th>User</th> | ||||
|                         <th style="width:60px; min-width: 60px;">Items</th> | ||||
|                         <th style="width:65px; min-width: 65px;">Created at</th> | ||||
|                         <th style="width:70px; min-width: 65px;">Last Active</th> | ||||
|                         <th style="width:35px; min-width: 35px;">Items</th> | ||||
|                         <th>Attachments</th> | ||||
|                         <th style="min-width: 120px;">Organizations</th> | ||||
|                         <th style="width: 140px; min-width: 140px;">Actions</th> | ||||
|                         <th style="width: 120px; min-width: 120px;">Actions</th> | ||||
|                     </tr> | ||||
|                 </thead> | ||||
|                 <tbody> | ||||
| @@ -21,8 +23,6 @@ | ||||
|                             <div class="float-left"> | ||||
|                                 <strong>{{Name}}</strong> | ||||
|                                 <span class="d-block">{{Email}}</span> | ||||
|                                 <span class="d-block">Created at: {{created_at}}</span> | ||||
|                                 <span class="d-block">Last active: {{last_active}}</span> | ||||
|                                 <span class="d-block"> | ||||
|                                     {{#unless user_enabled}} | ||||
|                                         <span class="badge badge-danger mr-2" title="User is disabled">Disabled</span> | ||||
| @@ -39,6 +39,12 @@ | ||||
|                                 </span> | ||||
|                             </div> | ||||
|                         </td> | ||||
|                         <td> | ||||
|                             <span class="d-block">{{created_at}}</span> | ||||
|                         </td> | ||||
|                         <td> | ||||
|                             <span class="d-block">{{last_active}}</span> | ||||
|                         </td> | ||||
|                         <td> | ||||
|                             <span class="d-block">{{cipher_count}}</span> | ||||
|                         </td> | ||||
| @@ -49,9 +55,11 @@ | ||||
|                             {{/if}} | ||||
|                         </td> | ||||
|                         <td> | ||||
|                             <div class="overflow-auto" style="max-height: 120px;"> | ||||
|                             {{#each Organizations}} | ||||
|                             <span class="badge badge-primary" data-orgtype="{{Type}}">{{Name}}</span> | ||||
|                             {{/each}} | ||||
|                             </div> | ||||
|                         </td> | ||||
|                         <td style="font-size: 90%; text-align: right; padding-right: 15px"> | ||||
|                             {{#if TwoFactorEnabled}} | ||||
| @@ -173,18 +181,43 @@ | ||||
|         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(event) { | ||||
|         $('#users-table').DataTable({ | ||||
|             "responsive": true, | ||||
|             "lengthMenu": [ [-1, 5, 10, 25, 50], ["All", 5, 10, 25, 50] ], | ||||
|             "pageLength": -1, // Default show all | ||||
|             "columns": [ | ||||
|                 null,                                        // Userdata | ||||
|                 null,                                        // Items | ||||
|                 null,                                        // Attachments | ||||
|                 null,                                        // Organizations | ||||
|                 { "searchable": false, "orderable": false }, // Actions | ||||
|             ], | ||||
|             "columnDefs": [ | ||||
|                 { "targets": [1,2], "type": "date-iso" }, | ||||
|                 { "targets": 6, "searchable": false, "orderable": false } | ||||
|             ] | ||||
|         }); | ||||
|     }); | ||||
| </script> | ||||