From 3a44dc963b3df563edbed3cfcdb8d6b931b87e3e Mon Sep 17 00:00:00 2001 From: Mathijs van Veluw Date: Mon, 26 May 2025 20:37:50 +0200 Subject: [PATCH] Update admin interface (#5880) - Updated Backend Admin dependencies - Fixed NTP time by using CloudFlare trace - Fixes #5797 - Fixed web-vault version check = Fixes #5761 - Fixed an issue with the css not hiding the 'Create Account' link. There were no braces around the function call. Also added a hide for newer web-vault versions as it still causes confusion with the cached /api/config. Signed-off-by: BlackDex --- src/api/admin.rs | 54 ++- src/api/core/accounts.rs | 5 +- src/api/core/ciphers.rs | 2 +- src/static/scripts/admin.css | 4 +- src/static/scripts/admin_diagnostics.js | 17 +- src/static/scripts/bootstrap.bundle.js | 14 +- src/static/scripts/bootstrap.css | 45 +- src/static/scripts/datatables.css | 121 ++++- src/static/scripts/datatables.js | 426 +++++++++++++----- src/static/templates/admin/diagnostics.hbs | 15 +- src/static/templates/admin/organizations.hbs | 2 +- src/static/templates/admin/users.hbs | 2 +- .../templates/scss/vaultwarden.scss.hbs | 8 +- 13 files changed, 510 insertions(+), 205 deletions(-) diff --git a/src/api/admin.rs b/src/api/admin.rs index 505765d6..f840fc6f 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -591,20 +591,14 @@ struct GitCommit { sha: String, } -#[derive(Deserialize)] -struct TimeApi { - year: u16, - month: u8, - day: u8, - hour: u8, - minute: u8, - seconds: u8, -} - async fn get_json_api(url: &str) -> Result { Ok(make_http_request(Method::GET, url)?.send().await?.error_for_status()?.json::().await?) } +async fn get_text_api(url: &str) -> Result { + Ok(make_http_request(Method::GET, url)?.send().await?.error_for_status()?.text().await?) +} + async fn has_http_access() -> bool { let Ok(req) = make_http_request(Method::HEAD, "https://github.com/dani-garcia/vaultwarden") else { return false; @@ -616,9 +610,10 @@ async fn has_http_access() -> bool { } use cached::proc_macro::cached; -/// Cache this function to prevent API call rate limit. Github only allows 60 requests per hour, and we use 3 here already. -/// It will cache this function for 300 seconds (5 minutes) which should prevent the exhaustion of the rate limit. -#[cached(time = 300, sync_writes = "default")] +/// Cache this function to prevent API call rate limit. Github only allows 60 requests per hour, and we use 3 here already +/// It will cache this function for 600 seconds (10 minutes) which should prevent the exhaustion of the rate limit +/// Any cache will be lost if Vaultwarden is restarted +#[cached(time = 600, sync_writes = "default")] async fn get_release_info(has_http_access: bool, running_within_container: bool) -> (String, String, String) { // If the HTTP Check failed, do not even attempt to check for new versions since we were not able to connect with github.com anyway. if has_http_access { @@ -636,7 +631,7 @@ async fn get_release_info(has_http_access: bool, running_within_container: bool) } _ => "-".to_string(), }, - // Do not fetch the web-vault version when running within a container. + // Do not fetch the web-vault version when running within a container // The web-vault version is embedded within the container it self, and should not be updated manually if running_within_container { "-".to_string() @@ -658,17 +653,18 @@ async fn get_release_info(has_http_access: bool, running_within_container: bool) async fn get_ntp_time(has_http_access: bool) -> String { if has_http_access { - if let Ok(ntp_time) = get_json_api::("https://www.timeapi.io/api/Time/current/zone?timeZone=UTC").await - { - return format!( - "{year}-{month:02}-{day:02} {hour:02}:{minute:02}:{seconds:02} UTC", - year = ntp_time.year, - month = ntp_time.month, - day = ntp_time.day, - hour = ntp_time.hour, - minute = ntp_time.minute, - seconds = ntp_time.seconds - ); + if let Ok(cf_trace) = get_text_api("https://cloudflare.com/cdn-cgi/trace").await { + for line in cf_trace.lines() { + if let Some((key, value)) = line.split_once('=') { + if key == "ts" { + let ts = value.split_once('.').map_or(value, |(s, _)| s); + if let Ok(dt) = chrono::DateTime::parse_from_str(ts, "%s") { + return dt.format("%Y-%m-%d %H:%M:%S UTC").to_string(); + } + break; + } + } + } } } String::from("Unable to fetch NTP time.") @@ -701,6 +697,12 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, mut conn: DbConn) // Get current running versions let web_vault_version = get_web_vault_version(); + // Check if the running version is newer than the latest stable released version + let web_ver_match = semver::VersionReq::parse(&format!(">{latest_web_build}")).unwrap(); + let web_vault_pre_release = web_ver_match.matches( + &semver::Version::parse(&web_vault_version).unwrap_or_else(|_| semver::Version::parse("2025.1.1").unwrap()), + ); + let diagnostics_json = json!({ "dns_resolved": dns_resolved, "current_release": VERSION, @@ -709,6 +711,7 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, mut conn: DbConn) "web_vault_enabled": &CONFIG.web_vault_enabled(), "web_vault_version": web_vault_version, "latest_web_build": latest_web_build, + "web_vault_pre_release": web_vault_pre_release, "running_within_container": running_within_container, "container_base_image": if running_within_container { container_base_image() } else { "Not applicable" }, "has_http_access": has_http_access, @@ -724,6 +727,7 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, mut conn: DbConn) "overrides": &CONFIG.get_overrides().join(", "), "host_arch": env::consts::ARCH, "host_os": env::consts::OS, + "tz_env": env::var("TZ").unwrap_or_default(), "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 server date/time check as late as possible to minimize the time difference "ntp_time": get_ntp_time(has_http_access).await, // Run the ntp check as late as possible to minimize the time difference diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index 07947b67..0b2266cc 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -128,9 +128,8 @@ async fn is_email_2fa_required(member_id: Option, conn: &mut DbCon if CONFIG.email_2fa_enforce_on_verified_invite() { return true; } - if member_id.is_some() { - return OrgPolicy::is_enabled_for_member(&member_id.unwrap(), OrgPolicyType::TwoFactorAuthentication, conn) - .await; + if let Some(member_id) = member_id { + return OrgPolicy::is_enabled_for_member(&member_id, OrgPolicyType::TwoFactorAuthentication, conn).await; } false } diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index e954e7f8..ad024076 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -1581,7 +1581,7 @@ async fn move_cipher_selected( nt.send_cipher_update( UpdateType::SyncCipherUpdate, &cipher, - &[user_id.clone()], + std::slice::from_ref(&user_id), &headers.device.uuid, None, &mut conn, diff --git a/src/static/scripts/admin.css b/src/static/scripts/admin.css index ee035ac4..44448d6a 100644 --- a/src/static/scripts/admin.css +++ b/src/static/scripts/admin.css @@ -38,8 +38,8 @@ img { max-width: 130px; } #users-table .vw-actions, #orgs-table .vw-actions { - min-width: 135px; - max-width: 140px; + min-width: 155px; + max-width: 160px; } #users-table .vw-org-cell { max-height: 120px; diff --git a/src/static/scripts/admin_diagnostics.js b/src/static/scripts/admin_diagnostics.js index 566e6a56..07b75d18 100644 --- a/src/static/scripts/admin_diagnostics.js +++ b/src/static/scripts/admin_diagnostics.js @@ -29,7 +29,7 @@ function isValidIp(ip) { return ipv4Regex.test(ip) || ipv6Regex.test(ip); } -function checkVersions(platform, installed, latest, commit=null) { +function checkVersions(platform, installed, latest, commit=null, pre_release=false) { if (installed === "-" || latest === "-") { document.getElementById(`${platform}-failed`).classList.remove("d-none"); return; @@ -37,10 +37,12 @@ function checkVersions(platform, installed, latest, commit=null) { // Only check basic versions, no commit revisions if (commit === null || installed.indexOf("-") === -1) { - if (installed !== latest) { - document.getElementById(`${platform}-warning`).classList.remove("d-none"); - } else { + if (platform === "web" && pre_release === true) { + document.getElementById(`${platform}-prerelease`).classList.remove("d-none"); + } else if (installed == latest) { document.getElementById(`${platform}-success`).classList.remove("d-none"); + } else { + document.getElementById(`${platform}-warning`).classList.remove("d-none"); } } else { // Check if this is a branched version. @@ -86,7 +88,7 @@ async function generateSupportString(event, dj) { supportString += `* Running within a container: ${dj.running_within_container} (Base: ${dj.container_base_image})\n`; supportString += `* Database type: ${dj.db_type}\n`; supportString += `* Database version: ${dj.db_version}\n`; - supportString += `* Environment settings overridden!: ${dj.overrides !== ""}\n`; + supportString += `* Uses config.json: ${dj.overrides !== ""}\n`; supportString += `* Uses a reverse proxy: ${dj.ip_header_exists}\n`; if (dj.ip_header_exists) { supportString += `* IP Header check: ${dj.ip_header_match} (${dj.ip_header_name})\n`; @@ -94,6 +96,9 @@ async function generateSupportString(event, dj) { supportString += `* Internet access: ${dj.has_http_access}\n`; supportString += `* Internet access via a proxy: ${dj.uses_proxy}\n`; supportString += `* DNS Check: ${dnsCheck}\n`; + if (dj.tz_env !== "") { + supportString += `* TZ environment: ${dj.tz_env}\n`; + } supportString += `* Browser/Server Time Check: ${timeCheck}\n`; supportString += `* Server/NTP Time Check: ${ntpTimeCheck}\n`; supportString += `* Domain Configuration Check: ${domainCheck}\n`; @@ -206,7 +211,7 @@ function initVersionCheck(dj) { if (!dj.running_within_container) { const webInstalled = dj.web_vault_version; const webLatest = dj.latest_web_build; - checkVersions("web", webInstalled, webLatest); + checkVersions("web", webInstalled, webLatest, null, dj.web_vault_pre_release); } } diff --git a/src/static/scripts/bootstrap.bundle.js b/src/static/scripts/bootstrap.bundle.js index 4a880107..859e9d2b 100644 --- a/src/static/scripts/bootstrap.bundle.js +++ b/src/static/scripts/bootstrap.bundle.js @@ -1,5 +1,5 @@ /*! - * Bootstrap v5.3.4 (https://getbootstrap.com/) + * Bootstrap v5.3.6 (https://getbootstrap.com/) * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ @@ -647,7 +647,7 @@ * Constants */ - const VERSION = '5.3.4'; + const VERSION = '5.3.6'; /** * Class definition @@ -673,6 +673,8 @@ this[propertyName] = null; } } + + // Private _queueCallback(callback, element, isAnimated = true) { executeAfterTransition(callback, element, isAnimated); } @@ -1604,11 +1606,11 @@ this._element.style[dimension] = ''; this._queueCallback(complete, this._element, true); } + + // Private _isShown(element = this._element) { return element.classList.contains(CLASS_NAME_SHOW$7); } - - // Private _configAfterMerge(config) { config.toggle = Boolean(config.toggle); // Coerce string values config.parent = getElement(config.parent); @@ -3688,6 +3690,9 @@ this._element.setAttribute('aria-expanded', 'false'); Manipulator.removeDataAttribute(this._menu, 'popper'); EventHandler.trigger(this._element, EVENT_HIDDEN$5, relatedTarget); + + // Explicitly return focus to the trigger element + this._element.focus(); } _getConfig(config) { config = super._getConfig(config); @@ -6209,7 +6214,6 @@ } // Private - _maybeScheduleHide() { if (!this._config.autohide) { return; diff --git a/src/static/scripts/bootstrap.css b/src/static/scripts/bootstrap.css index 8855419e..cb819ec2 100644 --- a/src/static/scripts/bootstrap.css +++ b/src/static/scripts/bootstrap.css @@ -1,6 +1,6 @@ @charset "UTF-8"; /*! - * Bootstrap v5.3.4 (https://getbootstrap.com/) + * Bootstrap v5.3.6 (https://getbootstrap.com/) * Copyright 2011-2025 The Bootstrap Authors * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ @@ -2156,10 +2156,6 @@ progress { display: block; padding: 0; } -.form-control::-moz-placeholder { - color: var(--bs-secondary-color); - opacity: 1; -} .form-control::placeholder { color: var(--bs-secondary-color); opacity: 1; @@ -2629,17 +2625,10 @@ textarea.form-control-lg { .form-floating > .form-control-plaintext { padding: 1rem 0.75rem; } -.form-floating > .form-control::-moz-placeholder, .form-floating > .form-control-plaintext::-moz-placeholder { - color: transparent; -} .form-floating > .form-control::placeholder, .form-floating > .form-control-plaintext::placeholder { color: transparent; } -.form-floating > .form-control:not(:-moz-placeholder), .form-floating > .form-control-plaintext:not(:-moz-placeholder) { - padding-top: 1.625rem; - padding-bottom: 0.625rem; -} .form-floating > .form-control:focus, .form-floating > .form-control:not(:placeholder-shown), .form-floating > .form-control-plaintext:focus, .form-floating > .form-control-plaintext:not(:placeholder-shown) { @@ -2656,9 +2645,6 @@ textarea.form-control-lg { padding-bottom: 0.625rem; padding-left: 0.75rem; } -.form-floating > .form-control:not(:-moz-placeholder) ~ label { - transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem); -} .form-floating > .form-control:focus ~ label, .form-floating > .form-control:not(:placeholder-shown) ~ label, .form-floating > .form-control-plaintext ~ label, @@ -2668,15 +2654,6 @@ textarea.form-control-lg { .form-floating > .form-control:-webkit-autofill ~ label { transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem); } -.form-floating > textarea:not(:-moz-placeholder) ~ label::after { - position: absolute; - inset: 1rem 0.375rem; - z-index: -1; - height: 1.5em; - content: ""; - background-color: var(--bs-body-bg); - border-radius: var(--bs-border-radius); -} .form-floating > textarea:focus ~ label::after, .form-floating > textarea:not(:placeholder-shown) ~ label::after { position: absolute; @@ -4540,24 +4517,24 @@ textarea.form-control-lg { border-top-right-radius: 0; border-bottom-right-radius: 0; } - .card-group > .card:not(:last-child) .card-img-top, - .card-group > .card:not(:last-child) .card-header { + .card-group > .card:not(:last-child) > .card-img-top, + .card-group > .card:not(:last-child) > .card-header { border-top-right-radius: 0; } - .card-group > .card:not(:last-child) .card-img-bottom, - .card-group > .card:not(:last-child) .card-footer { + .card-group > .card:not(:last-child) > .card-img-bottom, + .card-group > .card:not(:last-child) > .card-footer { border-bottom-right-radius: 0; } .card-group > .card:not(:first-child) { border-top-left-radius: 0; border-bottom-left-radius: 0; } - .card-group > .card:not(:first-child) .card-img-top, - .card-group > .card:not(:first-child) .card-header { + .card-group > .card:not(:first-child) > .card-img-top, + .card-group > .card:not(:first-child) > .card-header { border-top-left-radius: 0; } - .card-group > .card:not(:first-child) .card-img-bottom, - .card-group > .card:not(:first-child) .card-footer { + .card-group > .card:not(:first-child) > .card-img-bottom, + .card-group > .card:not(:first-child) > .card-footer { border-bottom-left-radius: 0; } } @@ -7179,6 +7156,10 @@ textarea.form-control-lg { .visually-hidden-focusable:not(:focus):not(:focus-within):not(caption) { position: absolute !important; } +.visually-hidden *, +.visually-hidden-focusable:not(:focus):not(:focus-within) * { + overflow: hidden !important; +} .stretched-link::after { position: absolute; diff --git a/src/static/scripts/datatables.css b/src/static/scripts/datatables.css index 690f32c2..06c823f2 100644 --- a/src/static/scripts/datatables.css +++ b/src/static/scripts/datatables.css @@ -4,10 +4,10 @@ * * To rebuild or modify this file with the latest versions of the included * software please visit: - * https://datatables.net/download/#bs5/dt-2.2.2 + * https://datatables.net/download/#bs5/dt-2.3.1 * * Included libraries: - * DataTables 2.2.2 + * DataTables 2.3.1 */ :root { @@ -104,24 +104,14 @@ table.dataTable thead > tr > td.dt-ordering-desc span.dt-column-order:after { content: "\25BC"; content: "\25BC"/""; } -table.dataTable thead > tr > th.dt-orderable-asc, table.dataTable thead > tr > th.dt-orderable-desc, table.dataTable thead > tr > th.dt-ordering-asc, table.dataTable thead > tr > th.dt-ordering-desc, -table.dataTable thead > tr > td.dt-orderable-asc, -table.dataTable thead > tr > td.dt-orderable-desc, -table.dataTable thead > tr > td.dt-ordering-asc, -table.dataTable thead > tr > td.dt-ordering-desc { - position: relative; - padding-right: 30px; -} table.dataTable thead > tr > th.dt-orderable-asc span.dt-column-order, table.dataTable thead > tr > th.dt-orderable-desc span.dt-column-order, table.dataTable thead > tr > th.dt-ordering-asc span.dt-column-order, table.dataTable thead > tr > th.dt-ordering-desc span.dt-column-order, table.dataTable thead > tr > td.dt-orderable-asc span.dt-column-order, table.dataTable thead > tr > td.dt-orderable-desc span.dt-column-order, table.dataTable thead > tr > td.dt-ordering-asc span.dt-column-order, table.dataTable thead > tr > td.dt-ordering-desc span.dt-column-order { - position: absolute; - right: 12px; - top: 0; - bottom: 0; + position: relative; width: 12px; + height: 20px; } table.dataTable thead > tr > th.dt-orderable-asc span.dt-column-order:before, table.dataTable thead > tr > th.dt-orderable-asc span.dt-column-order:after, table.dataTable thead > tr > th.dt-orderable-desc span.dt-column-order:before, table.dataTable thead > tr > th.dt-orderable-desc span.dt-column-order:after, table.dataTable thead > tr > th.dt-ordering-asc span.dt-column-order:before, table.dataTable thead > tr > th.dt-ordering-asc span.dt-column-order:after, table.dataTable thead > tr > th.dt-ordering-desc span.dt-column-order:before, table.dataTable thead > tr > th.dt-ordering-desc span.dt-column-order:after, table.dataTable thead > tr > td.dt-orderable-asc span.dt-column-order:before, @@ -163,6 +153,40 @@ table.dataTable thead > tr > td:active { outline: none; } +table.dataTable thead > tr > th div.dt-column-header, +table.dataTable thead > tr > th div.dt-column-footer, +table.dataTable thead > tr > td div.dt-column-header, +table.dataTable thead > tr > td div.dt-column-footer, +table.dataTable tfoot > tr > th div.dt-column-header, +table.dataTable tfoot > tr > th div.dt-column-footer, +table.dataTable tfoot > tr > td div.dt-column-header, +table.dataTable tfoot > tr > td div.dt-column-footer { + display: flex; + justify-content: space-between; + align-items: center; + gap: 4px; +} +table.dataTable thead > tr > th div.dt-column-header span.dt-column-title, +table.dataTable thead > tr > th div.dt-column-footer span.dt-column-title, +table.dataTable thead > tr > td div.dt-column-header span.dt-column-title, +table.dataTable thead > tr > td div.dt-column-footer span.dt-column-title, +table.dataTable tfoot > tr > th div.dt-column-header span.dt-column-title, +table.dataTable tfoot > tr > th div.dt-column-footer span.dt-column-title, +table.dataTable tfoot > tr > td div.dt-column-header span.dt-column-title, +table.dataTable tfoot > tr > td div.dt-column-footer span.dt-column-title { + flex-grow: 1; +} +table.dataTable thead > tr > th div.dt-column-header span.dt-column-title:empty, +table.dataTable thead > tr > th div.dt-column-footer span.dt-column-title:empty, +table.dataTable thead > tr > td div.dt-column-header span.dt-column-title:empty, +table.dataTable thead > tr > td div.dt-column-footer span.dt-column-title:empty, +table.dataTable tfoot > tr > th div.dt-column-header span.dt-column-title:empty, +table.dataTable tfoot > tr > th div.dt-column-footer span.dt-column-title:empty, +table.dataTable tfoot > tr > td div.dt-column-header span.dt-column-title:empty, +table.dataTable tfoot > tr > td div.dt-column-footer span.dt-column-title:empty { + display: none; +} + div.dt-scroll-body > table.dataTable > thead > tr > th, div.dt-scroll-body > table.dataTable > thead > tr > td { overflow: hidden; @@ -258,10 +282,25 @@ table.dataTable td.dt-type-numeric, table.dataTable td.dt-type-date { text-align: right; } +table.dataTable th.dt-type-numeric div.dt-column-header, +table.dataTable th.dt-type-numeric div.dt-column-footer, table.dataTable th.dt-type-date div.dt-column-header, +table.dataTable th.dt-type-date div.dt-column-footer, +table.dataTable td.dt-type-numeric div.dt-column-header, +table.dataTable td.dt-type-numeric div.dt-column-footer, +table.dataTable td.dt-type-date div.dt-column-header, +table.dataTable td.dt-type-date div.dt-column-footer { + flex-direction: row-reverse; +} table.dataTable th.dt-left, table.dataTable td.dt-left { text-align: left; } +table.dataTable th.dt-left div.dt-column-header, +table.dataTable th.dt-left div.dt-column-footer, +table.dataTable td.dt-left div.dt-column-header, +table.dataTable td.dt-left div.dt-column-footer { + flex-direction: row; +} table.dataTable th.dt-center, table.dataTable td.dt-center { text-align: center; @@ -270,10 +309,22 @@ table.dataTable th.dt-right, table.dataTable td.dt-right { text-align: right; } +table.dataTable th.dt-right div.dt-column-header, +table.dataTable th.dt-right div.dt-column-footer, +table.dataTable td.dt-right div.dt-column-header, +table.dataTable td.dt-right div.dt-column-footer { + flex-direction: row-reverse; +} table.dataTable th.dt-justify, table.dataTable td.dt-justify { text-align: justify; } +table.dataTable th.dt-justify div.dt-column-header, +table.dataTable th.dt-justify div.dt-column-footer, +table.dataTable td.dt-justify div.dt-column-header, +table.dataTable td.dt-justify div.dt-column-footer { + flex-direction: row; +} table.dataTable th.dt-nowrap, table.dataTable td.dt-nowrap { white-space: nowrap; @@ -295,6 +346,16 @@ table.dataTable tfoot th.dt-head-left, table.dataTable tfoot td.dt-head-left { text-align: left; } +table.dataTable thead th.dt-head-left div.dt-column-header, +table.dataTable thead th.dt-head-left div.dt-column-footer, +table.dataTable thead td.dt-head-left div.dt-column-header, +table.dataTable thead td.dt-head-left div.dt-column-footer, +table.dataTable tfoot th.dt-head-left div.dt-column-header, +table.dataTable tfoot th.dt-head-left div.dt-column-footer, +table.dataTable tfoot td.dt-head-left div.dt-column-header, +table.dataTable tfoot td.dt-head-left div.dt-column-footer { + flex-direction: row; +} table.dataTable thead th.dt-head-center, table.dataTable thead td.dt-head-center, table.dataTable tfoot th.dt-head-center, @@ -307,12 +368,32 @@ table.dataTable tfoot th.dt-head-right, table.dataTable tfoot td.dt-head-right { text-align: right; } +table.dataTable thead th.dt-head-right div.dt-column-header, +table.dataTable thead th.dt-head-right div.dt-column-footer, +table.dataTable thead td.dt-head-right div.dt-column-header, +table.dataTable thead td.dt-head-right div.dt-column-footer, +table.dataTable tfoot th.dt-head-right div.dt-column-header, +table.dataTable tfoot th.dt-head-right div.dt-column-footer, +table.dataTable tfoot td.dt-head-right div.dt-column-header, +table.dataTable tfoot td.dt-head-right div.dt-column-footer { + flex-direction: row-reverse; +} table.dataTable thead th.dt-head-justify, table.dataTable thead td.dt-head-justify, table.dataTable tfoot th.dt-head-justify, table.dataTable tfoot td.dt-head-justify { text-align: justify; } +table.dataTable thead th.dt-head-justify div.dt-column-header, +table.dataTable thead th.dt-head-justify div.dt-column-footer, +table.dataTable thead td.dt-head-justify div.dt-column-header, +table.dataTable thead td.dt-head-justify div.dt-column-footer, +table.dataTable tfoot th.dt-head-justify div.dt-column-header, +table.dataTable tfoot th.dt-head-justify div.dt-column-footer, +table.dataTable tfoot td.dt-head-justify div.dt-column-header, +table.dataTable tfoot td.dt-head-justify div.dt-column-footer { + flex-direction: row; +} table.dataTable thead th.dt-head-nowrap, table.dataTable thead td.dt-head-nowrap, table.dataTable tfoot th.dt-head-nowrap, @@ -410,6 +491,9 @@ div.dt-container div.dt-layout-table > div { margin-left: 0; } } +div.dt-container { + position: relative; +} div.dt-container div.dt-length label { font-weight: normal; text-align: left; @@ -498,14 +582,19 @@ table.dataTable.table-sm > thead > tr td.dt-orderable-asc, table.dataTable.table-sm > thead > tr td.dt-orderable-desc, table.dataTable.table-sm > thead > tr td.dt-ordering-asc, table.dataTable.table-sm > thead > tr td.dt-ordering-desc { - padding-right: 20px; + padding-right: 0.25rem; } table.dataTable.table-sm > thead > tr th.dt-orderable-asc span.dt-column-order, table.dataTable.table-sm > thead > tr th.dt-orderable-desc span.dt-column-order, table.dataTable.table-sm > thead > tr th.dt-ordering-asc span.dt-column-order, table.dataTable.table-sm > thead > tr th.dt-ordering-desc span.dt-column-order, table.dataTable.table-sm > thead > tr td.dt-orderable-asc span.dt-column-order, table.dataTable.table-sm > thead > tr td.dt-orderable-desc span.dt-column-order, table.dataTable.table-sm > thead > tr td.dt-ordering-asc span.dt-column-order, table.dataTable.table-sm > thead > tr td.dt-ordering-desc span.dt-column-order { - right: 5px; + right: 0.25rem; +} +table.dataTable.table-sm > thead > tr th.dt-type-date span.dt-column-order, table.dataTable.table-sm > thead > tr th.dt-type-numeric span.dt-column-order, +table.dataTable.table-sm > thead > tr td.dt-type-date span.dt-column-order, +table.dataTable.table-sm > thead > tr td.dt-type-numeric span.dt-column-order { + left: 0.25rem; } div.dt-scroll-head table.table-bordered { diff --git a/src/static/scripts/datatables.js b/src/static/scripts/datatables.js index 3d30940b..368cfb36 100644 --- a/src/static/scripts/datatables.js +++ b/src/static/scripts/datatables.js @@ -4,13 +4,13 @@ * * To rebuild or modify this file with the latest versions of the included * software please visit: - * https://datatables.net/download/#bs5/dt-2.2.2 + * https://datatables.net/download/#bs5/dt-2.3.1 * * Included libraries: - * DataTables 2.2.2 + * DataTables 2.3.1 */ -/*! DataTables 2.2.2 +/*! DataTables 2.3.1 * © SpryMedia Ltd - datatables.net/license */ @@ -101,15 +101,19 @@ var defaults = DataTable.defaults; var $this = $(this); - - /* Sanity check */ + // Sanity check if ( this.nodeName.toLowerCase() != 'table' ) { _fnLog( null, 0, 'Non-table node initialisation ('+this.nodeName+')', 2 ); return; } - $(this).trigger( 'options.dt', oInit ); + // Special case for options + if (oInit.on && oInit.on.options) { + _fnListener($this, 'options', oInit.on.options); + } + + $this.trigger( 'options.dt', oInit ); /* Backwards compatibility for the defaults */ _fnCompatOpts( defaults ); @@ -248,6 +252,9 @@ "caption", "layout", "orderDescReverse", + "orderIndicators", + "orderHandler", + "titleRow", "typeDetect", [ "iCookieDuration", "iStateDuration" ], // backwards compat [ "oSearch", "oPreviousSearch" ], @@ -276,6 +283,13 @@ oSettings.rowIdFn = _fnGetObjectDataFn( oInit.rowId ); + // Add event listeners + if (oInit.on) { + Object.keys(oInit.on).forEach(function (key) { + _fnListener($this, key, oInit.on[key]); + }); + } + /* Browser support detection */ _fnBrowserDetect( oSettings ); @@ -336,7 +350,7 @@ /* HTML5 attribute detection - build an mData object automatically if the * attributes are found */ - var rowOne = $this.children('tbody').find('tr').eq(0); + var rowOne = $this.children('tbody').find('tr:first-child').eq(0); if ( rowOne.length ) { var a = function ( cell, name ) { @@ -494,6 +508,13 @@ * @namespace */ DataTable.ext = _ext = { + /** + * DataTables build type (expanded by the download builder) + * + * @type string + */ + builder: "bs5/dt-2.3.1", + /** * Buttons. For use with the Buttons extension for DataTables. This is * defined here so other extensions can define buttons regardless of load @@ -505,6 +526,14 @@ buttons: {}, + /** + * ColumnControl buttons and content + * + * @type object + */ + ccContent: {}, + + /** * Element class names * @@ -514,14 +543,6 @@ classes: {}, - /** - * DataTables build type (expanded by the download builder) - * - * @type string - */ - builder: "bs5/dt-2.2.2", - - /** * Error reporting. * @@ -1887,6 +1908,26 @@ init.scrollX = init.scrollX ? '100%' : ''; } + // Objects for ordering + if ( typeof init.bSort === 'object' ) { + init.orderIndicators = init.bSort.indicators !== undefined ? init.bSort.indicators : true; + init.orderHandler = init.bSort.handler !== undefined ? init.bSort.handler : true; + init.bSort = true; + } + else if (init.bSort === false) { + init.orderIndicators = false; + init.orderHandler = false; + } + else if (init.bSort === true) { + init.orderIndicators = true; + init.orderHandler = true; + } + + // Which cells are the title cells? + if (typeof init.bSortCellsTop === 'boolean') { + init.titleRow = init.bSortCellsTop; + } + // Column search objects are in an array, so it needs to be converted // element by element var searchCols = init.aoSearchCols; @@ -3264,7 +3305,7 @@ * @param {*} settings DataTables settings * @param {*} source Source layout array * @param {*} incColumns What columns should be included - * @returns Layout array + * @returns Layout array in column index order */ function _fnHeaderLayout( settings, source, incColumns ) { @@ -3548,7 +3589,9 @@ _fnDraw( settings ); - settings._drawHold = false; + settings.api.one('draw', function () { + settings._drawHold = false; + }); } @@ -3560,10 +3603,9 @@ var zero = oLang.sZeroRecords; var dataSrc = _fnDataSource( settings ); - if ( - (settings.iDraw < 1 && dataSrc === 'ssp') || - (settings.iDraw <= 1 && dataSrc === 'ajax') - ) { + // Make use of the fact that settings.json is only set once the initial data has + // been loaded. Show loading when that isn't the case + if ((dataSrc === 'ssp' || dataSrc === 'ajax') && ! settings.json) { zero = oLang.sLoadingRecords; } else if ( oLang.sEmptyTable && settings.fnRecordsTotal() === 0 ) @@ -3933,6 +3975,7 @@ var rows = $(thead).children('tr'); var row, cell; var i, k, l, iLen, shifted, column, colspan, rowspan; + var titleRow = settings.titleRow; var isHeader = thead && thead.nodeName.toLowerCase() === 'thead'; var layout = []; var unique; @@ -3961,6 +4004,7 @@ cell.nodeName.toUpperCase() == 'TH' ) { var cols = []; + var jqCell = $(cell); // Get the col and rowspan attributes from the DOM and sanitise them colspan = cell.getAttribute('colspan') * 1; @@ -3981,7 +4025,7 @@ if ( write ) { if (unique) { // Allow column options to be set from HTML attributes - _fnColumnOptions( settings, shifted, $(cell).data() ); + _fnColumnOptions( settings, shifted, jqCell.data() ); // Get the width for the column. This can be defined from the // width attribute, style attribute or `columns.width` option @@ -3998,7 +4042,14 @@ // Column title handling - can be user set, or read from the DOM // This happens before the render, so the original is still in place if ( columnDef.sTitle !== null && ! columnDef.autoTitle ) { - cell.innerHTML = columnDef.sTitle; + if ( + (titleRow === true && i === 0) || // top row + (titleRow === false && i === rows.length -1) || // bottom row + (titleRow === i) || // specific row + (titleRow === null) + ) { + cell.innerHTML = columnDef.sTitle; + } } if (! columnDef.sTitle && unique) { @@ -4016,12 +4067,12 @@ // Fall back to the aria-label attribute on the table header if no ariaTitle is // provided. if (! columnDef.ariaTitle) { - columnDef.ariaTitle = $(cell).attr("aria-label") || columnDef.sTitle; + columnDef.ariaTitle = jqCell.attr("aria-label") || columnDef.sTitle; } // Column specific class names if ( columnDef.className ) { - $(cell).addClass( columnDef.className ); + jqCell.addClass( columnDef.className ); } } @@ -4033,11 +4084,28 @@ .appendTo(cell); } - if ( isHeader && $('span.dt-column-order', cell).length === 0) { + if ( + settings.orderIndicators && + isHeader && + jqCell.filter(':not([data-dt-order=disable])').length !== 0 && + jqCell.parent(':not([data-dt-order=disable])').length !== 0 && + $('span.dt-column-order', cell).length === 0 + ) { $('') .addClass('dt-column-order') .appendTo(cell); } + + // We need to wrap the elements in the header in another element to use flexbox + // layout for those elements + var headerFooter = isHeader ? 'header' : 'footer'; + + if ( $('span.dt-column-' + headerFooter, cell).length === 0) { + $('
') + .addClass('dt-column-' + headerFooter) + .append(cell.childNodes) + .appendTo(cell); + } } // If there is col / rowspan, copy the information into the layout grid @@ -4188,6 +4256,11 @@ // Allow plug-ins and external processes to modify the data _fnCallbackFire( oSettings, null, 'preXhr', [oSettings, data, baseAjax], true ); + // Custom Ajax option to submit the parameters as a JSON string + if (baseAjax.submitAs === 'json' && typeof data === 'object') { + baseAjax.data = JSON.stringify(data); + } + if ( typeof ajax === 'function' ) { // Is a function - let the caller define what needs to be done @@ -5688,24 +5761,30 @@ function _fnSortInit( settings ) { var target = settings.nTHead; var headerRows = target.querySelectorAll('tr'); - var legacyTop = settings.bSortCellsTop; + var titleRow = settings.titleRow; var notSelector = ':not([data-dt-order="disable"]):not([data-dt-order="icon-only"])'; // Legacy support for `orderCellsTop` - if (legacyTop === true) { + if (titleRow === true) { target = headerRows[0]; } - else if (legacyTop === false) { + else if (titleRow === false) { target = headerRows[ headerRows.length - 1 ]; } + else if (titleRow !== null) { + target = headerRows[titleRow]; + } + // else - all rows - _fnSortAttachListener( - settings, - target, - target === settings.nTHead - ? 'tr'+notSelector+' th'+notSelector+', tr'+notSelector+' td'+notSelector - : 'th'+notSelector+', td'+notSelector - ); + if (settings.orderHandler) { + _fnSortAttachListener( + settings, + target, + target === settings.nTHead + ? 'tr'+notSelector+' th'+notSelector+', tr'+notSelector+' td'+notSelector + : 'th'+notSelector+', td'+notSelector + ); + } // Need to resolve the user input array into our internal structure var order = []; @@ -5720,7 +5799,9 @@ var run = false; var columns = column === undefined ? _fnColumnsFromHeader( e.target ) - : [column]; + : Array.isArray(column) + ? column + : [column]; if ( columns.length ) { for ( var i=0, ien=columns.length ; i= 0 - ? idx - : 0; + if (idx < 0) { + // If the column was not found ignore it and continue + return; + } + + set[0] = idx; } else if (set[0] >= columns.length) { - // If a column name, but it is out of bounds, set to 0 - set[0] = 0; + // If the column index is out of bounds ignore it and continue + return; } settings.aaSorting.push(set); @@ -6765,6 +6849,23 @@ } } + /** + * Add one or more listeners to the table + * + * @param {*} that JQ for the table + * @param {*} name Event name + * @param {*} src Listener(s) + */ + function _fnListener(that, name, src) { + if (!Array.isArray(src)) { + src = [src]; + } + + for (i=0 ; iWeb Installed Ok Update + Pre-Release
{{page_data.web_vault_version}} @@ -68,10 +69,14 @@ No {{/unless}}
-
Environment settings overridden
+
Uses config.json + {{#if page_data.overrides}} + Note + {{/if}} +
{{#if page_data.overrides}} - Yes + Yes {{/if}} {{#unless page_data.overrides}} No @@ -154,7 +159,11 @@
{{page_data.dns_resolved}}
-
Date & Time (Local)
+
Date & Time (Local) + {{#if page_data.tz_env}} + {{page_data.tz_env}} + {{/if}} +
Server: {{page_data.server_time_local}}
diff --git a/src/static/templates/admin/organizations.hbs b/src/static/templates/admin/organizations.hbs index 654f904e..130bb14b 100644 --- a/src/static/templates/admin/organizations.hbs +++ b/src/static/templates/admin/organizations.hbs @@ -43,7 +43,7 @@ Groups: {{group_count}} Events: {{event_count}} - +
diff --git a/src/static/templates/admin/users.hbs b/src/static/templates/admin/users.hbs index 65470525..19e489c1 100644 --- a/src/static/templates/admin/users.hbs +++ b/src/static/templates/admin/users.hbs @@ -60,7 +60,7 @@ {{/each}}
- + {{#if twoFactorEnabled}}
diff --git a/src/static/templates/scss/vaultwarden.scss.hbs b/src/static/templates/scss/vaultwarden.scss.hbs index a5552609..0ef53ab1 100644 --- a/src/static/templates/scss/vaultwarden.scss.hbs +++ b/src/static/templates/scss/vaultwarden.scss.hbs @@ -82,15 +82,19 @@ bit-nav-logo bit-nav-item .bwi-shield { {{#if signup_disabled}} /* From web vault 2025.1.2 and onwards, the signup button is hidden when signups are disabled as the web vault checks the /api/config endpoint. - Note that the clients tend to aggressively cache this endpoint, so it might + Note that the clients tend to cache this endpoint for about 1 hour, so it might take a while for the change to take effect. To avoid the button appearing when it shouldn't, we'll keep this style in place for a couple of versions */ -{{#if webver "<2025.3.0"}} /* Hide the register link on the login screen */ +{{#if (webver "<2025.3.0")}} app-login form div + div + div + div + hr, app-login form div + div + div + div + hr + p { @extend %vw-hide; } +{{else}} +app-root a[routerlink="/signup"] { + @extend %vw-hide; +} {{/if}} {{/if}}