Multiple Admin Interface fixes and some others.

Misc:
- Fixed hadolint workflow, new git cli needs some extra arguments.
- Add ignore paths to all specific on triggers.
- Updated hadolint version.
- Made SMTP_DEBUG read-only, since it can't be changed at runtime.

Admin:
- Migrated from Bootstrap v4 to v5
- Updated jquery to v3.6.0
- Updated Datatables
- Made Javascript strict
- Added a way to show which ENV Vars are overridden.
- Changed the way to provide data for handlebars.
- Fixed date/time check.
- Made support string use details and summary feature of markdown/github.
pull/1777/head
BlackDex 4 years ago
parent 5772836be5
commit 8615736e84

@ -2,6 +2,19 @@ name: Build
on: on:
push: push:
paths-ignore:
- "*.md"
- "*.txt"
- ".dockerignore"
- ".env.template"
- ".gitattributes"
- ".gitignore"
- "azure-pipelines.yml"
- "docker/**"
- "hooks/**"
- "tools/**"
- ".github/FUNDING.yml"
- ".github/ISSUE_TEMPLATE/**"
pull_request: pull_request:
# Ignore when there are only changes done too one of these paths # Ignore when there are only changes done too one of these paths
paths-ignore: paths-ignore:
@ -39,13 +52,13 @@ jobs:
features: [sqlite,mysql,postgresql] # Remember to update the `cargo test` to match the amount of features features: [sqlite,mysql,postgresql] # Remember to update the `cargo test` to match the amount of features
channel: nightly channel: nightly
os: ubuntu-18.04 os: ubuntu-18.04
ext: ext: ""
# - target-triple: x86_64-unknown-linux-gnu # - target-triple: x86_64-unknown-linux-gnu
# host-triple: x86_64-unknown-linux-gnu # host-triple: x86_64-unknown-linux-gnu
# features: "sqlite,mysql,postgresql" # features: "sqlite,mysql,postgresql"
# channel: stable # channel: stable
# os: ubuntu-18.04 # os: ubuntu-18.04
# ext: # ext: ""
name: Building ${{ matrix.channel }}-${{ matrix.target-triple }} name: Building ${{ matrix.channel }}-${{ matrix.target-triple }}
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}

@ -2,6 +2,9 @@ name: Hadolint
on: on:
push: push:
# Ignore when there are only changes done too one of these paths
paths:
- "docker/**"
pull_request: pull_request:
# Ignore when there are only changes done too one of these paths # Ignore when there are only changes done too one of these paths
paths: paths:
@ -22,14 +25,14 @@ jobs:
- name: Download hadolint - name: Download hadolint
shell: bash shell: bash
run: | run: |
sudo curl -L https://github.com/hadolint/hadolint/releases/download/v$HADOLINT_VERSION/hadolint-$(uname -s)-$(uname -m) -o /usr/local/bin/hadolint && \ sudo curl -L https://github.com/hadolint/hadolint/releases/download/v${HADOLINT_VERSION}/hadolint-$(uname -s)-$(uname -m) -o /usr/local/bin/hadolint && \
sudo chmod +x /usr/local/bin/hadolint sudo chmod +x /usr/local/bin/hadolint
env: env:
HADOLINT_VERSION: 2.3.0 HADOLINT_VERSION: 2.5.0
# End Download hadolint # End Download hadolint
# Test Dockerfiles # Test Dockerfiles
- name: Run hadolint - name: Run hadolint
shell: bash shell: bash
run: git ls-files --exclude='docker/*/Dockerfile*' --ignored | xargs hadolint run: git ls-files --exclude='docker/*/Dockerfile*' --ignored --cached | xargs hadolint
# End Test Dockerfiles # End Test Dockerfiles

@ -196,9 +196,7 @@ fn _validate_token(token: &str) -> bool {
struct AdminTemplateData { struct AdminTemplateData {
page_content: String, page_content: String,
version: Option<&'static str>, version: Option<&'static str>,
users: Option<Vec<Value>>, page_data: Option<Value>,
organizations: Option<Vec<Value>>,
diagnostics: Option<Value>,
config: Value, config: Value,
can_backup: bool, can_backup: bool,
logged_in: bool, logged_in: bool,
@ -214,51 +212,19 @@ impl AdminTemplateData {
can_backup: *CAN_BACKUP, can_backup: *CAN_BACKUP,
logged_in: true, logged_in: true,
urlpath: CONFIG.domain_path(), urlpath: CONFIG.domain_path(),
users: None, page_data: None,
organizations: None,
diagnostics: None,
} }
} }
fn users(users: Vec<Value>) -> Self { fn with_data(page_content: &str, page_data: Value) -> Self {
Self { Self {
page_content: String::from("admin/users"), page_content: String::from(page_content),
version: VERSION, version: VERSION,
users: Some(users), page_data: Some(page_data),
config: CONFIG.prepare_json(), config: CONFIG.prepare_json(),
can_backup: *CAN_BACKUP, can_backup: *CAN_BACKUP,
logged_in: true, logged_in: true,
urlpath: CONFIG.domain_path(), urlpath: CONFIG.domain_path(),
organizations: None,
diagnostics: None,
}
}
fn organizations(organizations: Vec<Value>) -> Self {
Self {
page_content: String::from("admin/organizations"),
version: VERSION,
organizations: Some(organizations),
config: CONFIG.prepare_json(),
can_backup: *CAN_BACKUP,
logged_in: true,
urlpath: CONFIG.domain_path(),
users: None,
diagnostics: None,
}
}
fn diagnostics(diagnostics: Value) -> Self {
Self {
page_content: String::from("admin/diagnostics"),
version: VERSION,
organizations: None,
config: CONFIG.prepare_json(),
can_backup: *CAN_BACKUP,
logged_in: true,
urlpath: CONFIG.domain_path(),
users: None,
diagnostics: Some(diagnostics),
} }
} }
@ -360,7 +326,7 @@ fn users_overview(_token: AdminToken, conn: DbConn) -> ApiResult<Html<String>> {
}) })
.collect(); .collect();
let text = AdminTemplateData::users(users_json).render()?; let text = AdminTemplateData::with_data("admin/users", json!(users_json)).render()?;
Ok(Html(text)) Ok(Html(text))
} }
@ -466,7 +432,7 @@ fn organizations_overview(_token: AdminToken, conn: DbConn) -> ApiResult<Html<St
}) })
.collect(); .collect();
let text = AdminTemplateData::organizations(organizations_json).render()?; let text = AdminTemplateData::with_data("admin/organizations", json!(organizations_json)).render()?;
Ok(Html(text)) Ok(Html(text))
} }
@ -592,11 +558,12 @@ fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> ApiResu
"db_type": *DB_TYPE, "db_type": *DB_TYPE,
"db_version": get_sql_server_version(&conn), "db_version": get_sql_server_version(&conn),
"admin_url": format!("{}/diagnostics", admin_url(Referer(None))), "admin_url": format!("{}/diagnostics", admin_url(Referer(None))),
"overrides": &CONFIG.get_overrides().join(", "),
"server_time_local": Local::now().format("%Y-%m-%d %H:%M:%S %Z").to_string(), "server_time_local": Local::now().format("%Y-%m-%d %H:%M:%S %Z").to_string(),
"server_time": Utc::now().format("%Y-%m-%d %H:%M:%S UTC").to_string(), // Run the date/time check as the last item to minimize the difference "server_time": Utc::now().format("%Y-%m-%d %H:%M:%S UTC").to_string(), // Run the date/time check as the last item to minimize the difference
}); });
let text = AdminTemplateData::diagnostics(diagnostics_json).render()?; let text = AdminTemplateData::with_data("admin/diagnostics", diagnostics_json).render()?;
Ok(Html(text)) Ok(Html(text))
} }

@ -91,8 +91,8 @@ fn static_files(filename: String) -> Result<Content<&'static [u8]>, Error> {
"identicon.js" => Ok(Content(ContentType::JavaScript, include_bytes!("../static/scripts/identicon.js"))), "identicon.js" => Ok(Content(ContentType::JavaScript, include_bytes!("../static/scripts/identicon.js"))),
"datatables.js" => Ok(Content(ContentType::JavaScript, include_bytes!("../static/scripts/datatables.js"))), "datatables.js" => Ok(Content(ContentType::JavaScript, include_bytes!("../static/scripts/datatables.js"))),
"datatables.css" => Ok(Content(ContentType::CSS, include_bytes!("../static/scripts/datatables.css"))), "datatables.css" => Ok(Content(ContentType::CSS, include_bytes!("../static/scripts/datatables.css"))),
"jquery-3.5.1.slim.js" => { "jquery-3.6.0.slim.js" => {
Ok(Content(ContentType::JavaScript, include_bytes!("../static/scripts/jquery-3.5.1.slim.js"))) Ok(Content(ContentType::JavaScript, include_bytes!("../static/scripts/jquery-3.6.0.slim.js")))
} }
_ => err!(format!("Static file not found: {}", filename)), _ => err!(format!("Static file not found: {}", filename)),
} }

