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 once_cell::sync::Lazy;
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::process::Command;
|
use std::{env, process::Command, time::Duration};
|
||||||
|
|
||||||
|
use reqwest::{blocking::Client, header::USER_AGENT};
|
||||||
use rocket::{
|
use rocket::{
|
||||||
http::{Cookie, Cookies, SameSite},
|
http::{Cookie, Cookies, SameSite},
|
||||||
request::{self, FlashMessage, Form, FromRequest, Outcome, Request},
|
request::{self, FlashMessage, Form, FromRequest, Outcome, Request},
|
||||||
@ -18,7 +19,7 @@ use crate::{
|
|||||||
db::{backup_database, models::*, DbConn, DbConnType},
|
db::{backup_database, models::*, DbConn, DbConnType},
|
||||||
error::{Error, MapResult},
|
error::{Error, MapResult},
|
||||||
mail,
|
mail,
|
||||||
util::{get_display_size, format_naive_datetime_local},
|
util::{format_naive_datetime_local, get_display_size},
|
||||||
CONFIG,
|
CONFIG,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -47,9 +48,20 @@ pub fn routes() -> Vec<Route> {
|
|||||||
users_overview,
|
users_overview,
|
||||||
organizations_overview,
|
organizations_overview,
|
||||||
diagnostics,
|
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(|| {
|
static CAN_BACKUP: Lazy<bool> = Lazy::new(|| {
|
||||||
DbConnType::from_url(&CONFIG.database_url())
|
DbConnType::from_url(&CONFIG.database_url())
|
||||||
.map(|t| t == DbConnType::sqlite)
|
.map(|t| t == DbConnType::sqlite)
|
||||||
@ -307,7 +319,8 @@ fn users_overview(_token: AdminToken, conn: DbConn) -> ApiResult<Html<String>> {
|
|||||||
None => json!("Never")
|
None => json!("Never")
|
||||||
};
|
};
|
||||||
usr
|
usr
|
||||||
}).collect();
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
let text = AdminTemplateData::users(users_json).render()?;
|
let text = AdminTemplateData::users(users_json).render()?;
|
||||||
Ok(Html(text))
|
Ok(Html(text))
|
||||||
@ -362,14 +375,16 @@ fn update_revision_users(_token: AdminToken, conn: DbConn) -> EmptyResult {
|
|||||||
#[get("/organizations/overview")]
|
#[get("/organizations/overview")]
|
||||||
fn organizations_overview(_token: AdminToken, conn: DbConn) -> ApiResult<Html<String>> {
|
fn organizations_overview(_token: AdminToken, conn: DbConn) -> ApiResult<Html<String>> {
|
||||||
let organizations = Organization::get_all(&conn);
|
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();
|
let mut org = o.to_json();
|
||||||
org["user_count"] = json!(UserOrganization::count_by_org(&o.uuid, &conn));
|
org["user_count"] = json!(UserOrganization::count_by_org(&o.uuid, &conn));
|
||||||
org["cipher_count"] = json!(Cipher::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_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["attachment_size"] = json!(get_display_size(Attachment::size_by_org(&o.uuid, &conn) as i32));
|
||||||
org
|
org
|
||||||
}).collect();
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
let text = AdminTemplateData::organizations(organizations_json).render()?;
|
let text = AdminTemplateData::organizations(organizations_json).render()?;
|
||||||
Ok(Html(text))
|
Ok(Html(text))
|
||||||
@ -391,77 +406,104 @@ struct GitCommit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn get_github_api<T: DeserializeOwned>(url: &str) -> Result<T, Error> {
|
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()?;
|
let github_api = Client::builder().build()?;
|
||||||
|
|
||||||
Ok(
|
Ok(github_api
|
||||||
github_api.get(url)
|
.get(url)
|
||||||
.timeout(Duration::from_secs(10))
|
.timeout(Duration::from_secs(10))
|
||||||
.header(USER_AGENT, "Bitwarden_RS")
|
.header(USER_AGENT, "Bitwarden_RS")
|
||||||
.send()?
|
.send()?
|
||||||
.error_for_status()?
|
.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")]
|
#[get("/diagnostics")]
|
||||||
fn diagnostics(_token: AdminToken, _conn: DbConn) -> ApiResult<Html<String>> {
|
fn diagnostics(_token: AdminToken, _conn: DbConn) -> ApiResult<Html<String>> {
|
||||||
use std::net::ToSocketAddrs;
|
|
||||||
use chrono::prelude::*;
|
|
||||||
use crate::util::read_file_string;
|
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_path = format!("{}/{}", CONFIG.web_vault_folder(), "version.json");
|
||||||
let vault_version_str = read_file_string(&vault_version_path)?;
|
let vault_version_str = read_file_string(&vault_version_path)?;
|
||||||
let web_vault_version: WebVaultVersion = serde_json::from_str(&vault_version_str)?;
|
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());
|
// Execute some environment checks
|
||||||
let (dns_resolved, dns_ok) = match github_ips {
|
let running_within_docker = std::path::Path::new("/.dockerenv").exists();
|
||||||
Ok(Some(a)) => (a.ip().to_string(), true),
|
let has_http_access = has_http_access();
|
||||||
_ => ("Could not resolve domain name.".to_string(), false),
|
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
|
// 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.
|
||||||
let (latest_release, latest_commit, latest_web_build) = if dns_ok {
|
// 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") {
|
match get_github_api::<GitRelease>("https://api.github.com/repos/dani-garcia/bitwarden_rs/releases/latest") {
|
||||||
Ok(r) => r.tag_name,
|
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") {
|
match get_github_api::<GitCommit>("https://api.github.com/repos/dani-garcia/bitwarden_rs/commits/master") {
|
||||||
Ok(mut c) => {
|
Ok(mut c) => {
|
||||||
c.sha.truncate(8);
|
c.sha.truncate(8);
|
||||||
c.sha
|
c.sha
|
||||||
},
|
}
|
||||||
_ => "-".to_string()
|
_ => "-".to_string(),
|
||||||
},
|
},
|
||||||
match get_github_api::<GitRelease>("https://api.github.com/repos/dani-garcia/bw_web_builds/releases/latest") {
|
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(),
|
Ok(r) => r.tag_name.trim_start_matches('v').to_string(),
|
||||||
_ => "-".to_string()
|
_ => "-".to_string(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
("-".to_string(), "-".to_string(), "-".to_string())
|
("-".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!({
|
let diagnostics_json = json!({
|
||||||
"dns_resolved": dns_resolved,
|
"dns_resolved": dns_resolved,
|
||||||
"server_time": server_time,
|
|
||||||
"web_vault_version": web_vault_version.version,
|
"web_vault_version": web_vault_version.version,
|
||||||
"latest_release": latest_release,
|
"latest_release": latest_release,
|
||||||
"latest_commit": latest_commit,
|
"latest_commit": latest_commit,
|
||||||
"latest_web_build": latest_web_build,
|
"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()?;
|
let text = AdminTemplateData::diagnostics(diagnostics_json).render()?;
|
||||||
Ok(Html(text))
|
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>")]
|
#[post("/config", data = "<data>")]
|
||||||
fn post_config(data: Json<ConfigBuilder>, _token: AdminToken) -> EmptyResult {
|
fn post_config(data: Json<ConfigBuilder>, _token: AdminToken) -> EmptyResult {
|
||||||
let data: ConfigBuilder = data.into_inner();
|
let data: ConfigBuilder = data.into_inner();
|
||||||
|
@ -172,7 +172,7 @@ fn hibp_breach(username: String) -> JsonResult {
|
|||||||
"Domain": "haveibeenpwned.com",
|
"Domain": "haveibeenpwned.com",
|
||||||
"BreachDate": "2019-08-18T00:00:00Z",
|
"BreachDate": "2019-08-18T00:00:00Z",
|
||||||
"AddedDate": "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",
|
"LogoPath": "bwrs_static/hibp.png",
|
||||||
"PwnCount": 0,
|
"PwnCount": 0,
|
||||||
"DataClasses": [
|
"DataClasses": [
|
||||||
|
@ -2,6 +2,7 @@ use std::process::exit;
|
|||||||
use std::sync::RwLock;
|
use std::sync::RwLock;
|
||||||
|
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
|
use regex::Regex;
|
||||||
use reqwest::Url;
|
use reqwest::Url;
|
||||||
|
|
||||||
use crate::{
|
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;
|
pub type Pass = String;
|
||||||
|
|
||||||
macro_rules! make_config {
|
macro_rules! make_config {
|
||||||
@ -52,6 +68,7 @@ macro_rules! make_config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ConfigBuilder {
|
impl ConfigBuilder {
|
||||||
|
#[allow(clippy::field_reassign_with_default)]
|
||||||
fn from_env() -> Self {
|
fn from_env() -> Self {
|
||||||
match dotenv::from_path(".env") {
|
match dotenv::from_path(".env") {
|
||||||
Ok(_) => (),
|
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
|
// Group or empty string
|
||||||
( @show ) => { "" };
|
( @show ) => { "" };
|
||||||
( @show $lit:literal ) => { $lit };
|
( @show $lit:literal ) => { $lit };
|
||||||
@ -458,7 +504,6 @@ make_config! {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
|
fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
|
||||||
|
|
||||||
// Validate connection URL is valid and DB feature is enabled
|
// Validate connection URL is valid and DB feature is enabled
|
||||||
DbConnType::from_url(&cfg.database_url)?;
|
DbConnType::from_url(&cfg.database_url)?;
|
||||||
|
|
||||||
@ -472,7 +517,9 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
|
|||||||
|
|
||||||
let dom = cfg.domain.to_lowercase();
|
let dom = cfg.domain.to_lowercase();
|
||||||
if !dom.starts_with("http://") && !dom.starts_with("https://") {
|
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;
|
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();
|
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.is_empty() || org_creation_users == "all" || org_creation_users == "none")
|
||||||
if org_creation_users.split(',').any(|u| !u.contains('@')) {
|
&& org_creation_users.split(',').any(|u| !u.contains('@'))
|
||||||
err!("`ORG_CREATION_USERS` contains invalid email addresses");
|
{
|
||||||
}
|
err!("`ORG_CREATION_USERS` contains invalid email addresses");
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(ref token) = cfg.admin_token {
|
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
|
// Check if the icon blacklist regex is valid
|
||||||
if let Some(ref r) = cfg.icon_blacklist_regex {
|
if let Some(ref r) = cfg.icon_blacklist_regex {
|
||||||
use regex::Regex;
|
|
||||||
let validate_regex = Regex::new(&r);
|
let validate_regex = Regex::new(&r);
|
||||||
match validate_regex {
|
match validate_regex {
|
||||||
Ok(_) => (),
|
Ok(_) => (),
|
||||||
@ -577,7 +623,12 @@ impl Config {
|
|||||||
validate_config(&config)?;
|
validate_config(&config)?;
|
||||||
|
|
||||||
Ok(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.
|
/// Tests whether the specified user is allowed to create an organization.
|
||||||
pub fn is_org_creation_allowed(&self, email: &str) -> bool {
|
pub fn is_org_creation_allowed(&self, email: &str) -> bool {
|
||||||
let users = self.org_creation_users();
|
let users = self.org_creation_users();
|
||||||
if users == "" || users == "all" {
|
if users.is_empty() || users == "all" {
|
||||||
true
|
true
|
||||||
} else if users == "none" {
|
} else if users == "none" {
|
||||||
false
|
false
|
||||||
@ -704,8 +755,10 @@ impl Config {
|
|||||||
let akey_s = data_encoding::BASE64.encode(&akey);
|
let akey_s = data_encoding::BASE64.encode(&akey);
|
||||||
|
|
||||||
// Save the new value
|
// Save the new value
|
||||||
let mut builder = ConfigBuilder::default();
|
let builder = ConfigBuilder {
|
||||||
builder._duo_akey = Some(akey_s.clone());
|
_duo_akey: Some(akey_s.clone()),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
self.update_config_partial(builder).ok();
|
self.update_config_partial(builder).ok();
|
||||||
|
|
||||||
akey_s
|
akey_s
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
#![cfg_attr(feature = "unstable", feature(ip))]
|
#![cfg_attr(feature = "unstable", feature(ip))]
|
||||||
#![recursion_limit = "256"]
|
#![recursion_limit = "512"]
|
||||||
|
|
||||||
extern crate openssl;
|
extern crate openssl;
|
||||||
#[macro_use]
|
#[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/)
|
* Native JavaScript for Bootstrap v3.0.15 (https://thednp.github.io/bootstrap.native/)
|
||||||
* Copyright 2015-2020 © dnp_theme
|
* Copyright 2015-2021 © dnp_theme
|
||||||
* Licensed under MIT (https://github.com/thednp/bootstrap.native/blob/master/LICENSE)
|
* Licensed under MIT (https://github.com/thednp/bootstrap.native/blob/master/LICENSE)
|
||||||
*/
|
*/
|
||||||
(function (global, factory) {
|
(function (global, factory) {
|
||||||
@ -15,10 +15,14 @@
|
|||||||
|
|
||||||
var transitionDuration = 'webkitTransition' in document.head.style ? 'webkitTransitionDuration' : 'transitionDuration';
|
var transitionDuration = 'webkitTransition' in document.head.style ? 'webkitTransitionDuration' : 'transitionDuration';
|
||||||
|
|
||||||
|
var transitionProperty = 'webkitTransition' in document.head.style ? 'webkitTransitionProperty' : 'transitionProperty';
|
||||||
|
|
||||||
function getElementTransitionDuration(element) {
|
function getElementTransitionDuration(element) {
|
||||||
var duration = supportTransition ? parseFloat(getComputedStyle(element)[transitionDuration]) : 0;
|
var computedStyle = getComputedStyle(element),
|
||||||
duration = typeof duration === 'number' && !isNaN(duration) ? duration * 1000 : 0;
|
property = computedStyle[transitionProperty],
|
||||||
return duration;
|
duration = supportTransition && property && property !== 'none'
|
||||||
|
? parseFloat(computedStyle[transitionDuration]) : 0;
|
||||||
|
return !isNaN(duration) ? duration * 1000 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function emulateTransitionEnd(element,handler){
|
function emulateTransitionEnd(element,handler){
|
||||||
@ -35,9 +39,15 @@
|
|||||||
return selector instanceof Element ? selector : lookUp.querySelector(selector);
|
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});
|
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;
|
return OriginalCustomEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -352,7 +362,7 @@
|
|||||||
};
|
};
|
||||||
self.slideTo = function (next) {
|
self.slideTo = function (next) {
|
||||||
if (vars.isSliding) { return; }
|
if (vars.isSliding) { return; }
|
||||||
var activeItem = self.getActiveIndex(), orientation;
|
var activeItem = self.getActiveIndex(), orientation, eventProperties;
|
||||||
if ( activeItem === next ) {
|
if ( activeItem === next ) {
|
||||||
return;
|
return;
|
||||||
} else if ( (activeItem < next ) || (activeItem === 0 && next === slides.length -1 ) ) {
|
} else if ( (activeItem < next ) || (activeItem === 0 && next === slides.length -1 ) ) {
|
||||||
@ -363,8 +373,9 @@
|
|||||||
if ( next < 0 ) { next = slides.length - 1; }
|
if ( next < 0 ) { next = slides.length - 1; }
|
||||||
else if ( next >= slides.length ){ next = 0; }
|
else if ( next >= slides.length ){ next = 0; }
|
||||||
orientation = vars.direction === 'left' ? 'next' : 'prev';
|
orientation = vars.direction === 'left' ? 'next' : 'prev';
|
||||||
slideCustomEvent = bootstrapCustomEvent('slide', 'carousel', slides[next]);
|
eventProperties = { relatedTarget: slides[next], direction: vars.direction, from: activeItem, to: next };
|
||||||
slidCustomEvent = bootstrapCustomEvent('slid', 'carousel', slides[next]);
|
slideCustomEvent = bootstrapCustomEvent('slide', 'carousel', eventProperties);
|
||||||
|
slidCustomEvent = bootstrapCustomEvent('slid', 'carousel', eventProperties);
|
||||||
dispatchCustomEvent.call(element, slideCustomEvent);
|
dispatchCustomEvent.call(element, slideCustomEvent);
|
||||||
if (slideCustomEvent.defaultPrevented) { return; }
|
if (slideCustomEvent.defaultPrevented) { return; }
|
||||||
vars.index = next;
|
vars.index = next;
|
||||||
@ -615,7 +626,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.show = function () {
|
self.show = function () {
|
||||||
showCustomEvent = bootstrapCustomEvent('show', 'dropdown', relatedTarget);
|
showCustomEvent = bootstrapCustomEvent('show', 'dropdown', { relatedTarget: relatedTarget });
|
||||||
dispatchCustomEvent.call(parent, showCustomEvent);
|
dispatchCustomEvent.call(parent, showCustomEvent);
|
||||||
if ( showCustomEvent.defaultPrevented ) { return; }
|
if ( showCustomEvent.defaultPrevented ) { return; }
|
||||||
menu.classList.add('show');
|
menu.classList.add('show');
|
||||||
@ -626,12 +637,12 @@
|
|||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
setFocus( menu.getElementsByTagName('INPUT')[0] || element );
|
setFocus( menu.getElementsByTagName('INPUT')[0] || element );
|
||||||
toggleDismiss();
|
toggleDismiss();
|
||||||
shownCustomEvent = bootstrapCustomEvent( 'shown', 'dropdown', relatedTarget);
|
shownCustomEvent = bootstrapCustomEvent('shown', 'dropdown', { relatedTarget: relatedTarget });
|
||||||
dispatchCustomEvent.call(parent, shownCustomEvent);
|
dispatchCustomEvent.call(parent, shownCustomEvent);
|
||||||
},1);
|
},1);
|
||||||
};
|
};
|
||||||
self.hide = function () {
|
self.hide = function () {
|
||||||
hideCustomEvent = bootstrapCustomEvent('hide', 'dropdown', relatedTarget);
|
hideCustomEvent = bootstrapCustomEvent('hide', 'dropdown', { relatedTarget: relatedTarget });
|
||||||
dispatchCustomEvent.call(parent, hideCustomEvent);
|
dispatchCustomEvent.call(parent, hideCustomEvent);
|
||||||
if ( hideCustomEvent.defaultPrevented ) { return; }
|
if ( hideCustomEvent.defaultPrevented ) { return; }
|
||||||
menu.classList.remove('show');
|
menu.classList.remove('show');
|
||||||
@ -643,7 +654,7 @@
|
|||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
element.Dropdown && element.addEventListener('click',clickHandler,false);
|
element.Dropdown && element.addEventListener('click',clickHandler,false);
|
||||||
},1);
|
},1);
|
||||||
hiddenCustomEvent = bootstrapCustomEvent('hidden', 'dropdown', relatedTarget);
|
hiddenCustomEvent = bootstrapCustomEvent('hidden', 'dropdown', { relatedTarget: relatedTarget });
|
||||||
dispatchCustomEvent.call(parent, hiddenCustomEvent);
|
dispatchCustomEvent.call(parent, hiddenCustomEvent);
|
||||||
};
|
};
|
||||||
self.toggle = function () {
|
self.toggle = function () {
|
||||||
@ -749,7 +760,7 @@
|
|||||||
setFocus(modal);
|
setFocus(modal);
|
||||||
modal.isAnimating = false;
|
modal.isAnimating = false;
|
||||||
toggleEvents(1);
|
toggleEvents(1);
|
||||||
shownCustomEvent = bootstrapCustomEvent('shown', 'modal', relatedTarget);
|
shownCustomEvent = bootstrapCustomEvent('shown', 'modal', { relatedTarget: relatedTarget });
|
||||||
dispatchCustomEvent.call(modal, shownCustomEvent);
|
dispatchCustomEvent.call(modal, shownCustomEvent);
|
||||||
}
|
}
|
||||||
function triggerHide(force) {
|
function triggerHide(force) {
|
||||||
@ -804,7 +815,7 @@
|
|||||||
};
|
};
|
||||||
self.show = function () {
|
self.show = function () {
|
||||||
if (modal.classList.contains('show') && !!modal.isAnimating ) {return}
|
if (modal.classList.contains('show') && !!modal.isAnimating ) {return}
|
||||||
showCustomEvent = bootstrapCustomEvent('show', 'modal', relatedTarget);
|
showCustomEvent = bootstrapCustomEvent('show', 'modal', { relatedTarget: relatedTarget });
|
||||||
dispatchCustomEvent.call(modal, showCustomEvent);
|
dispatchCustomEvent.call(modal, showCustomEvent);
|
||||||
if ( showCustomEvent.defaultPrevented ) { return; }
|
if ( showCustomEvent.defaultPrevented ) { return; }
|
||||||
modal.isAnimating = true;
|
modal.isAnimating = true;
|
||||||
@ -1193,7 +1204,7 @@
|
|||||||
if (dropLink && !dropLink.classList.contains('active') ) {
|
if (dropLink && !dropLink.classList.contains('active') ) {
|
||||||
dropLink.classList.add('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 ) {
|
} else if ( isActive && !inside ) {
|
||||||
item.classList.remove('active');
|
item.classList.remove('active');
|
||||||
if (dropLink && dropLink.classList.contains('active') && !item.parentNode.getElementsByClassName('active').length ) {
|
if (dropLink && dropLink.classList.contains('active') && !item.parentNode.getElementsByClassName('active').length ) {
|
||||||
@ -1278,7 +1289,7 @@
|
|||||||
} else {
|
} else {
|
||||||
tabs.isAnimating = false;
|
tabs.isAnimating = false;
|
||||||
}
|
}
|
||||||
shownCustomEvent = bootstrapCustomEvent('shown', 'tab', activeTab);
|
shownCustomEvent = bootstrapCustomEvent('shown', 'tab', { relatedTarget: activeTab });
|
||||||
dispatchCustomEvent.call(next, shownCustomEvent);
|
dispatchCustomEvent.call(next, shownCustomEvent);
|
||||||
}
|
}
|
||||||
function triggerHide() {
|
function triggerHide() {
|
||||||
@ -1287,8 +1298,8 @@
|
|||||||
nextContent.style.float = 'left';
|
nextContent.style.float = 'left';
|
||||||
containerHeight = activeContent.scrollHeight;
|
containerHeight = activeContent.scrollHeight;
|
||||||
}
|
}
|
||||||
showCustomEvent = bootstrapCustomEvent('show', 'tab', activeTab);
|
showCustomEvent = bootstrapCustomEvent('show', 'tab', { relatedTarget: activeTab });
|
||||||
hiddenCustomEvent = bootstrapCustomEvent('hidden', 'tab', next);
|
hiddenCustomEvent = bootstrapCustomEvent('hidden', 'tab', { relatedTarget: next });
|
||||||
dispatchCustomEvent.call(next, showCustomEvent);
|
dispatchCustomEvent.call(next, showCustomEvent);
|
||||||
if ( showCustomEvent.defaultPrevented ) { return; }
|
if ( showCustomEvent.defaultPrevented ) { return; }
|
||||||
nextContent.classList.add('active');
|
nextContent.classList.add('active');
|
||||||
@ -1331,7 +1342,7 @@
|
|||||||
nextContent = queryElement(next.getAttribute('href'));
|
nextContent = queryElement(next.getAttribute('href'));
|
||||||
activeTab = getActiveTab();
|
activeTab = getActiveTab();
|
||||||
activeContent = getActiveContent();
|
activeContent = getActiveContent();
|
||||||
hideCustomEvent = bootstrapCustomEvent( 'hide', 'tab', next);
|
hideCustomEvent = bootstrapCustomEvent( 'hide', 'tab', { relatedTarget: next });
|
||||||
dispatchCustomEvent.call(activeTab, hideCustomEvent);
|
dispatchCustomEvent.call(activeTab, hideCustomEvent);
|
||||||
if (hideCustomEvent.defaultPrevented) { return; }
|
if (hideCustomEvent.defaultPrevented) { return; }
|
||||||
tabs.isAnimating = true;
|
tabs.isAnimating = true;
|
||||||
@ -1637,7 +1648,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var version = "3.0.10";
|
var version = "3.0.15";
|
||||||
|
|
||||||
var index = {
|
var index = {
|
||||||
Alert: Alert,
|
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 The Bootstrap Authors
|
||||||
* Copyright 2011-2020 Twitter, Inc.
|
* Copyright 2011-2020 Twitter, Inc.
|
||||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||||
*/
|
*/
|
||||||
:root {
|
:root {
|
||||||
--blue: #007bff;
|
--blue: #007bff;
|
||||||
--indigo: #6610f2;
|
--indigo: #6610f2;
|
||||||
--purple: #6f42c1;
|
--purple: #6f42c1;
|
||||||
@ -216,6 +216,7 @@ caption {
|
|||||||
|
|
||||||
th {
|
th {
|
||||||
text-align: inherit;
|
text-align: inherit;
|
||||||
|
text-align: -webkit-match-parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
label {
|
label {
|
||||||
@ -3750,6 +3751,8 @@ input[type="button"].btn-block {
|
|||||||
display: block;
|
display: block;
|
||||||
min-height: 1.5rem;
|
min-height: 1.5rem;
|
||||||
padding-left: 1.5rem;
|
padding-left: 1.5rem;
|
||||||
|
-webkit-print-color-adjust: exact;
|
||||||
|
color-adjust: exact;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-control-inline {
|
.custom-control-inline {
|
||||||
@ -5289,6 +5292,7 @@ a.badge-dark:focus, a.badge-dark.focus {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
z-index: 2;
|
||||||
padding: 0.75rem 1.25rem;
|
padding: 0.75rem 1.25rem;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
@ -10163,7 +10167,7 @@ a.text-dark:hover, a.text-dark:focus {
|
|||||||
|
|
||||||
.text-break {
|
.text-break {
|
||||||
word-break: break-word !important;
|
word-break: break-word !important;
|
||||||
overflow-wrap: break-word !important;
|
word-wrap: break-word !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-reset {
|
.text-reset {
|
||||||
@ -10256,3 +10260,4 @@ a.text-dark:hover, a.text-dark:focus {
|
|||||||
border-color: #dee2e6;
|
border-color: #dee2e6;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/*# sourceMappingURL=bootstrap.css.map */
|
@ -4,12 +4,13 @@
|
|||||||
*
|
*
|
||||||
* To rebuild or modify this file with the latest versions of the included
|
* To rebuild or modify this file with the latest versions of the included
|
||||||
* software please visit:
|
* software please visit:
|
||||||
* https://datatables.net/download/#bs4/dt-1.10.22
|
* https://datatables.net/download/#bs4/dt-1.10.23
|
||||||
*
|
*
|
||||||
* Included libraries:
|
* Included libraries:
|
||||||
* DataTables 1.10.22
|
* DataTables 1.10.23
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@charset "UTF-8";
|
||||||
table.dataTable {
|
table.dataTable {
|
||||||
clear: both;
|
clear: both;
|
||||||
margin-top: 6px !important;
|
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_asc_disabled:before,
|
||||||
table.dataTable > thead .sorting_desc_disabled:before {
|
table.dataTable > thead .sorting_desc_disabled:before {
|
||||||
right: 1em;
|
right: 1em;
|
||||||
content: "\2191";
|
content: "↑";
|
||||||
}
|
}
|
||||||
table.dataTable > thead .sorting:after,
|
table.dataTable > thead .sorting:after,
|
||||||
table.dataTable > thead .sorting_asc: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_asc_disabled:after,
|
||||||
table.dataTable > thead .sorting_desc_disabled:after {
|
table.dataTable > thead .sorting_desc_disabled:after {
|
||||||
right: 0.5em;
|
right: 0.5em;
|
||||||
content: "\2193";
|
content: "↓";
|
||||||
}
|
}
|
||||||
table.dataTable > thead .sorting_asc:before,
|
table.dataTable > thead .sorting_asc:before,
|
||||||
table.dataTable > thead .sorting_desc:after {
|
table.dataTable > thead .sorting_desc:after {
|
||||||
@ -165,9 +166,9 @@ div.dataTables_scrollFoot > .dataTables_scrollFootInner > table {
|
|||||||
|
|
||||||
@media screen and (max-width: 767px) {
|
@media screen and (max-width: 767px) {
|
||||||
div.dataTables_wrapper div.dataTables_length,
|
div.dataTables_wrapper div.dataTables_length,
|
||||||
div.dataTables_wrapper div.dataTables_filter,
|
div.dataTables_wrapper div.dataTables_filter,
|
||||||
div.dataTables_wrapper div.dataTables_info,
|
div.dataTables_wrapper div.dataTables_info,
|
||||||
div.dataTables_wrapper div.dataTables_paginate {
|
div.dataTables_wrapper div.dataTables_paginate {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
div.dataTables_wrapper div.dataTables_paginate ul.pagination {
|
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 {
|
div.table-responsive > div.dataTables_wrapper > div.row {
|
||||||
margin: 0;
|
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;
|
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;
|
padding-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,20 +4,20 @@
|
|||||||
*
|
*
|
||||||
* To rebuild or modify this file with the latest versions of the included
|
* To rebuild or modify this file with the latest versions of the included
|
||||||
* software please visit:
|
* software please visit:
|
||||||
* https://datatables.net/download/#bs4/dt-1.10.22
|
* https://datatables.net/download/#bs4/dt-1.10.23
|
||||||
*
|
*
|
||||||
* Included libraries:
|
* 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
|
* ©2008-2020 SpryMedia Ltd - datatables.net/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @summary DataTables
|
* @summary DataTables
|
||||||
* @description Paginate, search and order HTML tables
|
* @description Paginate, search and order HTML tables
|
||||||
* @version 1.10.22
|
* @version 1.10.23
|
||||||
* @file jquery.dataTables.js
|
* @file jquery.dataTables.js
|
||||||
* @author SpryMedia Ltd
|
* @author SpryMedia Ltd
|
||||||
* @contact www.datatables.net
|
* @contact www.datatables.net
|
||||||
@ -2775,7 +2775,7 @@
|
|||||||
for ( var i=0, iLen=a.length-1 ; i<iLen ; i++ )
|
for ( var i=0, iLen=a.length-1 ; i<iLen ; i++ )
|
||||||
{
|
{
|
||||||
// Protect against prototype pollution
|
// Protect against prototype pollution
|
||||||
if (a[i] === '__proto__') {
|
if (a[i] === '__proto__' || a[i] === 'constructor') {
|
||||||
throw new Error('Cannot set prototype values');
|
throw new Error('Cannot set prototype values');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3157,7 +3157,7 @@
|
|||||||
cells.push( nTd );
|
cells.push( nTd );
|
||||||
|
|
||||||
// Need to create the HTML if new, or if a rendering function is defined
|
// 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')
|
(!$.isPlainObject(oCol.mData) || oCol.mData._ !== i+'.display')
|
||||||
)) {
|
)) {
|
||||||
nTd.innerHTML = _fnGetCellData( oSettings, iRow, i, 'display' );
|
nTd.innerHTML = _fnGetCellData( oSettings, iRow, i, 'display' );
|
||||||
@ -3189,10 +3189,6 @@
|
|||||||
|
|
||||||
_fnCallbackFire( oSettings, 'aoRowCreatedCallback', null, [nTr, rowData, iRow, cells] );
|
_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
|
* @type string
|
||||||
* @default Version number
|
* @default Version number
|
||||||
*/
|
*/
|
||||||
DataTable.version = "1.10.22";
|
DataTable.version = "1.10.23";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Private data store, containing all of the settings objects that are
|
* Private data store, containing all of the settings objects that are
|
||||||
@ -13970,7 +13966,7 @@
|
|||||||
*
|
*
|
||||||
* @type string
|
* @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 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="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
|
||||||
<meta name="robots" content="noindex,nofollow" />
|
<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>
|
<title>Bitwarden_rs Admin Panel</title>
|
||||||
<link rel="stylesheet" href="{{urlpath}}/bwrs_static/bootstrap.css" />
|
<link rel="stylesheet" href="{{urlpath}}/bwrs_static/bootstrap.css" />
|
||||||
<style>
|
<style>
|
||||||
@ -73,7 +74,7 @@
|
|||||||
|
|
||||||
<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">
|
<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>
|
<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"
|
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarCollapse"
|
||||||
aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
|
aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
@ -96,7 +97,7 @@
|
|||||||
</li>
|
</li>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
<li class="nav-item">
|
<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>
|
</li>
|
||||||
</ul>
|
</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">
|
<div id="diagnostics-block" class="my-3 p-3 bg-white rounded shadow">
|
||||||
<h6 class="border-bottom pb-2 mb-2">Diagnostics</h6>
|
<h6 class="border-bottom pb-2 mb-2">Diagnostics</h6>
|
||||||
|
|
||||||
@ -15,7 +15,7 @@
|
|||||||
<span id="server-installed">{{version}}</span>
|
<span id="server-installed">{{version}}</span>
|
||||||
</dd>
|
</dd>
|
||||||
<dt class="col-sm-5">Server Latest
|
<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>
|
</dt>
|
||||||
<dd class="col-sm-7">
|
<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>
|
<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>
|
<span id="web-installed">{{diagnostics.web_vault_version}}</span>
|
||||||
</dd>
|
</dd>
|
||||||
<dt class="col-sm-5">Web Latest
|
<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>
|
</dt>
|
||||||
<dd class="col-sm-7">
|
<dd class="col-sm-7">
|
||||||
<span id="web-latest">{{diagnostics.latest_web_build}}</span>
|
<span id="web-latest">{{diagnostics.latest_web_build}}</span>
|
||||||
@ -41,6 +41,40 @@
|
|||||||
<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">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)
|
<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-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>
|
<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-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>
|
<span id="time-browser" class="d-block"><b>Browser:</b> <span id="time-browser-string"></span></span>
|
||||||
</dd>
|
</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>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -64,7 +136,12 @@
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
dnsCheck = false;
|
||||||
|
timeCheck = false;
|
||||||
|
domainCheck = false;
|
||||||
(() => {
|
(() => {
|
||||||
|
// ================================
|
||||||
|
// Date & Time Check
|
||||||
const d = new Date();
|
const d = new Date();
|
||||||
const year = d.getUTCFullYear();
|
const year = d.getUTCFullYear();
|
||||||
const month = String(d.getUTCMonth()+1).padStart(2, '0');
|
const month = String(d.getUTCMonth()+1).padStart(2, '0');
|
||||||
@ -81,16 +158,21 @@
|
|||||||
document.getElementById('time-warning').classList.remove('d-none');
|
document.getElementById('time-warning').classList.remove('d-none');
|
||||||
} else {
|
} else {
|
||||||
document.getElementById('time-success').classList.remove('d-none');
|
document.getElementById('time-success').classList.remove('d-none');
|
||||||
|
timeCheck = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ================================
|
||||||
// Check if the output is a valid IP
|
// 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);
|
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)) {
|
if (isValidIp(document.getElementById('dns-resolved').innerText)) {
|
||||||
document.getElementById('dns-success').classList.remove('d-none');
|
document.getElementById('dns-success').classList.remove('d-none');
|
||||||
|
dnsCheck = true;
|
||||||
} else {
|
} else {
|
||||||
document.getElementById('dns-warning').classList.remove('d-none');
|
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 serverInstalled = document.getElementById('server-installed').innerText;
|
||||||
let serverLatest = document.getElementById('server-latest').innerText;
|
let serverLatest = document.getElementById('server-latest').innerText;
|
||||||
let serverLatestCommit = document.getElementById('server-latest-commit').innerText.replace('-', '');
|
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>
|
</script>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<main class="container">
|
<main class="container-xl">
|
||||||
{{#if error}}
|
{{#if error}}
|
||||||
<div class="align-items-center p-3 mb-3 text-white-50 bg-warning rounded shadow">
|
<div class="align-items-center p-3 mb-3 text-white-50 bg-warning rounded shadow">
|
||||||
<div>
|
<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">
|
<div id="organizations-block" class="my-3 p-3 bg-white rounded shadow">
|
||||||
<h6 class="border-bottom pb-2 mb-3">Organizations</h6>
|
<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 id="config-block" class="align-items-center p-3 mb-3 bg-secondary rounded shadow">
|
||||||
<div>
|
<div>
|
||||||
<h6 class="text-white mb-3">Configuration</h6>
|
<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">
|
<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>
|
||||||
|
|
||||||
@ -7,10 +7,12 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>User</th>
|
<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>Attachments</th>
|
||||||
<th style="min-width: 120px;">Organizations</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>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -21,8 +23,6 @@
|
|||||||
<div class="float-left">
|
<div class="float-left">
|
||||||
<strong>{{Name}}</strong>
|
<strong>{{Name}}</strong>
|
||||||
<span class="d-block">{{Email}}</span>
|
<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">
|
<span class="d-block">
|
||||||
{{#unless user_enabled}}
|
{{#unless user_enabled}}
|
||||||
<span class="badge badge-danger mr-2" title="User is disabled">Disabled</span>
|
<span class="badge badge-danger mr-2" title="User is disabled">Disabled</span>
|
||||||
@ -39,6 +39,12 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="d-block">{{created_at}}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="d-block">{{last_active}}</span>
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="d-block">{{cipher_count}}</span>
|
<span class="d-block">{{cipher_count}}</span>
|
||||||
</td>
|
</td>
|
||||||
@ -49,9 +55,11 @@
|
|||||||
{{/if}}
|
{{/if}}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
<div class="overflow-auto" style="max-height: 120px;">
|
||||||
{{#each Organizations}}
|
{{#each Organizations}}
|
||||||
<span class="badge badge-primary" data-orgtype="{{Type}}">{{Name}}</span>
|
<span class="badge badge-primary" data-orgtype="{{Type}}">{{Name}}</span>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td style="font-size: 90%; text-align: right; padding-right: 15px">
|
<td style="font-size: 90%; text-align: right; padding-right: 15px">
|
||||||
{{#if TwoFactorEnabled}}
|
{{#if TwoFactorEnabled}}
|
||||||
@ -173,18 +181,43 @@
|
|||||||
e.title = orgtype.name;
|
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) {
|
document.addEventListener("DOMContentLoaded", function(event) {
|
||||||
$('#users-table').DataTable({
|
$('#users-table').DataTable({
|
||||||
"responsive": true,
|
"responsive": true,
|
||||||
"lengthMenu": [ [-1, 5, 10, 25, 50], ["All", 5, 10, 25, 50] ],
|
"lengthMenu": [ [-1, 5, 10, 25, 50], ["All", 5, 10, 25, 50] ],
|
||||||
"pageLength": -1, // Default show all
|
"pageLength": -1, // Default show all
|
||||||
"columns": [
|
"columnDefs": [
|
||||||
null, // Userdata
|
{ "targets": [1,2], "type": "date-iso" },
|
||||||
null, // Items
|
{ "targets": 6, "searchable": false, "orderable": false }
|
||||||
null, // Attachments
|
]
|
||||||
null, // Organizations
|
|
||||||
{ "searchable": false, "orderable": false }, // Actions
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|