@ -57,6 +57,8 @@ macro_rules! make_config {
_env: ConfigBuilder, _env: ConfigBuilder,
_usr: ConfigBuilder, _usr: ConfigBuilder,
_overrides: Vec<String>,
} }
#[derive(Debug, Clone, Default, Deserialize, Serialize)] #[derive(Debug, Clone, Default, Deserialize, Serialize)]
@ -113,8 +115,7 @@ macro_rules! make_config {
/// Merges the values of both builders into a new builder. /// Merges the values of both builders into a new builder.
/// If both have the same element, `other` wins. /// If both have the same element, `other` wins.
fn merge(&self, other: &Self, show_overrides: bool) -> Self { fn merge(&self, other: &Self, show_overrides: bool, overrides: &mut Vec<String>) -> Self {
let mut overrides = Vec::new();
let mut builder = self.clone(); let mut builder = self.clone();
$($( $($(
if let v @Some(_) = &other.$name { if let v @Some(_) = &other.$name {
@ -176,9 +177,9 @@ macro_rules! make_config {
)+)+ )+)+
pub fn prepare_json(&self) -> serde_json::Value { pub fn prepare_json(&self) -> serde_json::Value {
let (def, cfg) = { let (def, cfg, overriden) = {
let inner = &self.inner.read().unwrap(); let inner = &self.inner.read().unwrap();
(inner._env.build(), inner.config.clone()) (inner._env.build(), inner.config.clone(), inner._overrides.clone())
}; };
fn _get_form_type(rust_type: &str) -> &'static str { fn _get_form_type(rust_type: &str) -> &'static str {
@ -210,6 +211,7 @@ macro_rules! make_config {
"default": def.$name, "default": def.$name,
"type": _get_form_type(stringify!($ty)), "type": _get_form_type(stringify!($ty)),
"doc": _get_doc(concat!($($doc),+)), "doc": _get_doc(concat!($($doc),+)),
"overridden": overriden.contains(&stringify!($name).to_uppercase()),
}, )+ }, )+
]}, )+ ]) ]}, )+ ])
} }
@ -224,6 +226,15 @@ macro_rules! make_config {
stringify!($name): make_config!{ @supportstr $name, cfg.$name, $ty, $none_action }, stringify!($name): make_config!{ @supportstr $name, cfg.$name, $ty, $none_action },
)+)+ }) )+)+ })
} }
pub fn get_overrides(&self) -> Vec<String> {
let overrides = {
let inner = &self.inner.read().unwrap();
inner._overrides.clone()
};
overrides
}
} }
}; };
@ -505,7 +516,7 @@ make_config! {
/// Server name sent during HELO |> By default this value should be is on the machine's hostname, but might need to be changed in case it trips some anti-spam filters /// Server name sent during HELO |> By default this value should be is on the machine's hostname, but might need to be changed in case it trips some anti-spam filters
helo_name: String, true, option; helo_name: String, true, option;
/// Enable SMTP debugging (Know the risks!) |> DANGEROUS: Enabling this will output very detailed SMTP messages. This could contain sensitive information like passwords and usernames! Only enable this during troubleshooting! /// Enable SMTP debugging (Know the risks!) |> DANGEROUS: Enabling this will output very detailed SMTP messages. This could contain sensitive information like passwords and usernames! Only enable this during troubleshooting!
smtp_debug: bool, true, def, false; smtp_debug: bool, false, def, false;
/// Accept Invalid Certs (Know the risks!) |> DANGEROUS: Allow invalid certificates. This option introduces significant vulnerabilities to man-in-the-middle attacks! /// Accept Invalid Certs (Know the risks!) |> DANGEROUS: Allow invalid certificates. This option introduces significant vulnerabilities to man-in-the-middle attacks!
smtp_accept_invalid_certs: bool, true, def, false; smtp_accept_invalid_certs: bool, true, def, false;
/// Accept Invalid Hostnames (Know the risks!) |> DANGEROUS: Allow invalid hostnames. This option introduces significant vulnerabilities to man-in-the-middle attacks! /// Accept Invalid Hostnames (Know the risks!) |> DANGEROUS: Allow invalid hostnames. This option introduces significant vulnerabilities to man-in-the-middle attacks!
@ -639,7 +650,8 @@ impl Config {
let _usr = ConfigBuilder::from_file(&CONFIG_FILE).unwrap_or_default(); let _usr = ConfigBuilder::from_file(&CONFIG_FILE).unwrap_or_default();
// Create merged config, config file overwrites env // Create merged config, config file overwrites env
let builder = _env.merge(&_usr, true); let mut _overrides = Vec::new();
let builder = _env.merge(&_usr, true, &mut _overrides);
// Fill any missing with defaults // Fill any missing with defaults
let config = builder.build(); let config = builder.build();
@ -651,6 +663,7 @@ impl Config {
config, config,
_env, _env,
_usr, _usr,
_overrides,
}), }),
}) })
} }
@ -666,9 +679,10 @@ impl Config {
let config_str = serde_json::to_string_pretty(&builder)?; let config_str = serde_json::to_string_pretty(&builder)?;
// Prepare the combined config // Prepare the combined config
let mut overrides = Vec::new();
let config = { let config = {
let env = &self.inner.read().unwrap()._env; let env = &self.inner.read().unwrap()._env;
env.merge(&builder, false).build() env.merge(&builder, false, &mut overrides).build()
}; };
validate_config(&config)?; validate_config(&config)?;
@ -677,6 +691,7 @@ impl Config {
let mut writer = self.inner.write().unwrap(); let mut writer = self.inner.write().unwrap();
writer.config = config; writer.config = config;
writer._usr = builder; writer._usr = builder;
writer._overrides = overrides;
} }
//Save to file //Save to file
@ -690,7 +705,8 @@ impl Config {
pub fn update_config_partial(&self, other: ConfigBuilder) -> Result<(), Error> { pub fn update_config_partial(&self, other: ConfigBuilder) -> Result<(), Error> {
let builder = { let builder = {
let usr = &self.inner.read().unwrap()._usr; let usr = &self.inner.read().unwrap()._usr;
usr.merge(&other, false) let mut _overrides = Vec::new();
usr.merge(&other, false, &mut _overrides)
}; };
self.update_config(builder) self.update_config(builder)
} }
@ -751,6 +767,7 @@ impl Config {
let mut writer = self.inner.write().unwrap(); let mut writer = self.inner.write().unwrap();
writer.config = config; writer.config = config;
writer._usr = usr; writer._usr = usr;
writer._overrides = Vec::new();
} }
Ok(()) Ok(())

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -4,13 +4,18 @@
* *
* 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.23 * https://datatables.net/download/#bs5/dt-1.10.25
* *
* Included libraries: * Included libraries:
* DataTables 1.10.23 * DataTables 1.10.25
*/ */
@charset "UTF-8"; @charset "UTF-8";
/*! Bootstrap 5 integration for DataTables
*
* ©2020 SpryMedia Ltd, all rights reserved.
* License: MIT datatables.net/license/mit
*/
table.dataTable { table.dataTable {
clear: both; clear: both;
margin-top: 6px !important; margin-top: 6px !important;
@ -105,7 +110,7 @@ table.dataTable > thead .sorting_asc_disabled:after,
table.dataTable > thead .sorting_desc_disabled:before, table.dataTable > thead .sorting_desc_disabled:before,
table.dataTable > thead .sorting_desc_disabled:after { table.dataTable > thead .sorting_desc_disabled:after {
position: absolute; position: absolute;
bottom: 0.9em; bottom: 0.5em;
display: block; display: block;
opacity: 0.3; opacity: 0.3;
} }
@ -193,18 +198,27 @@ table.dataTable.table-sm .sorting_desc:after {
table.table-bordered.dataTable { table.table-bordered.dataTable {
border-right-width: 0; border-right-width: 0;
} }
table.table-bordered.dataTable thead tr:first-child th,
table.table-bordered.dataTable thead tr:first-child td {
border-top-width: 1px;
}
table.table-bordered.dataTable th, table.table-bordered.dataTable th,
table.table-bordered.dataTable td { table.table-bordered.dataTable td {
border-left-width: 0; border-left-width: 0;
} }
table.table-bordered.dataTable th:first-child, table.table-bordered.dataTable th:first-child,
table.table-bordered.dataTable td:first-child,
table.table-bordered.dataTable td:first-child {
border-left-width: 1px;
}
table.table-bordered.dataTable th:last-child, table.table-bordered.dataTable th:last-child, table.table-bordered.dataTable th:last-child, table.table-bordered.dataTable th:last-child,
table.table-bordered.dataTable td:last-child, table.table-bordered.dataTable td:last-child,
table.table-bordered.dataTable td:last-child { table.table-bordered.dataTable td:last-child {
border-right-width: 1px; border-right-width: 1px;
} }
table.table-bordered.dataTable tbody th, table.table-bordered.dataTable th,
table.table-bordered.dataTable tbody td { table.table-bordered.dataTable td {
border-bottom-width: 0; border-bottom-width: 1px;
} }
div.dataTables_scrollHead table.table-bordered { div.dataTables_scrollHead table.table-bordered {

@ -4,24 +4,24 @@
* *
* 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.23 * https://datatables.net/download/#bs5/dt-1.10.25
* *
* Included libraries: * Included libraries:
* DataTables 1.10.23 * DataTables 1.10.25
*/ */
/*! DataTables 1.10.23 /*! DataTables 1.10.25
* ©2008-2020 SpryMedia Ltd - datatables.net/license * ©2008-2021 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.23 * @version 1.10.25
* @file jquery.dataTables.js * @file jquery.dataTables.js
* @author SpryMedia Ltd * @author SpryMedia Ltd
* @contact www.datatables.net * @contact www.datatables.net
* @copyright Copyright 2008-2020 SpryMedia Ltd. * @copyright Copyright 2008-2021 SpryMedia Ltd.
* *
* This source file is free software, available under the following license: * This source file is free software, available under the following license:
* MIT license - http://datatables.net/license * MIT license - http://datatables.net/license
@ -1100,6 +1100,8 @@
_fnLanguageCompat( json ); _fnLanguageCompat( json );
_fnCamelToHungarian( defaults.oLanguage, json ); _fnCamelToHungarian( defaults.oLanguage, json );
$.extend( true, oLanguage, json ); $.extend( true, oLanguage, json );
_fnCallbackFire( oSettings, null, 'i18n', [oSettings]);
_fnInitialise( oSettings ); _fnInitialise( oSettings );
}, },
error: function () { error: function () {
@ -1109,6 +1111,9 @@
} ); } );
bInitHandedOff = true; bInitHandedOff = true;
} }
else {
_fnCallbackFire( oSettings, null, 'i18n', [oSettings]);
}
/* /*
* Stripes * Stripes
@ -1260,7 +1265,7 @@
var tbody = $this.children('tbody'); var tbody = $this.children('tbody');
if ( tbody.length === 0 ) { if ( tbody.length === 0 ) {
tbody = $('<tbody/>').appendTo($this); tbody = $('<tbody/>').insertAfter(thead);
} }
oSettings.nTBody = tbody[0]; oSettings.nTBody = tbody[0];
@ -2315,8 +2320,9 @@
} }
// Only a single match is needed for html type since it is // Only a single match is needed for html type since it is
// bottom of the pile and very similar to string // bottom of the pile and very similar to string - but it
if ( detectedType === 'html' ) { // must not be empty
if ( detectedType === 'html' && ! _empty(cache[k]) ) {
break; break;
} }
} }
@ -3421,9 +3427,10 @@
/** /**
* Insert the required TR nodes into the table for display * Insert the required TR nodes into the table for display
* @param {object} oSettings dataTables settings object * @param {object} oSettings dataTables settings object
* @param ajaxComplete true after ajax call to complete rendering
* @memberof DataTable#oApi * @memberof DataTable#oApi
*/ */
function _fnDraw( oSettings ) function _fnDraw( oSettings, ajaxComplete )
{ {
/* Provide a pre-callback function which can be used to cancel the draw is false is returned */ /* Provide a pre-callback function which can be used to cancel the draw is false is returned */
var aPreDraw = _fnCallbackFire( oSettings, 'aoPreDrawCallback', 'preDraw', [oSettings] ); var aPreDraw = _fnCallbackFire( oSettings, 'aoPreDrawCallback', 'preDraw', [oSettings] );
@ -3472,8 +3479,9 @@
{ {
oSettings.iDraw++; oSettings.iDraw++;
} }
else if ( !oSettings.bDestroying && !_fnAjaxUpdate( oSettings ) ) else if ( !oSettings.bDestroying && !ajaxComplete)
{ {
_fnAjaxUpdate( oSettings );
return; return;
} }
@ -4005,21 +4013,16 @@
*/ */
function _fnAjaxUpdate( settings ) function _fnAjaxUpdate( settings )
{ {
if ( settings.bAjaxDataGet ) { settings.iDraw++;
settings.iDraw++; _fnProcessingDisplay( settings, true );
_fnProcessingDisplay( settings, true );
_fnBuildAjax(
settings,
_fnAjaxParameters( settings ),
function(json) {
_fnAjaxUpdateDraw( settings, json );
}
);
return false; _fnBuildAjax(
} settings,
return true; _fnAjaxParameters( settings ),
function(json) {
_fnAjaxUpdateDraw( settings, json );
}
);
} }
@ -4172,14 +4175,12 @@
} }
settings.aiDisplay = settings.aiDisplayMaster.slice(); settings.aiDisplay = settings.aiDisplayMaster.slice();
settings.bAjaxDataGet = false; _fnDraw( settings, true );
_fnDraw( settings );
if ( ! settings._bInitComplete ) { if ( ! settings._bInitComplete ) {
_fnInitComplete( settings, json ); _fnInitComplete( settings, json );
} }
settings.bAjaxDataGet = true;
_fnProcessingDisplay( settings, false ); _fnProcessingDisplay( settings, false );
} }
@ -6108,7 +6109,7 @@
{ {
var col = columns[i]; var col = columns[i];
var asSorting = col.asSorting; var asSorting = col.asSorting;
var sTitle = col.sTitle.replace( /<.*?>/g, "" ); var sTitle = col.ariaTitle || col.sTitle.replace( /<.*?>/g, "" );
var th = col.nTh; var th = col.nTh;
// IE7 is throwing an error when setting these properties with jQuery's // IE7 is throwing an error when setting these properties with jQuery's
@ -9542,7 +9543,7 @@
* @type string * @type string
* @default Version number * @default Version number
*/ */
DataTable.version = "1.10.23"; DataTable.version = "1.10.25";
/** /**
* Private data store, containing all of the settings objects that are * Private data store, containing all of the settings objects that are
@ -13623,13 +13624,6 @@
*/ */
"sAjaxDataProp": null, "sAjaxDataProp": null,
/**
* Note if draw should be blocked while getting data
* @type boolean
* @default true
*/
"bAjaxDataGet": true,
/** /**
* The last jQuery XHR object that was used for server-side data gathering. * The last jQuery XHR object that was used for server-side data gathering.
* This can be used for working with the XHR information in one of the * This can be used for working with the XHR information in one of the
@ -13966,7 +13960,7 @@
* *
* @type string * @type string
*/ */
build:"bs4/dt-1.10.23", build:"bs5/dt-1.10.25",
/** /**
@ -14494,8 +14488,8 @@
"sSortAsc": "sorting_asc", "sSortAsc": "sorting_asc",
"sSortDesc": "sorting_desc", "sSortDesc": "sorting_desc",
"sSortable": "sorting", /* Sortable in both directions */ "sSortable": "sorting", /* Sortable in both directions */
"sSortableAsc": "sorting_asc_disabled", "sSortableAsc": "sorting_desc_disabled",
"sSortableDesc": "sorting_desc_disabled", "sSortableDesc": "sorting_asc_disabled",
"sSortableNone": "sorting_disabled", "sSortableNone": "sorting_disabled",
"sSortColumn": "sorting_", /* Note that an int is postfixed for the sorting order */ "sSortColumn": "sorting_", /* Note that an int is postfixed for the sorting order */
@ -14936,7 +14930,6 @@
cell cell
.removeClass( .removeClass(
column.sSortingClass +' '+
classes.sSortAsc +' '+ classes.sSortAsc +' '+
classes.sSortDesc classes.sSortDesc
) )
@ -15061,6 +15054,11 @@
decimal+(d - intPart).toFixed( precision ).substring( 2 ): decimal+(d - intPart).toFixed( precision ).substring( 2 ):
''; '';
// If zero, then can't have a negative prefix
if (intPart === 0 && parseFloat(floatPart) === 0) {
negative = '';
}
return negative + (prefix||'') + return negative + (prefix||'') +
intPart.toString().replace( intPart.toString().replace(
/\B(?=(\d{3})+(?!\d))/g, thousands /\B(?=(\d{3})+(?!\d))/g, thousands
@ -15395,12 +15393,12 @@
})); }));
/*! DataTables Bootstrap 4 integration /*! DataTables Bootstrap 5 integration
* ©2011-2017 SpryMedia Ltd - datatables.net/license * 2020 SpryMedia Ltd - datatables.net/license
*/ */
/** /**
* DataTables integration for Bootstrap 4. This requires Bootstrap 4 and * DataTables integration for Bootstrap 4. This requires Bootstrap 5 and
* DataTables 1.10 or newer. * DataTables 1.10 or newer.
* *
* This file sets the defaults and adds options to DataTables to style its * This file sets the defaults and adds options to DataTables to style its
@ -15452,9 +15450,9 @@ $.extend( true, DataTable.defaults, {
/* Default class modification */ /* Default class modification */
$.extend( DataTable.ext.classes, { $.extend( DataTable.ext.classes, {
sWrapper: "dataTables_wrapper dt-bootstrap4", sWrapper: "dataTables_wrapper dt-bootstrap5",
sFilterInput: "form-control form-control-sm", sFilterInput: "form-control form-control-sm",
sLengthSelect: "custom-select custom-select-sm form-control form-control-sm", sLengthSelect: "form-select form-select-sm",
sProcessing: "dataTables_processing card", sProcessing: "dataTables_processing card",
sPageButton: "paginate_button page-item" sPageButton: "paginate_button page-item"
} ); } );

@ -1,15 +1,15 @@
/*! /*!
* jQuery JavaScript Library v3.5.1 -ajax,-ajax/jsonp,-ajax/load,-ajax/script,-ajax/var/location,-ajax/var/nonce,-ajax/var/rquery,-ajax/xhr,-manipulation/_evalUrl,-deprecated/ajax-event-alias,-effects,-effects/Tween,-effects/animatedSelector * jQuery JavaScript Library v3.6.0 -ajax,-ajax/jsonp,-ajax/load,-ajax/script,-ajax/var/location,-ajax/var/nonce,-ajax/var/rquery,-ajax/xhr,-manipulation/_evalUrl,-deprecated/ajax-event-alias,-effects,-effects/Tween,-effects/animatedSelector
* https://jquery.com/ * https://jquery.com/
* *
* Includes Sizzle.js * Includes Sizzle.js
* https://sizzlejs.com/ * https://sizzlejs.com/
* *
* Copyright JS Foundation and other contributors * Copyright OpenJS Foundation and other contributors
* Released under the MIT license * Released under the MIT license
* https://jquery.org/license * https://jquery.org/license
* *
* Date: 2020-05-04T22:49Z * Date: 2021-03-02T17:08Z
*/ */
( function( global, factory ) { ( function( global, factory ) {
@ -76,12 +76,16 @@ var support = {};
var isFunction = function isFunction( obj ) { var isFunction = function isFunction( obj ) {
// Support: Chrome <=57, Firefox <=52 // Support: Chrome <=57, Firefox <=52
// In some browsers, typeof returns "function" for HTML <object> elements // In some browsers, typeof returns "function" for HTML <object> elements
// (i.e., `typeof document.createElement( "object" ) === "function"`). // (i.e., `typeof document.createElement( "object" ) === "function"`).
// We don't want to classify *any* DOM node as a function. // We don't want to classify *any* DOM node as a function.
return typeof obj === "function" && typeof obj.nodeType !== "number"; // Support: QtWeb <=3.8.5, WebKit <=534.34, wkhtmltopdf tool <=0.12.5
}; // Plus for old WebKit, typeof returns "function" for HTML collections
// (e.g., `typeof document.getElementsByTagName("div") === "function"`). (gh-4756)
return typeof obj === "function" && typeof obj.nodeType !== "number" &&
typeof obj.item !== "function";
};
var isWindow = function isWindow( obj ) { var isWindow = function isWindow( obj ) {
@ -147,7 +151,7 @@ function toType( obj ) {
var var
version = "3.5.1 -ajax,-ajax/jsonp,-ajax/load,-ajax/script,-ajax/var/location,-ajax/var/nonce,-ajax/var/rquery,-ajax/xhr,-manipulation/_evalUrl,-deprecated/ajax-event-alias,-effects,-effects/Tween,-effects/animatedSelector", version = "3.6.0 -ajax,-ajax/jsonp,-ajax/load,-ajax/script,-ajax/var/location,-ajax/var/nonce,-ajax/var/rquery,-ajax/xhr,-manipulation/_evalUrl,-deprecated/ajax-event-alias,-effects,-effects/Tween,-effects/animatedSelector",
// Define a local copy of jQuery // Define a local copy of jQuery
jQuery = function( selector, context ) { jQuery = function( selector, context ) {
@ -401,7 +405,7 @@ jQuery.extend( {
if ( isArrayLike( Object( arr ) ) ) { if ( isArrayLike( Object( arr ) ) ) {
jQuery.merge( ret, jQuery.merge( ret,
typeof arr === "string" ? typeof arr === "string" ?
[ arr ] : arr [ arr ] : arr
); );
} else { } else {
push.call( ret, arr ); push.call( ret, arr );
@ -496,9 +500,9 @@ if ( typeof Symbol === "function" ) {
// Populate the class2type map // Populate the class2type map
jQuery.each( "Boolean Number String Function Array Date RegExp Object Error Symbol".split( " " ), jQuery.each( "Boolean Number String Function Array Date RegExp Object Error Symbol".split( " " ),
function( _i, name ) { function( _i, name ) {
class2type[ "[object " + name + "]" ] = name.toLowerCase(); class2type[ "[object " + name + "]" ] = name.toLowerCase();
} ); } );
function isArrayLike( obj ) { function isArrayLike( obj ) {
@ -518,14 +522,14 @@ function isArrayLike( obj ) {
} }
var Sizzle = var Sizzle =
/*! /*!
* Sizzle CSS Selector Engine v2.3.5 * Sizzle CSS Selector Engine v2.3.6
* https://sizzlejs.com/ * https://sizzlejs.com/
* *
* Copyright JS Foundation and other contributors * Copyright JS Foundation and other contributors
* Released under the MIT license * Released under the MIT license
* https://js.foundation/ * https://js.foundation/
* *
* Date: 2020-03-14 * Date: 2021-02-16
*/ */
( function( window ) { ( function( window ) {
var i, var i,
@ -1108,8 +1112,8 @@ support = Sizzle.support = {};
* @returns {Boolean} True iff elem is a non-HTML XML node * @returns {Boolean} True iff elem is a non-HTML XML node
*/ */
isXML = Sizzle.isXML = function( elem ) { isXML = Sizzle.isXML = function( elem ) {
var namespace = elem.namespaceURI, var namespace = elem && elem.namespaceURI,
docElem = ( elem.ownerDocument || elem ).documentElement; docElem = elem && ( elem.ownerDocument || elem ).documentElement;
// Support: IE <=8 // Support: IE <=8
// Assume HTML when documentElement doesn't yet exist, such as inside loading iframes // Assume HTML when documentElement doesn't yet exist, such as inside loading iframes
@ -3024,9 +3028,9 @@ var rneedsContext = jQuery.expr.match.needsContext;
function nodeName( elem, name ) { function nodeName( elem, name ) {
return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase(); return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase();
}; }
var rsingleTag = ( /^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i ); var rsingleTag = ( /^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i );
@ -3997,8 +4001,8 @@ jQuery.extend( {
resolveContexts = Array( i ), resolveContexts = Array( i ),
resolveValues = slice.call( arguments ), resolveValues = slice.call( arguments ),
// the master Deferred // the primary Deferred
master = jQuery.Deferred(), primary = jQuery.Deferred(),
// subordinate callback factory // subordinate callback factory
updateFunc = function( i ) { updateFunc = function( i ) {
@ -4006,30 +4010,30 @@ jQuery.extend( {
resolveContexts[ i ] = this; resolveContexts[ i ] = this;
resolveValues[ i ] = arguments.length > 1 ? slice.call( arguments ) : value; resolveValues[ i ] = arguments.length > 1 ? slice.call( arguments ) : value;
if ( !( --remaining ) ) { if ( !( --remaining ) ) {
master.resolveWith( resolveContexts, resolveValues ); primary.resolveWith( resolveContexts, resolveValues );
} }
}; };
}; };
// Single- and empty arguments are adopted like Promise.resolve // Single- and empty arguments are adopted like Promise.resolve
if ( remaining <= 1 ) { if ( remaining <= 1 ) {
adoptValue( singleValue, master.done( updateFunc( i ) ).resolve, master.reject, adoptValue( singleValue, primary.done( updateFunc( i ) ).resolve, primary.reject,
!remaining ); !remaining );
// Use .then() to unwrap secondary thenables (cf. gh-3000) // Use .then() to unwrap secondary thenables (cf. gh-3000)
if ( master.state() === "pending" || if ( primary.state() === "pending" ||
isFunction( resolveValues[ i ] && resolveValues[ i ].then ) ) { isFunction( resolveValues[ i ] && resolveValues[ i ].then ) ) {
return master.then(); return primary.then();
} }
} }
// Multiple arguments are aggregated like Promise.all array elements // Multiple arguments are aggregated like Promise.all array elements
while ( i-- ) { while ( i-- ) {
adoptValue( resolveValues[ i ], updateFunc( i ), master.reject ); adoptValue( resolveValues[ i ], updateFunc( i ), primary.reject );
} }
return master.promise(); return primary.promise();
} }
} ); } );
@ -4180,8 +4184,8 @@ var access = function( elems, fn, key, value, chainable, emptyGet, raw ) {
for ( ; i < len; i++ ) { for ( ; i < len; i++ ) {
fn( fn(
elems[ i ], key, raw ? elems[ i ], key, raw ?
value : value :
value.call( elems[ i ], i, fn( elems[ i ], key ) ) value.call( elems[ i ], i, fn( elems[ i ], key ) )
); );
} }
} }
@ -5089,10 +5093,7 @@ function buildFragment( elems, context, scripts, selection, ignored ) {
} }
var var rtypenamespace = /^([^.]*)(?:\.(.+)|)/;
rkeyEvent = /^key/,
rmouseEvent = /^(?:mouse|pointer|contextmenu|drag|drop)|click/,
rtypenamespace = /^([^.]*)(?:\.(.+)|)/;
function returnTrue() { function returnTrue() {
return true; return true;
@ -5387,8 +5388,8 @@ jQuery.event = {
event = jQuery.event.fix( nativeEvent ), event = jQuery.event.fix( nativeEvent ),
handlers = ( handlers = (
dataPriv.get( this, "events" ) || Object.create( null ) dataPriv.get( this, "events" ) || Object.create( null )
)[ event.type ] || [], )[ event.type ] || [],
special = jQuery.event.special[ event.type ] || {}; special = jQuery.event.special[ event.type ] || {};
// Use the fix-ed jQuery.Event rather than the (read-only) native event // Use the fix-ed jQuery.Event rather than the (read-only) native event
@ -5512,12 +5513,12 @@ jQuery.event = {
get: isFunction( hook ) ? get: isFunction( hook ) ?
function() { function() {
if ( this.originalEvent ) { if ( this.originalEvent ) {
return hook( this.originalEvent ); return hook( this.originalEvent );
} }
} : } :
function() { function() {
if ( this.originalEvent ) { if ( this.originalEvent ) {
return this.originalEvent[ name ]; return this.originalEvent[ name ];
} }
}, },
@ -5656,7 +5657,13 @@ function leverageNative( el, type, expectSync ) {
// Cancel the outer synthetic event // Cancel the outer synthetic event
event.stopImmediatePropagation(); event.stopImmediatePropagation();
event.preventDefault(); event.preventDefault();
return result.value;
// Support: Chrome 86+
// In Chrome, if an element having a focusout handler is blurred by
// clicking outside of it, it invokes the handler synchronously. If
// that handler calls `.remove()` on the element, the data is cleared,
// leaving `result` undefined. We need to guard against this.
return result && result.value;
} }
// If this is an inner synthetic event for an event with a bubbling surrogate // If this is an inner synthetic event for an event with a bubbling surrogate
@ -5821,34 +5828,7 @@ jQuery.each( {
targetTouches: true, targetTouches: true,
toElement: true, toElement: true,
touches: true, touches: true,
which: true
which: function( event ) {
var button = event.button;
// Add which for key events
if ( event.which == null && rkeyEvent.test( event.type ) ) {
return event.charCode != null ? event.charCode : event.keyCode;
}
// Add which for click: 1 === left; 2 === middle; 3 === right
if ( !event.which && button !== undefined && rmouseEvent.test( event.type ) ) {
if ( button & 1 ) {
return 1;
}
if ( button & 2 ) {
return 3;
}
if ( button & 4 ) {
return 2;
}
return 0;
}
return event.which;
}
}, jQuery.event.addProp ); }, jQuery.event.addProp );
jQuery.each( { focus: "focusin", blur: "focusout" }, function( type, delegateType ) { jQuery.each( { focus: "focusin", blur: "focusout" }, function( type, delegateType ) {
@ -5874,6 +5854,12 @@ jQuery.each( { focus: "focusin", blur: "focusout" }, function( type, delegateTyp
return true; return true;
}, },
// Suppress native focus or blur as it's already being fired
// in leverageNative.
_default: function() {
return true;
},
delegateType: delegateType delegateType: delegateType
}; };
} ); } );
@ -6541,6 +6527,10 @@ var rboxStyle = new RegExp( cssExpand.join( "|" ), "i" );
// set in CSS while `offset*` properties report correct values. // set in CSS while `offset*` properties report correct values.
// Behavior in IE 9 is more subtle than in newer versions & it passes // Behavior in IE 9 is more subtle than in newer versions & it passes
// some versions of this test; make sure not to make it pass there! // some versions of this test; make sure not to make it pass there!
//
// Support: Firefox 70+
// Only Firefox includes border widths
// in computed dimensions. (gh-4529)
reliableTrDimensions: function() { reliableTrDimensions: function() {
var table, tr, trChild, trStyle; var table, tr, trChild, trStyle;
if ( reliableTrDimensionsVal == null ) { if ( reliableTrDimensionsVal == null ) {
@ -6548,17 +6538,32 @@ var rboxStyle = new RegExp( cssExpand.join( "|" ), "i" );
tr = document.createElement( "tr" ); tr = document.createElement( "tr" );
trChild = document.createElement( "div" ); trChild = document.createElement( "div" );
table.style.cssText = "position:absolute;left:-11111px"; table.style.cssText = "position:absolute;left:-11111px;border-collapse:separate";
tr.style.cssText = "border:1px solid";
// Support: Chrome 86+
// Height set through cssText does not get applied.
// Computed height then comes back as 0.
tr.style.height = "1px"; tr.style.height = "1px";
trChild.style.height = "9px"; trChild.style.height = "9px";
// Support: Android 8 Chrome 86+
// In our bodyBackground.html iframe,
// display for all div elements is set to "inline",
// which causes a problem only in Android 8 Chrome 86.
// Ensuring the div is display: block
// gets around this issue.
trChild.style.display = "block";
documentElement documentElement
.appendChild( table ) .appendChild( table )
.appendChild( tr ) .appendChild( tr )
.appendChild( trChild ); .appendChild( trChild );
trStyle = window.getComputedStyle( tr ); trStyle = window.getComputedStyle( tr );
reliableTrDimensionsVal = parseInt( trStyle.height ) > 3; reliableTrDimensionsVal = ( parseInt( trStyle.height, 10 ) +
parseInt( trStyle.borderTopWidth, 10 ) +
parseInt( trStyle.borderBottomWidth, 10 ) ) === tr.offsetHeight;
documentElement.removeChild( table ); documentElement.removeChild( table );
} }
@ -7022,10 +7027,10 @@ jQuery.each( [ "height", "width" ], function( _i, dimension ) {
// Running getBoundingClientRect on a disconnected node // Running getBoundingClientRect on a disconnected node
// in IE throws an error. // in IE throws an error.
( !elem.getClientRects().length || !elem.getBoundingClientRect().width ) ? ( !elem.getClientRects().length || !elem.getBoundingClientRect().width ) ?
swap( elem, cssShow, function() { swap( elem, cssShow, function() {
return getWidthOrHeight( elem, dimension, extra ); return getWidthOrHeight( elem, dimension, extra );
} ) : } ) :
getWidthOrHeight( elem, dimension, extra ); getWidthOrHeight( elem, dimension, extra );
} }
}, },
@ -7084,7 +7089,7 @@ jQuery.cssHooks.marginLeft = addGetHookIf( support.reliableMarginLeft,
swap( elem, { marginLeft: 0 }, function() { swap( elem, { marginLeft: 0 }, function() {
return elem.getBoundingClientRect().left; return elem.getBoundingClientRect().left;
} ) } )
) + "px"; ) + "px";
} }
} }
); );
@ -7608,8 +7613,8 @@ jQuery.fn.extend( {
if ( this.setAttribute ) { if ( this.setAttribute ) {
this.setAttribute( "class", this.setAttribute( "class",
className || value === false ? className || value === false ?
"" : "" :
dataPriv.get( this, "__className__" ) || "" dataPriv.get( this, "__className__" ) || ""
); );
} }
} }
@ -7624,7 +7629,7 @@ jQuery.fn.extend( {
while ( ( elem = this[ i++ ] ) ) { while ( ( elem = this[ i++ ] ) ) {
if ( elem.nodeType === 1 && if ( elem.nodeType === 1 &&
( " " + stripAndCollapse( getClass( elem ) ) + " " ).indexOf( className ) > -1 ) { ( " " + stripAndCollapse( getClass( elem ) ) + " " ).indexOf( className ) > -1 ) {
return true; return true;
} }
} }
@ -7914,9 +7919,7 @@ jQuery.extend( jQuery.event, {
special.bindType || type; special.bindType || type;
// jQuery handler // jQuery handler
handle = ( handle = ( dataPriv.get( cur, "events" ) || Object.create( null ) )[ event.type ] &&
dataPriv.get( cur, "events" ) || Object.create( null )
)[ event.type ] &&
dataPriv.get( cur, "handle" ); dataPriv.get( cur, "handle" );
if ( handle ) { if ( handle ) {
handle.apply( cur, data ); handle.apply( cur, data );
@ -8057,7 +8060,7 @@ if ( !support.focusin ) {
// Cross-browser xml parsing // Cross-browser xml parsing
jQuery.parseXML = function( data ) { jQuery.parseXML = function( data ) {
var xml; var xml, parserErrorElem;
if ( !data || typeof data !== "string" ) { if ( !data || typeof data !== "string" ) {
return null; return null;
} }
@ -8066,12 +8069,17 @@ jQuery.parseXML = function( data ) {
// IE throws on parseFromString with invalid input. // IE throws on parseFromString with invalid input.
try { try {
xml = ( new window.DOMParser() ).parseFromString( data, "text/xml" ); xml = ( new window.DOMParser() ).parseFromString( data, "text/xml" );
} catch ( e ) { } catch ( e ) {}
xml = undefined;
} parserErrorElem = xml && xml.getElementsByTagName( "parsererror" )[ 0 ];
if ( !xml || parserErrorElem ) {
if ( !xml || xml.getElementsByTagName( "parsererror" ).length ) { jQuery.error( "Invalid XML: " + (
jQuery.error( "Invalid XML: " + data ); parserErrorElem ?
jQuery.map( parserErrorElem.childNodes, function( el ) {
return el.textContent;
} ).join( "\n" ) :
data
) );
} }
return xml; return xml;
}; };
@ -8172,16 +8180,14 @@ jQuery.fn.extend( {
// Can add propHook for "elements" to filter or add form elements // Can add propHook for "elements" to filter or add form elements
var elements = jQuery.prop( this, "elements" ); var elements = jQuery.prop( this, "elements" );
return elements ? jQuery.makeArray( elements ) : this; return elements ? jQuery.makeArray( elements ) : this;
} ) } ).filter( function() {
.filter( function() {
var type = this.type; var type = this.type;
// Use .is( ":disabled" ) so that fieldset[disabled] works // Use .is( ":disabled" ) so that fieldset[disabled] works
return this.name && !jQuery( this ).is( ":disabled" ) && return this.name && !jQuery( this ).is( ":disabled" ) &&
rsubmittable.test( this.nodeName ) && !rsubmitterTypes.test( type ) && rsubmittable.test( this.nodeName ) && !rsubmitterTypes.test( type ) &&
( this.checked || !rcheckableType.test( type ) ); ( this.checked || !rcheckableType.test( type ) );
} ) } ).map( function( _i, elem ) {
.map( function( _i, elem ) {
var val = jQuery( this ).val(); var val = jQuery( this ).val();
if ( val == null ) { if ( val == null ) {
@ -8387,12 +8393,6 @@ jQuery.offset = {
options.using.call( elem, props ); options.using.call( elem, props );
} else { } else {
if ( typeof props.top === "number" ) {
props.top += "px";
}
if ( typeof props.left === "number" ) {
props.left += "px";
}
curElem.css( props ); curElem.css( props );
} }
} }
@ -8561,8 +8561,11 @@ jQuery.each( [ "top", "left" ], function( _i, prop ) {
// Create innerHeight, innerWidth, height, width, outerHeight and outerWidth methods // Create innerHeight, innerWidth, height, width, outerHeight and outerWidth methods
jQuery.each( { Height: "height", Width: "width" }, function( name, type ) { jQuery.each( { Height: "height", Width: "width" }, function( name, type ) {
jQuery.each( { padding: "inner" + name, content: type, "": "outer" + name }, jQuery.each( {
function( defaultExtra, funcName ) { padding: "inner" + name,
content: type,
"": "outer" + name
}, function( defaultExtra, funcName ) {
// Margin is only for outerHeight, outerWidth // Margin is only for outerHeight, outerWidth
jQuery.fn[ funcName ] = function( margin, value ) { jQuery.fn[ funcName ] = function( margin, value ) {
@ -8631,7 +8634,8 @@ jQuery.fn.extend( {
} }
} ); } );
jQuery.each( ( "blur focus focusin focusout resize scroll click dblclick " + jQuery.each(
( "blur focus focusin focusout resize scroll click dblclick " +
"mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " + "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " +
"change select submit keydown keypress keyup contextmenu" ).split( " " ), "change select submit keydown keypress keyup contextmenu" ).split( " " ),
function( _i, name ) { function( _i, name ) {
@ -8642,7 +8646,8 @@ jQuery.each( ( "blur focus focusin focusout resize scroll click dblclick " +
this.on( name, null, data, fn ) : this.on( name, null, data, fn ) :
this.trigger( name ); this.trigger( name );
}; };
} ); }
);

@ -15,14 +15,16 @@
width: 48px; width: 48px;
height: 48px; height: 48px;
} }
.navbar .vaultwarden-icon { .vaultwarden-icon {
height: 32px; height: 32px;
width: auto; width: auto;
margin: -5px -3px 0 0; margin: -5px 0 0 0;
} }
</style> </style>
<script src="{{urlpath}}/bwrs_static/identicon.js"></script> <script src="{{urlpath}}/bwrs_static/identicon.js"></script>
<script> <script>
'use strict';
function reload() { window.location.reload(); } function reload() { window.location.reload(); }
function msg(text, reload_page = true) { function msg(text, reload_page = true) {
text && alert(text); text && alert(text);
@ -78,19 +80,18 @@
}); });
} }
</script> </script>
</head> </head>
<body class="bg-light"> <body class="bg-light">
<nav class="navbar navbar-expand-md navbar-dark bg-dark mb-4 shadow fixed-top"> <nav class="navbar navbar-expand-md navbar-dark bg-dark mb-4 shadow fixed-top">
<div class="container-xl"> <div class="container-xl">
<a class="navbar-brand" href="{{urlpath}}/admin"><img class="pr-1 vaultwarden-icon" src="{{urlpath}}/bwrs_static/vaultwarden-icon.png" alt="V">aultwarden Admin</a> <a class="navbar-brand" href="{{urlpath}}/admin"><img class="vaultwarden-icon" src="{{urlpath}}/bwrs_static/vaultwarden-icon.png" alt="V">aultwarden Admin</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarCollapse" <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarCollapse"
aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation"> aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
</button> </button>
<div class="collapse navbar-collapse" id="navbarCollapse"> <div class="collapse navbar-collapse" id="navbarCollapse">
<ul class="navbar-nav mr-auto"> <ul class="navbar-nav me-auto">
{{#if logged_in}} {{#if logged_in}}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{{urlpath}}/admin">Settings</a> <a class="nav-link" href="{{urlpath}}/admin">Settings</a>
@ -121,17 +122,19 @@
<!-- This script needs to be at the bottom, else it will fail! --> <!-- This script needs to be at the bottom, else it will fail! -->
<script> <script>
'use strict';
// get current URL path and assign 'active' class to the correct nav-item // get current URL path and assign 'active' class to the correct nav-item
(function () { (() => {
var pathname = window.location.pathname; var pathname = window.location.pathname;
if (pathname === "") return; if (pathname === "") return;
var navItem = document.querySelectorAll('.navbar-nav .nav-item a[href="'+pathname+'"]'); var navItem = document.querySelectorAll('.navbar-nav .nav-item a[href="'+pathname+'"]');
if (navItem.length === 1) { if (navItem.length === 1) {
navItem[0].parentElement.className = navItem[0].parentElement.className + ' active'; navItem[0].className = navItem[0].className + ' active';
navItem[0].setAttribute('aria-current', 'page');
} }
})(); })();
</script> </script>
<!-- This script needs to be at the bottom, else it will fail! -->
<script src="{{urlpath}}/bwrs_static/bootstrap-native.js"></script> <script src="{{urlpath}}/bwrs_static/bootstrap-native.js"></script>
</body> </body>
</html> </html>

@ -7,37 +7,37 @@
<div class="col-md"> <div class="col-md">
<dl class="row"> <dl class="row">
<dt class="col-sm-5">Server Installed <dt class="col-sm-5">Server Installed
<span class="badge badge-success d-none" id="server-success" title="Latest version is installed.">Ok</span> <span class="badge bg-success d-none" id="server-success" title="Latest version is installed.">Ok</span>
<span class="badge badge-warning d-none" id="server-warning" title="There seems to be an update available.">Update</span> <span class="badge bg-warning d-none" id="server-warning" title="There seems to be an update available.">Update</span>
<span class="badge badge-info d-none" id="server-branch" title="This is a branched version.">Branched</span> <span class="badge bg-info d-none" id="server-branch" title="This is a branched version.">Branched</span>
</dt> </dt>
<dd class="col-sm-7"> <dd class="col-sm-7">
<span id="server-installed">{{version}}</span> <span id="server-installed">{{version}}</span>
</dd> </dd>
<dt class="col-sm-5">Server Latest <dt class="col-sm-5">Server Latest
<span class="badge badge-secondary d-none" id="server-failed" title="Unable to determine latest version.">Unknown</span> <span class="badge bg-secondary d-none" id="server-failed" title="Unable to determine latest version.">Unknown</span>
</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">{{page_data.latest_release}}<span id="server-latest-commit" class="d-none">-{{page_data.latest_commit}}</span></span>
</dd> </dd>
{{#if diagnostics.web_vault_enabled}} {{#if page_data.web_vault_enabled}}
<dt class="col-sm-5">Web Installed <dt class="col-sm-5">Web Installed
<span class="badge badge-success d-none" id="web-success" title="Latest version is installed.">Ok</span> <span class="badge bg-success d-none" id="web-success" title="Latest version is installed.">Ok</span>
<span class="badge badge-warning d-none" id="web-warning" title="There seems to be an update available.">Update</span> <span class="badge bg-warning d-none" id="web-warning" title="There seems to be an update available.">Update</span>
</dt> </dt>
<dd class="col-sm-7"> <dd class="col-sm-7">
<span id="web-installed">{{diagnostics.web_vault_version}}</span> <span id="web-installed">{{page_data.web_vault_version}}</span>
</dd> </dd>
{{#unless diagnostics.running_within_docker}} {{#unless page_data.running_within_docker}}
<dt class="col-sm-5">Web Latest <dt class="col-sm-5">Web Latest
<span class="badge badge-secondary d-none" id="web-failed" title="Unable to determine latest version.">Unknown</span> <span class="badge bg-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">{{page_data.latest_web_build}}</span>
</dd> </dd>
{{/unless}} {{/unless}}
{{/if}} {{/if}}
{{#unless diagnostics.web_vault_enabled}} {{#unless page_data.web_vault_enabled}}
<dt class="col-sm-5">Web Installed</dt> <dt class="col-sm-5">Web Installed</dt>
<dd class="col-sm-7"> <dd class="col-sm-7">
<span id="web-installed">Web Vault is disabled</span> <span id="web-installed">Web Vault is disabled</span>
@ -45,7 +45,7 @@
{{/unless}} {{/unless}}
<dt class="col-sm-5">Database</dt> <dt class="col-sm-5">Database</dt>
<dd class="col-sm-7"> <dd class="col-sm-7">
<span><b>{{diagnostics.db_type}}:</b> {{diagnostics.db_version}}</span> <span><b>{{page_data.db_type}}:</b> {{page_data.db_version}}</span>
</dd> </dd>
</dl> </dl>
</div> </div>
@ -57,96 +57,105 @@
<dl class="row"> <dl class="row">
<dt class="col-sm-5">Running within Docker</dt> <dt class="col-sm-5">Running within Docker</dt>
<dd class="col-sm-7"> <dd class="col-sm-7">
{{#if diagnostics.running_within_docker}} {{#if page_data.running_within_docker}}
<span class="d-block"><b>Yes</b></span> <span class="d-block"><b>Yes</b></span>
{{/if}} {{/if}}
{{#unless diagnostics.running_within_docker}} {{#unless page_data.running_within_docker}}
<span class="d-block"><b>No</b></span>
{{/unless}}
</dd>
<dt class="col-sm-5">Environment settings overridden</dt>
<dd class="col-sm-7">
{{#if page_data.overrides}}
<span class="d-block" title="The following settings are overridden: {{page_data.overrides}}"><b>Yes</b></span>
{{/if}}
{{#unless page_data.overrides}}
<span class="d-block"><b>No</b></span> <span class="d-block"><b>No</b></span>
{{/unless}} {{/unless}}
</dd> </dd>
<dt class="col-sm-5">Uses a reverse proxy</dt> <dt class="col-sm-5">Uses a reverse proxy</dt>
<dd class="col-sm-7"> <dd class="col-sm-7">
{{#if diagnostics.ip_header_exists}} {{#if page_data.ip_header_exists}}
<span class="d-block" title="IP Header found."><b>Yes</b></span> <span class="d-block" title="IP Header found."><b>Yes</b></span>
{{/if}} {{/if}}
{{#unless diagnostics.ip_header_exists}} {{#unless page_data.ip_header_exists}}
<span class="d-block" title="No IP Header found."><b>No</b></span> <span class="d-block" title="No IP Header found."><b>No</b></span>
{{/unless}} {{/unless}}
</dd> </dd>
{{!-- Only show this if the IP Header Exists --}} {{!-- Only show this if the IP Header Exists --}}
{{#if diagnostics.ip_header_exists}} {{#if page_data.ip_header_exists}}
<dt class="col-sm-5">IP header <dt class="col-sm-5">IP header
{{#if diagnostics.ip_header_match}} {{#if page_data.ip_header_match}}
<span class="badge badge-success" title="IP_HEADER config seems to be valid.">Match</span> <span class="badge bg-success" title="IP_HEADER config seems to be valid.">Match</span>
{{/if}} {{/if}}
{{#unless diagnostics.ip_header_match}} {{#unless page_data.ip_header_match}}
<span class="badge badge-danger" title="IP_HEADER config seems to be invalid. IP's in the log could be invalid. Please fix.">No Match</span> <span class="badge bg-danger" title="IP_HEADER config seems to be invalid. IP's in the log could be invalid. Please fix.">No Match</span>
{{/unless}} {{/unless}}
</dt> </dt>
<dd class="col-sm-7"> <dd class="col-sm-7">
{{#if diagnostics.ip_header_match}} {{#if page_data.ip_header_match}}
<span class="d-block"><b>Config/Server:</b> {{ diagnostics.ip_header_name }}</span> <span class="d-block"><b>Config/Server:</b> {{ page_data.ip_header_name }}</span>
{{/if}} {{/if}}
{{#unless diagnostics.ip_header_match}} {{#unless page_data.ip_header_match}}
<span class="d-block"><b>Config:</b> {{ diagnostics.ip_header_config }}</span> <span class="d-block"><b>Config:</b> {{ page_data.ip_header_config }}</span>
<span class="d-block"><b>Server:</b> {{ diagnostics.ip_header_name }}</span> <span class="d-block"><b>Server:</b> {{ page_data.ip_header_name }}</span>
{{/unless}} {{/unless}}
</dd> </dd>
{{/if}} {{/if}}
{{!-- End if IP Header Exists --}} {{!-- End if IP Header Exists --}}
<dt class="col-sm-5">Internet access <dt class="col-sm-5">Internet access
{{#if diagnostics.has_http_access}} {{#if page_data.has_http_access}}
<span class="badge badge-success" title="We have internet access!">Ok</span> <span class="badge bg-success" title="We have internet access!">Ok</span>
{{/if}} {{/if}}
{{#unless diagnostics.has_http_access}} {{#unless page_data.has_http_access}}
<span class="badge badge-danger" title="There seems to be no internet access. Please fix.">Error</span> <span class="badge bg-danger" title="There seems to be no internet access. Please fix.">Error</span>
{{/unless}} {{/unless}}
</dt> </dt>
<dd class="col-sm-7"> <dd class="col-sm-7">
{{#if diagnostics.has_http_access}} {{#if page_data.has_http_access}}
<span class="d-block"><b>Yes</b></span> <span class="d-block"><b>Yes</b></span>
{{/if}} {{/if}}
{{#unless diagnostics.has_http_access}} {{#unless page_data.has_http_access}}
<span class="d-block"><b>No</b></span> <span class="d-block"><b>No</b></span>
{{/unless}} {{/unless}}
</dd> </dd>
<dt class="col-sm-5">Internet access via a proxy</dt> <dt class="col-sm-5">Internet access via a proxy</dt>
<dd class="col-sm-7"> <dd class="col-sm-7">
{{#if diagnostics.uses_proxy}} {{#if page_data.uses_proxy}}
<span class="d-block" title="Internet access goes via a proxy (HTTPS_PROXY or HTTP_PROXY is configured)."><b>Yes</b></span> <span class="d-block" title="Internet access goes via a proxy (HTTPS_PROXY or HTTP_PROXY is configured)."><b>Yes</b></span>
{{/if}} {{/if}}
{{#unless diagnostics.uses_proxy}} {{#unless page_data.uses_proxy}}
<span class="d-block" title="We have direct internet access, no outgoing proxy configured."><b>No</b></span> <span class="d-block" title="We have direct internet access, no outgoing proxy configured."><b>No</b></span>
{{/unless}} {{/unless}}
</dd> </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 bg-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 bg-danger d-none" id="dns-warning" title="DNS Resolving failed. Please fix.">Error</span>
</dt> </dt>
<dd class="col-sm-7"> <dd class="col-sm-7">
<span id="dns-resolved">{{diagnostics.dns_resolved}}</span> <span id="dns-resolved">{{page_data.dns_resolved}}</span>
</dd> </dd>
<dt class="col-sm-5">Date & Time (Local)</dt> <dt class="col-sm-5">Date & Time (Local)</dt>
<dd class="col-sm-7"> <dd class="col-sm-7">
<span><b>Server:</b> {{diagnostics.server_time_local}}</span> <span><b>Server:</b> {{page_data.server_time_local}}</span>
</dd> </dd>
<dt class="col-sm-5">Date & Time (UTC) <dt class="col-sm-5">Date & Time (UTC)
<span class="badge badge-success d-none" id="time-success" title="Time offsets seem to be correct.">Ok</span> <span class="badge bg-success d-none" id="time-success" title="Time offsets seem to be correct.">Ok</span>
<span class="badge badge-danger d-none" id="time-warning" title="Time offsets are too mouch at drift.">Error</span> <span class="badge bg-danger d-none" id="time-warning" title="Time offsets are too mouch at drift.">Error</span>
</dt> </dt>
<dd class="col-sm-7"> <dd class="col-sm-7">
<span id="time-server" class="d-block"><b>Server:</b> <span id="time-server-string">{{diagnostics.server_time}}</span></span> <span id="time-server" class="d-block"><b>Server:</b> <span id="time-server-string">{{page_data.server_time}}</span></span>
<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 <dt class="col-sm-5">Domain configuration
<span class="badge badge-success d-none" id="domain-success" title="The domain variable matches the browser location and seems to be configured correctly.">Match</span> <span class="badge bg-success d-none" id="domain-success" title="The domain variable matches the browser location and seems to be configured correctly.">Match</span>
<span class="badge badge-danger d-none" id="domain-warning" title="The domain variable does not matches the browsers location.&#013;&#010;The domain variable does not seem to be configured correctly.&#013;&#010;Some features may not work as expected!">No Match</span> <span class="badge bg-danger d-none" id="domain-warning" title="The domain variable does not matches the browsers location.&#013;&#010;The domain variable does not seem to be configured correctly.&#013;&#010;Some features may not work as expected!">No Match</span>
<span class="badge badge-success d-none" id="https-success" title="Configurued to use HTTPS">HTTPS</span> <span class="badge bg-success d-none" id="https-success" title="Configurued to use HTTPS">HTTPS</span>
<span class="badge badge-danger d-none" id="https-warning" title="Not configured to use HTTPS.&#013;&#010;Some features may not work as expected!">No HTTPS</span> <span class="badge bg-danger d-none" id="https-warning" title="Not configured to use HTTPS.&#013;&#010;Some features may not work as expected!">No HTTPS</span>
</dt> </dt>
<dd class="col-sm-7"> <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-server" class="d-block"><b>Server:</b> <span id="domain-server-string">{{page_data.admin_url}}</span></span>
<span id="domain-browser" class="d-block"><b>Browser:</b> <span id="domain-browser-string"></span></span> <span id="domain-browser" class="d-block"><b>Browser:</b> <span id="domain-browser-string"></span></span>
</dd> </dd>
</dl> </dl>
@ -173,10 +182,17 @@
<dt class="col-sm-3"> <dt class="col-sm-3">
<button type="button" id="gen-support" class="btn btn-primary" onclick="generateSupportString(); return false;">Generate Support String</button> <button type="button" id="gen-support" class="btn btn-primary" onclick="generateSupportString(); return false;">Generate Support String</button>
<br><br> <br><br>
<button type="button" id="copy-support" class="btn btn-info d-none" onclick="copyToClipboard(); return false;">Copy To Clipboard</button> <button type="button" id="copy-support" class="btn btn-info mb-3 d-none" onclick="copyToClipboard(); return false;">Copy To Clipboard</button>
<div class="toast-container position-absolute float-start" style="width: 15rem;">
<div id="toastClipboardCopy" class="toast fade hide" role="status" aria-live="polite" aria-atomic="true" data-bs-autohide="true" data-bs-delay="1500">
<div class="toast-body">
Copied to clipboard!
</div>
</div>
</div>
</dt> </dt>
<dd class="col-sm-9"> <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> <pre id="support-string" class="pre-scrollable d-none w-100 border p-2" style="height: 16rem;"></pre>
</dd> </dd>
</dl> </dl>
</div> </div>
@ -185,10 +201,13 @@
</main> </main>
<script> <script>
dnsCheck = false; 'use strict';
timeCheck = false;
domainCheck = false; var dnsCheck = false;
httpsCheck = false; var timeCheck = false;
var domainCheck = false;
var httpsCheck = false;
(() => { (() => {
// ================================ // ================================
// Date & Time Check // Date & Time Check
@ -203,7 +222,10 @@
document.getElementById("time-browser-string").innerText = browserUTC; document.getElementById("time-browser-string").innerText = browserUTC;
const serverUTC = document.getElementById("time-server-string").innerText; const serverUTC = document.getElementById("time-server-string").innerText;
const timeDrift = (Date.parse(serverUTC) - Date.parse(browserUTC)) / 1000; const timeDrift = (
Date.parse(serverUTC.replace(' ', 'T').replace(' UTC', '')) -
Date.parse(browserUTC.replace(' ', 'T').replace(' UTC', ''))
) / 1000;
if (timeDrift > 30 || timeDrift < -30) { if (timeDrift > 30 || timeDrift < -30) {
document.getElementById('time-warning').classList.remove('d-none'); document.getElementById('time-warning').classList.remove('d-none');
} else { } else {
@ -233,7 +255,7 @@
const webInstalled = document.getElementById('web-installed').innerText; const webInstalled = document.getElementById('web-installed').innerText;
checkVersions('server', serverInstalled, serverLatest, serverLatestCommit); checkVersions('server', serverInstalled, serverLatest, serverLatestCommit);
{{#unless diagnostics.running_within_docker}} {{#unless page_data.running_within_docker}}
const webLatest = document.getElementById('web-latest').innerText; const webLatest = document.getElementById('web-latest').innerText;
checkVersions('web', webInstalled, webLatest); checkVersions('web', webInstalled, webLatest);
{{/unless}} {{/unless}}
@ -303,30 +325,38 @@
// ================================ // ================================
// Generate support string to be pasted on github or the forum // Generate support string to be pasted on github or the forum
async function generateSupportString() { async function generateSupportString() {
supportString = "### Your environment (Generated via diagnostics page)\n"; let supportString = "### Your environment (Generated via diagnostics page)\n";
supportString += "* Vaultwarden version: v{{ version }}\n"; supportString += "* Vaultwarden version: v{{ version }}\n";
supportString += "* Web-vault version: v{{ diagnostics.web_vault_version }}\n"; supportString += "* Web-vault version: v{{ page_data.web_vault_version }}\n";
supportString += "* Running within Docker: {{ diagnostics.running_within_docker }}\n"; supportString += "* Running within Docker: {{ page_data.running_within_docker }}\n";
supportString += "* Uses a reverse proxy: {{ diagnostics.ip_header_exists }}\n"; supportString += "* Environment settings overridden: ";
{{#if diagnostics.ip_header_exists}} {{#if page_data.overrides}}
supportString += "* IP Header check: {{ diagnostics.ip_header_match }} ({{ diagnostics.ip_header_name }})\n"; supportString += "true\n"
{{else}}
supportString += "false\n"
{{/if}}
supportString += "* Uses a reverse proxy: {{ page_data.ip_header_exists }}\n";
{{#if page_data.ip_header_exists}}
supportString += "* IP Header check: {{ page_data.ip_header_match }} ({{ page_data.ip_header_name }})\n";
{{/if}} {{/if}}
supportString += "* Internet access: {{ diagnostics.has_http_access }}\n"; supportString += "* Internet access: {{ page_data.has_http_access }}\n";
supportString += "* Internet access via a proxy: {{ diagnostics.uses_proxy }}\n"; supportString += "* Internet access via a proxy: {{ page_data.uses_proxy }}\n";
supportString += "* DNS Check: " + dnsCheck + "\n"; supportString += "* DNS Check: " + dnsCheck + "\n";
supportString += "* Time Check: " + timeCheck + "\n"; supportString += "* Time Check: " + timeCheck + "\n";
supportString += "* Domain Configuration Check: " + domainCheck + "\n"; supportString += "* Domain Configuration Check: " + domainCheck + "\n";
supportString += "* HTTPS Check: " + httpsCheck + "\n"; supportString += "* HTTPS Check: " + httpsCheck + "\n";
supportString += "* Database type: {{ diagnostics.db_type }}\n"; supportString += "* Database type: {{ page_data.db_type }}\n";
supportString += "* Database version: {{ diagnostics.db_version }}\n"; supportString += "* Database version: {{ page_data.db_version }}\n";
supportString += "* Clients used: \n"; supportString += "* Clients used: \n";
supportString += "* Reverse proxy and version: \n"; supportString += "* Reverse proxy and version: \n";
supportString += "* Other relevant information: \n"; supportString += "* Other relevant information: \n";
jsonResponse = await fetch('{{urlpath}}/admin/diagnostics/config'); let jsonResponse = await fetch('{{urlpath}}/admin/diagnostics/config');
configJson = await jsonResponse.json(); const configJson = await jsonResponse.json();
supportString += "\n### Config (Generated via diagnostics page)\n```json\n" + JSON.stringify(configJson, undefined, 2) + "\n```\n"; supportString += "\n### Config (Generated via diagnostics page)\n<details><summary>Show Running Config</summary>\n"
supportString += "\n**Environment settings which are overridden:** {{page_data.overrides}}\n"
supportString += "\n\n```json\n" + JSON.stringify(configJson, undefined, 2) + "\n```\n</details>\n";
document.getElementById('support-string').innerText = supportString; document.getElementById('support-string').innerText = supportString;
document.getElementById('support-string').classList.remove('d-none'); document.getElementById('support-string').classList.remove('d-none');
@ -334,16 +364,19 @@
} }
function copyToClipboard() { function copyToClipboard() {
const str = document.getElementById('support-string').innerText; const supportStr = document.getElementById('support-string').innerText;
const el = document.createElement('textarea'); const tmpCopyEl = document.createElement('textarea');
el.value = str;
el.setAttribute('readonly', ''); tmpCopyEl.setAttribute('id', 'copy-support-string');
el.style.position = 'absolute'; tmpCopyEl.setAttribute('readonly', '');
el.style.left = '-9999px'; tmpCopyEl.value = supportStr;
document.body.appendChild(el); tmpCopyEl.style.position = 'absolute';
el.select(); tmpCopyEl.style.left = '-9999px';
document.body.appendChild(tmpCopyEl);
tmpCopyEl.select();
document.execCommand('copy'); document.execCommand('copy');
document.body.removeChild(el); tmpCopyEl.remove();
}
new BSN.Toast('#toastClipboardCopy').show();
}
</script> </script>

@ -1,7 +1,6 @@
<main class="container-xl"> <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>
<div class="table-responsive-xl small"> <div class="table-responsive-xl small">
<table id="orgs-table" class="table table-sm table-striped table-hover"> <table id="orgs-table" class="table table-sm table-striped table-hover">
<thead> <thead>
@ -10,19 +9,19 @@
<th>Users</th> <th>Users</th>
<th>Items</th> <th>Items</th>
<th>Attachments</th> <th>Attachments</th>
<th style="width: 120px; min-width: 120px;">Actions</th> <th style="width: 130px; min-width: 130px;">Actions</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{{#each organizations}} {{#each page_data}}
<tr> <tr>
<td> <td>
<img class="mr-2 float-left rounded identicon" data-src="{{Id}}"> <img class="float-start me-2 rounded identicon" data-src="{{Id}}">
<div class="float-left"> <div class="float-start">
<strong>{{Name}}</strong> <strong>{{Name}}</strong>
<span class="mr-2">({{BillingEmail}})</span> <span class="me-2">({{BillingEmail}})</span>
<span class="d-block"> <span class="d-block">
<span class="badge badge-success">{{Id}}</span> <span class="badge bg-success">{{Id}}</span>
</span> </span>
</div> </div>
</td> </td>
@ -38,7 +37,7 @@
<span class="d-block"><strong>Size:</strong> {{attachment_size}}</span> <span class="d-block"><strong>Size:</strong> {{attachment_size}}</span>
{{/if}} {{/if}}
</td> </td>
<td style="font-size: 90%; text-align: right; padding-right: 15px"> <td class="text-end pe-2 small">
<a class="d-block" href="#" onclick='deleteOrganization({{jsesc Id}}, {{jsesc Name}}, {{jsesc BillingEmail}})'>Delete Organization</a> <a class="d-block" href="#" onclick='deleteOrganization({{jsesc Id}}, {{jsesc Name}}, {{jsesc BillingEmail}})'>Delete Organization</a>
</td> </td>
</tr> </tr>
@ -46,14 +45,15 @@
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>
</main> </main>
<link rel="stylesheet" href="{{urlpath}}/bwrs_static/datatables.css" /> <link rel="stylesheet" href="{{urlpath}}/bwrs_static/datatables.css" />
<script src="{{urlpath}}/bwrs_static/jquery-3.5.1.slim.js"></script> <script src="{{urlpath}}/bwrs_static/jquery-3.6.0.slim.js"></script>
<script src="{{urlpath}}/bwrs_static/datatables.js"></script> <script src="{{urlpath}}/bwrs_static/datatables.js"></script>
<script> <script>
'use strict';
function deleteOrganization(id, name, billing_email) { function deleteOrganization(id, name, billing_email) {
// First make sure the user wants to delete this organization // First make sure the user wants to delete this organization
var continueDelete = confirm("WARNING: All data of this organization ("+ name +") will be lost!\nMake sure you have a backup, this cannot be undone!"); var continueDelete = confirm("WARNING: All data of this organization ("+ name +") will be lost!\nMake sure you have a backup, this cannot be undone!");
@ -79,7 +79,7 @@
} }
})(); })();
document.addEventListener("DOMContentLoaded", function(event) { document.addEventListener("DOMContentLoaded", function() {
$('#orgs-table').DataTable({ $('#orgs-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] ],

@ -3,34 +3,32 @@
<div> <div>
<h6 class="text-white mb-3">Configuration</h6> <h6 class="text-white mb-3">Configuration</h6>
<div class="small text-white mb-3"> <div class="small text-white mb-3">
NOTE: The settings here override the environment variables. Once saved, it's recommended to stop setting <span class="font-weight-bolder">NOTE:</span> The settings here override the environment variables. Once saved, it's recommended to stop setting them to avoid confusion.<br>
them to avoid confusion. This does not apply to the read-only section, which can only be set through the This does not apply to the read-only section, which can only be set via environment variables.<br>
environment. Settings which are overridden are shown with <span class="is-overridden-true">double underscores</span>.
</div> </div>
<form class="form accordion" id="config-form" onsubmit="saveConfig(); return false;"> <form class="form needs-validation" id="config-form" onsubmit="saveConfig(); return false;" novalidate>
{{#each config}} {{#each config}}
{{#if groupdoc}} {{#if groupdoc}}
<div class="card bg-light mb-3"> <div class="card bg-light mb-3">
<div class="card-header"><button type="button" class="btn btn-link collapsed" data-toggle="collapse" <div class="card-header" role="button" data-bs-toggle="collapse" data-bs-target="#g_{{group}}">
data-target="#g_{{group}}">{{groupdoc}}</button></div> <button type="button" class="btn btn-link text-decoration-none collapsed" data-bs-toggle="collapse" data-bs-target="#g_{{group}}">{{groupdoc}}</button>
<div id="g_{{group}}" class="card-body collapse" data-parent="#config-form"> </div>
<div id="g_{{group}}" class="card-body collapse">
{{#each elements}} {{#each elements}}
{{#if editable}} {{#if editable}}
<div class="form-group row align-items-center" title="[{{name}}] {{doc.description}}"> <div class="row my-2 align-items-center is-overridden-{{overridden}}" title="[{{name}}] {{doc.description}}">
{{#case type "text" "number" "password"}} {{#case type "text" "number" "password"}}
<label for="input_{{name}}" class="col-sm-3 col-form-label">{{doc.name}}</label> <label for="input_{{name}}" class="col-sm-3 col-form-label">{{doc.name}}</label>
<div class="col-sm-8 input-group"> <div class="col-sm-8">
<div class="input-group">
<input class="form-control conf-{{type}}" id="input_{{name}}" type="{{type}}" <input class="form-control conf-{{type}}" id="input_{{name}}" type="{{type}}"
name="{{name}}" value="{{value}}" {{#if default}} placeholder="Default: {{default}}" name="{{name}}" value="{{value}}" {{#if default}} placeholder="Default: {{default}}"{{/if}}>
{{/if}}>
{{#case type "password"}} {{#case type "password"}}
<div class="input-group-append"> <button class="btn btn-outline-secondary input-group-text" type="button" onclick="toggleVis('input_{{name}}');">Show/hide</button>
<button class="btn btn-outline-secondary" type="button"
onclick="toggleVis('input_{{name}}');">Show/hide</button>
</div>
{{/case}} {{/case}}
</div>
</div> </div>
{{/case}} {{/case}}
{{#case type "checkbox"}} {{#case type "checkbox"}}
@ -48,13 +46,12 @@
{{/if}} {{/if}}
{{/each}} {{/each}}
{{#case group "smtp"}} {{#case group "smtp"}}
<div class="form-group row align-items-center pt-3 border-top" title="Send a test email to given email address"> <div class="row my-2 align-items-center pt-3 border-top" title="Send a test email to given email address">
<label for="smtp-test-email" class="col-sm-3 col-form-label">Test SMTP</label> <label for="smtp-test-email" class="col-sm-3 col-form-label">Test SMTP</label>
<div class="col-sm-8 input-group"> <div class="col-sm-8 input-group">
<input class="form-control" id="smtp-test-email" type="email" placeholder="Enter test email"> <input class="form-control" id="smtp-test-email" type="email" placeholder="Enter test email" required>
<div class="input-group-append"> <button type="button" class="btn btn-outline-primary input-group-text" onclick="smtpTest(); return false;">Send test email</button>
<button type="button" class="btn btn-outline-primary" onclick="smtpTest(); return false;">Send test email</button> <div class="invalid-tooltip">Please provide a valid email address</div>
</div>
</div> </div>
</div> </div>
{{/case}} {{/case}}
@ -64,9 +61,11 @@
{{/each}} {{/each}}
<div class="card bg-light mb-3"> <div class="card bg-light mb-3">
<div class="card-header"><button type="button" class="btn btn-link collapsed" data-toggle="collapse" <div class="card-header" role="button" data-bs-toggle="collapse" data-bs-target="#g_readonly">
data-target="#g_readonly">Read-Only Config</button></div> <button type="button" class="btn btn-link text-decoration-none collapsed" data-bs-toggle="collapse" data-bs-target="#g_readonly">Read-Only Config</button>
<div id="g_readonly" class="card-body collapse" data-parent="#config-form"> </div>
<div id="g_readonly" class="card-body collapse">
<div class="small mb-3"> <div class="small mb-3">
NOTE: These options can't be modified in the editor because they would require the server NOTE: These options can't be modified in the editor because they would require the server
to be restarted. To modify them, you need to set the correct environment variables when to be restarted. To modify them, you need to set the correct environment variables when
@ -76,19 +75,17 @@
{{#each config}} {{#each config}}
{{#each elements}} {{#each elements}}
{{#unless editable}} {{#unless editable}}
<div class="form-group row align-items-center" title="[{{name}}] {{doc.description}}"> <div class="row my-2 align-items-center" title="[{{name}}] {{doc.description}}">
{{#case type "text" "number" "password"}} {{#case type "text" "number" "password"}}
<label for="input_{{name}}" class="col-sm-3 col-form-label">{{doc.name}}</label> <label for="input_{{name}}" class="col-sm-3 col-form-label">{{doc.name}}</label>
<div class="col-sm-8 input-group"> <div class="col-sm-8">
<div class="input-group">
<input readonly class="form-control" id="input_{{name}}" type="{{type}}" <input readonly class="form-control" id="input_{{name}}" type="{{type}}"
value="{{value}}" {{#if default}} placeholder="Default: {{default}}" {{/if}}> value="{{value}}" {{#if default}} placeholder="Default: {{default}}" {{/if}}>
{{#case type "password"}} {{#case type "password"}}
<div class="input-group-append"> <button class="btn btn-outline-secondary" type="button" onclick="toggleVis('input_{{name}}');">Show/hide</button>
<button class="btn btn-outline-secondary" type="button"
onclick="toggleVis('input_{{name}}');">Show/hide</button>
</div>
{{/case}} {{/case}}
</div>
</div> </div>
{{/case}} {{/case}}
{{#case type "checkbox"}} {{#case type "checkbox"}}
@ -112,9 +109,10 @@
{{#if can_backup}} {{#if can_backup}}
<div class="card bg-light mb-3"> <div class="card bg-light mb-3">
<div class="card-header"><button type="button" class="btn btn-link collapsed" data-toggle="collapse" <div class="card-header" role="button" data-bs-toggle="collapse" data-bs-target="#g_database">
data-target="#g_database">Backup Database</button></div> <button type="button" class="btn btn-link text-decoration-none collapsed" data-bs-toggle="collapse" data-bs-target="#g_database">Backup Database</button>
<div id="g_database" class="card-body collapse" data-parent="#config-form"> </div>
<div id="g_database" class="card-body collapse">
<div class="small mb-3"> <div class="small mb-3">
WARNING: This function only creates a backup copy of the SQLite database. WARNING: This function only creates a backup copy of the SQLite database.
This does not include any configuration or file attachment data that may This does not include any configuration or file attachment data that may
@ -128,7 +126,7 @@
{{/if}} {{/if}}
<button type="submit" class="btn btn-primary">Save</button> <button type="submit" class="btn btn-primary">Save</button>
<button type="button" class="btn btn-danger float-right" onclick="deleteConf();">Reset defaults</button> <button type="button" class="btn btn-danger float-end" onclick="deleteConf();">Reset defaults</button>
</form> </form>
</div> </div>
</div> </div>
@ -139,16 +137,34 @@
/* Most modern browsers support this now. */ /* Most modern browsers support this now. */
color: orangered; color: orangered;
} }
.is-overridden-true {
text-decoration: underline double;
}
</style> </style>
<script> <script>
'use strict';
function smtpTest() { function smtpTest() {
if (formHasChanges(config_form)) { if (formHasChanges(config_form)) {
event.preventDefault();
event.stopPropagation();
alert("Config has been changed but not yet saved.\nPlease save the changes first before sending a test email."); alert("Config has been changed but not yet saved.\nPlease save the changes first before sending a test email.");
return false; return false;
} }
test_email = document.getElementById("smtp-test-email");
data = JSON.stringify({ "email": test_email.value }); let test_email = document.getElementById("smtp-test-email");
// Do a very very basic email address check.
if (test_email.value.match(/\S+@\S+/i) === null) {
test_email.parentElement.classList.add('was-validated');
event.preventDefault();
event.stopPropagation();
return false;
}
const data = JSON.stringify({ "email": test_email.value });
_post("{{urlpath}}/admin/test/smtp/", _post("{{urlpath}}/admin/test/smtp/",
"SMTP Test email sent correctly", "SMTP Test email sent correctly",
"Error sending SMTP test email", data, false); "Error sending SMTP test email", data, false);
@ -157,21 +173,21 @@
function getFormData() { function getFormData() {
let data = {}; let data = {};
document.querySelectorAll(".conf-checkbox").forEach(function (e, i) { document.querySelectorAll(".conf-checkbox").forEach(function (e) {
data[e.name] = e.checked; data[e.name] = e.checked;
}); });
document.querySelectorAll(".conf-number").forEach(function (e, i) { document.querySelectorAll(".conf-number").forEach(function (e) {
data[e.name] = e.value ? +e.value : null; data[e.name] = e.value ? +e.value : null;
}); });
document.querySelectorAll(".conf-text, .conf-password").forEach(function (e, i) { document.querySelectorAll(".conf-text, .conf-password").forEach(function (e) {
data[e.name] = e.value || null; data[e.name] = e.value || null;
}); });
return data; return data;
} }
function saveConfig() { function saveConfig() {
data = JSON.stringify(getFormData()); const data = JSON.stringify(getFormData());
_post("{{urlpath}}/admin/config/", "Config saved correctly", _post("{{urlpath}}/admin/config/", "Config saved correctly",
"Error saving config", data); "Error saving config", data);
return false; return false;
@ -198,10 +214,10 @@
function masterCheck(check_id, inputs_query) { function masterCheck(check_id, inputs_query) {
function onChanged(checkbox, inputs_query) { function onChanged(checkbox, inputs_query) {
return function _fn() { return function _fn() {
document.querySelectorAll(inputs_query).forEach(function (e, i) { e.disabled = !checkbox.checked; }); document.querySelectorAll(inputs_query).forEach(function (e) { e.disabled = !checkbox.checked; });
checkbox.disabled = false; checkbox.disabled = false;
}; };
}; }
const checkbox = document.getElementById(check_id); const checkbox = document.getElementById(check_id);
const onChange = onChanged(checkbox, inputs_query); const onChange = onChanged(checkbox, inputs_query);
@ -238,7 +254,6 @@
Array.from(risk_el).forEach((el) => { Array.from(risk_el).forEach((el) => {
if (el.innerText.toLowerCase().includes('risks') ) { if (el.innerText.toLowerCase().includes('risks') ) {
el.parentElement.className += ' alert-danger' el.parentElement.className += ' alert-danger'
console.log(el)
} }
}); });
} }

@ -7,34 +7,34 @@
<thead> <thead>
<tr> <tr>
<th>User</th> <th>User</th>
<th style="width:65px; min-width: 65px;">Created at</th> <th style="width: 85px; min-width: 70px;">Created at</th>
<th style="width:70px; min-width: 65px;">Last Active</th> <th style="width: 85px; min-width: 70px;">Last Active</th>
<th style="width:35px; min-width: 35px;">Items</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: 120px; min-width: 120px;">Actions</th> <th style="width: 130px; min-width: 130px;">Actions</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{{#each users}} {{#each page_data}}
<tr> <tr>
<td> <td>
<img class="float-left mr-2 rounded identicon" data-src="{{Email}}"> <img class="float-start me-2 rounded identicon" data-src="{{Email}}">
<div class="float-left"> <div class="float-start">
<strong>{{Name}}</strong> <strong>{{Name}}</strong>
<span class="d-block">{{Email}}</span> <span class="d-block">{{Email}}</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 bg-danger me-2" title="User is disabled">Disabled</span>
{{/unless}} {{/unless}}
{{#if TwoFactorEnabled}} {{#if TwoFactorEnabled}}
<span class="badge badge-success mr-2" title="2FA is enabled">2FA</span> <span class="badge bg-success me-2" title="2FA is enabled">2FA</span>
{{/if}} {{/if}}
{{#case _Status 1}} {{#case _Status 1}}
<span class="badge badge-warning mr-2" title="User is invited">Invited</span> <span class="badge bg-warning me-2" title="User is invited">Invited</span>
{{/case}} {{/case}}
{{#if EmailVerified}} {{#if EmailVerified}}
<span class="badge badge-success mr-2" title="Email has been verified">Verified</span> <span class="badge bg-success me-2" title="Email has been verified">Verified</span>
{{/if}} {{/if}}
</span> </span>
</div> </div>
@ -57,11 +57,11 @@
<td> <td>
<div class="overflow-auto" style="max-height: 120px;"> <div class="overflow-auto" style="max-height: 120px;">
{{#each Organizations}} {{#each Organizations}}
<button class="badge badge-primary" data-toggle="modal" data-target="#userOrgTypeDialog" data-orgtype="{{Type}}" data-orguuid="{{jsesc Id no_quote}}" data-orgname="{{jsesc Name no_quote}}" data-useremail="{{jsesc ../Email no_quote}}" data-useruuid="{{jsesc ../Id no_quote}}">{{Name}}</button> <button class="badge" data-bs-toggle="modal" data-bs-target="#userOrgTypeDialog" data-orgtype="{{Type}}" data-orguuid="{{jsesc Id no_quote}}" data-orgname="{{jsesc Name no_quote}}" data-useremail="{{jsesc ../Email no_quote}}" data-useruuid="{{jsesc ../Id no_quote}}">{{Name}}</button>
{{/each}} {{/each}}
</div> </div>
</td> </td>
<td style="font-size: 90%; text-align: right; padding-right: 15px"> <td class="text-end pe-2 small">
{{#if TwoFactorEnabled}} {{#if TwoFactorEnabled}}
<a class="d-block" href="#" onclick='remove2fa({{jsesc Id}})'>Remove all 2FA</a> <a class="d-block" href="#" onclick='remove2fa({{jsesc Id}})'>Remove all 2FA</a>
{{/if}} {{/if}}
@ -85,7 +85,7 @@
Force clients to resync Force clients to resync
</button> </button>
<button type="button" class="btn btn-sm btn-primary float-right" onclick="reload();">Reload users</button> <button type="button" class="btn btn-sm btn-primary float-end" onclick="reload();">Reload users</button>
</div> </div>
</div> </div>
@ -94,8 +94,8 @@
<h6 class="mb-0 text-white">Invite User</h6> <h6 class="mb-0 text-white">Invite User</h6>
<small>Email:</small> <small>Email:</small>
<form class="form-inline" id="invite-form" onsubmit="inviteUser(); return false;"> <form class="form-inline input-group w-50" id="invite-form" onsubmit="inviteUser(); return false;">
<input type="email" class="form-control w-50 mr-2" id="email-invite" placeholder="Enter email"> <input type="email" class="form-control me-2" id="email-invite" placeholder="Enter email" required>
<button type="submit" class="btn btn-primary">Invite</button> <button type="submit" class="btn btn-primary">Invite</button>
</form> </form>
</div> </div>
@ -106,9 +106,7 @@
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h6 class="modal-title" id="userOrgTypeDialogTitle"></h6> <h6 class="modal-title" id="userOrgTypeDialogTitle"></h6>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
<span aria-hidden="true">&times;</span>
</button>
</div> </div>
<form class="form" id="userOrgTypeForm" onsubmit="updateUserOrgType(); return false;"> <form class="form" id="userOrgTypeForm" onsubmit="updateUserOrgType(); return false;">
<input type="hidden" name="user_uuid" id="userOrgTypeUserUuid" value=""> <input type="hidden" name="user_uuid" id="userOrgTypeUserUuid" value="">
@ -128,7 +126,7 @@
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-sm btn-secondary" data-dismiss="modal">Cancel</button> <button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-sm btn-primary">Change Role</button> <button type="submit" class="btn btn-sm btn-primary">Change Role</button>
</div> </div>
</form> </form>
@ -138,9 +136,11 @@
</main> </main>
<link rel="stylesheet" href="{{urlpath}}/bwrs_static/datatables.css" /> <link rel="stylesheet" href="{{urlpath}}/bwrs_static/datatables.css" />
<script src="{{urlpath}}/bwrs_static/jquery-3.5.1.slim.js"></script> <script src="{{urlpath}}/bwrs_static/jquery-3.6.0.slim.js"></script>
<script src="{{urlpath}}/bwrs_static/datatables.js"></script> <script src="{{urlpath}}/bwrs_static/datatables.js"></script>
<script> <script>
'use strict';
function deleteUser(id, mail) { function deleteUser(id, mail) {
var input_mail = prompt("To delete user '" + mail + "', please type the email below") var input_mail = prompt("To delete user '" + mail + "', please type the email below")
if (input_mail != null) { if (input_mail != null) {
@ -191,8 +191,8 @@
return false; return false;
} }
function inviteUser() { function inviteUser() {
inv = document.getElementById("email-invite"); const inv = document.getElementById("email-invite");
data = JSON.stringify({ "email": inv.value }); const data = JSON.stringify({ "email": inv.value });
inv.value = ""; inv.value = "";
_post("{{urlpath}}/admin/invite/", "User invited correctly", _post("{{urlpath}}/admin/invite/", "User invited correctly",
"Error inviting user", data); "Error inviting user", data);
@ -212,7 +212,7 @@
} }
})(); })();
document.querySelectorAll("[data-orgtype]").forEach(function (e, i) { document.querySelectorAll("[data-orgtype]").forEach(function (e) {
let orgtype = OrgTypes[e.dataset.orgtype]; let orgtype = OrgTypes[e.dataset.orgtype];
e.style.backgroundColor = orgtype.color; e.style.backgroundColor = orgtype.color;
e.title = orgtype.name; e.title = orgtype.name;
@ -225,7 +225,7 @@
let sortDate = a.replace(/(<([^>]+)>)/gi, "").trim(); let sortDate = a.replace(/(<([^>]+)>)/gi, "").trim();
if ( sortDate !== '' ) { if ( sortDate !== '' ) {
let dtParts = sortDate.split(' '); let dtParts = sortDate.split(' ');
var timeParts = (undefined != dtParts[1]) ? dtParts[1].split(':') : [00,00,00]; var timeParts = (undefined != dtParts[1]) ? dtParts[1].split(':') : ['00','00','00'];
var dateParts = dtParts[0].split('-'); var dateParts = dtParts[0].split('-');
x = (dateParts[0] + dateParts[1] + dateParts[2] + timeParts[0] + timeParts[1] + ((undefined != timeParts[2]) ? timeParts[2] : 0)) * 1; x = (dateParts[0] + dateParts[1] + dateParts[2] + timeParts[0] + timeParts[1] + ((undefined != timeParts[2]) ? timeParts[2] : 0)) * 1;
if ( isNaN(x) ) { if ( isNaN(x) ) {
@ -246,7 +246,7 @@
} }
}); });
document.addEventListener("DOMContentLoaded", function(event) { document.addEventListener("DOMContentLoaded", function() {
$('#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] ],
@ -275,7 +275,7 @@
}, false); }, false);
// Prevent accidental submission of the form with valid elements after the modal has been hidden. // Prevent accidental submission of the form with valid elements after the modal has been hidden.
userOrgTypeDialog.addEventListener('hide.bs.modal', function(event){ userOrgTypeDialog.addEventListener('hide.bs.modal', function(){
document.getElementById("userOrgTypeDialogTitle").innerHTML = ''; document.getElementById("userOrgTypeDialogTitle").innerHTML = '';
document.getElementById("userOrgTypeUserUuid").value = ''; document.getElementById("userOrgTypeUserUuid").value = '';
document.getElementById("userOrgTypeOrgUuid").value = ''; document.getElementById("userOrgTypeOrgUuid").value = '';

Loading…
Cancel
Save