From f10e6b6ac2edbdaab3b4ddad1328ef77c65ba02c Mon Sep 17 00:00:00 2001 From: BlackDex Date: Tue, 28 Feb 2023 20:43:22 +0100 Subject: [PATCH] Some Admin Interface updates - Updated datatables - Added NTP Time check - Added Collections, Groups and Events count for orgs - Renamed `Items` to `Ciphers` - Some small style updates --- src/api/admin.rs | 64 +++++++++++++++----- src/db/models/collection.rs | 11 ++++ src/db/models/event.rs | 11 ++++ src/db/models/group.rs | 11 ++++ src/static/scripts/admin.css | 6 +- src/static/scripts/admin_diagnostics.js | 32 +++++++--- src/static/scripts/admin_organizations.js | 2 +- src/static/scripts/admin_users.js | 2 +- src/static/scripts/datatables.css | 29 ++++++--- src/static/scripts/datatables.js | 64 ++++++++++++++------ src/static/templates/admin/diagnostics.hbs | 9 ++- src/static/templates/admin/organizations.hbs | 10 ++- src/static/templates/admin/users.hbs | 12 ++-- 13 files changed, 199 insertions(+), 64 deletions(-) diff --git a/src/api/admin.rs b/src/api/admin.rs index d875d8be..d5656e98 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -300,8 +300,9 @@ fn logout(cookies: &CookieJar<'_>) -> Redirect { #[get("/users")] async fn get_users_json(_token: AdminToken, mut conn: DbConn) -> Json { - let mut users_json = Vec::new(); - for u in User::get_all(&mut conn).await { + let users = User::get_all(&mut conn).await; + let mut users_json = Vec::with_capacity(users.len()); + for u in users { let mut usr = u.to_json(&mut conn).await; usr["UserEnabled"] = json!(u.enabled); usr["CreatedAt"] = json!(format_naive_datetime_local(&u.created_at, DT_FMT)); @@ -313,8 +314,9 @@ async fn get_users_json(_token: AdminToken, mut conn: DbConn) -> Json { #[get("/users/overview")] async fn users_overview(_token: AdminToken, mut conn: DbConn) -> ApiResult> { - let mut users_json = Vec::new(); - for u in User::get_all(&mut conn).await { + let users = User::get_all(&mut conn).await; + let mut users_json = Vec::with_capacity(users.len()); + for u in users { let mut usr = u.to_json(&mut conn).await; usr["cipher_count"] = json!(Cipher::count_owned_by_user(&u.uuid, &mut conn).await); usr["attachment_count"] = json!(Attachment::count_by_user(&u.uuid, &mut conn).await); @@ -490,11 +492,15 @@ async fn update_revision_users(_token: AdminToken, mut conn: DbConn) -> EmptyRes #[get("/organizations/overview")] async fn organizations_overview(_token: AdminToken, mut conn: DbConn) -> ApiResult> { - let mut organizations_json = Vec::new(); - for o in Organization::get_all(&mut conn).await { + let organizations = Organization::get_all(&mut conn).await; + let mut organizations_json = Vec::with_capacity(organizations.len()); + for o in organizations { let mut org = o.to_json(); org["user_count"] = json!(UserOrganization::count_by_org(&o.uuid, &mut conn).await); org["cipher_count"] = json!(Cipher::count_by_org(&o.uuid, &mut conn).await); + org["collection_count"] = json!(Collection::count_by_org(&o.uuid, &mut conn).await); + org["group_count"] = json!(Group::count_by_org(&o.uuid, &mut conn).await); + org["event_count"] = json!(Event::count_by_org(&o.uuid, &mut conn).await); org["attachment_count"] = json!(Attachment::count_by_org(&o.uuid, &mut conn).await); org["attachment_size"] = json!(get_display_size(Attachment::size_by_org(&o.uuid, &mut conn).await as i32)); organizations_json.push(org); @@ -525,10 +531,20 @@ struct GitCommit { sha: String, } -async fn get_github_api(url: &str) -> Result { - let github_api = get_reqwest_client(); +#[derive(Deserialize)] +struct TimeApi { + year: u16, + month: u8, + day: u8, + hour: u8, + minute: u8, + seconds: u8, +} + +async fn get_json_api(url: &str) -> Result { + let json_api = get_reqwest_client(); - Ok(github_api.get(url).send().await?.error_for_status()?.json::().await?) + Ok(json_api.get(url).send().await?.error_for_status()?.json::().await?) } async fn has_http_access() -> bool { @@ -548,14 +564,13 @@ async fn get_release_info(has_http_access: bool, running_within_docker: bool) -> // 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 { ( - match get_github_api::("https://api.github.com/repos/dani-garcia/vaultwarden/releases/latest") + match get_json_api::("https://api.github.com/repos/dani-garcia/vaultwarden/releases/latest") .await { Ok(r) => r.tag_name, _ => "-".to_string(), }, - match get_github_api::("https://api.github.com/repos/dani-garcia/vaultwarden/commits/main").await - { + match get_json_api::("https://api.github.com/repos/dani-garcia/vaultwarden/commits/main").await { Ok(mut c) => { c.sha.truncate(8); c.sha @@ -567,7 +582,7 @@ async fn get_release_info(has_http_access: bool, running_within_docker: bool) -> if running_within_docker { "-".to_string() } else { - match get_github_api::( + match get_json_api::( "https://api.github.com/repos/dani-garcia/bw_web_builds/releases/latest", ) .await @@ -582,6 +597,24 @@ async fn get_release_info(has_http_access: bool, running_within_docker: 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 + ); + } + } + String::from("Unable to fetch NTP time.") +} + #[get("/diagnostics")] async fn diagnostics(_token: AdminToken, ip_header: IpHeader, mut conn: DbConn) -> ApiResult> { use chrono::prelude::*; @@ -610,7 +643,7 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, mut conn: DbConn) // Check if we are able to resolve DNS entries let dns_resolved = match ("github.com", 0).to_socket_addrs().map(|mut i| i.next()) { Ok(Some(a)) => a.ip().to_string(), - _ => "Could not resolve domain name.".to_string(), + _ => "Unable to resolve domain name.".to_string(), }; let (latest_release, latest_commit, latest_web_build) = @@ -644,7 +677,8 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, mut conn: DbConn) "host_arch": std::env::consts::ARCH, "host_os": std::env::consts::OS, "server_time_local": Local::now().format("%Y-%m-%d %H:%M:%S %Z").to_string(), - "server_time": 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 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 }); let text = AdminTemplateData::new("admin/diagnostics", diagnostics_json).render()?; diff --git a/src/db/models/collection.rs b/src/db/models/collection.rs index 1ae29493..68d59a20 100644 --- a/src/db/models/collection.rs +++ b/src/db/models/collection.rs @@ -234,6 +234,17 @@ impl Collection { }} } + pub async fn count_by_org(org_uuid: &str, conn: &mut DbConn) -> i64 { + db_run! { conn: { + collections::table + .filter(collections::org_uuid.eq(org_uuid)) + .count() + .first::(conn) + .ok() + .unwrap_or(0) + }} + } + pub async fn find_by_uuid_and_org(uuid: &str, org_uuid: &str, conn: &mut DbConn) -> Option { db_run! { conn: { collections::table diff --git a/src/db/models/event.rs b/src/db/models/event.rs index 64312273..af2f6c66 100644 --- a/src/db/models/event.rs +++ b/src/db/models/event.rs @@ -263,6 +263,17 @@ impl Event { }} } + pub async fn count_by_org(org_uuid: &str, conn: &mut DbConn) -> i64 { + db_run! { conn: { + event::table + .filter(event::org_uuid.eq(org_uuid)) + .count() + .first::(conn) + .ok() + .unwrap_or(0) + }} + } + pub async fn find_by_org_and_user_org( org_uuid: &str, user_org_uuid: &str, diff --git a/src/db/models/group.rs b/src/db/models/group.rs index 6f267c10..e5919612 100644 --- a/src/db/models/group.rs +++ b/src/db/models/group.rs @@ -168,6 +168,17 @@ impl Group { }} } + pub async fn count_by_org(organizations_uuid: &str, conn: &mut DbConn) -> i64 { + db_run! { conn: { + groups::table + .filter(groups::organizations_uuid.eq(organizations_uuid)) + .count() + .first::(conn) + .ok() + .unwrap_or(0) + }} + } + pub async fn find_by_uuid(uuid: &str, conn: &mut DbConn) -> Option { db_run! { conn: { groups::table diff --git a/src/static/scripts/admin.css b/src/static/scripts/admin.css index 67f2c00d..d700af3c 100644 --- a/src/static/scripts/admin.css +++ b/src/static/scripts/admin.css @@ -25,10 +25,14 @@ img { min-width: 85px; max-width: 85px; } -#users-table .vw-items, #orgs-table .vw-items, #orgs-table .vw-users { +#users-table .vw-ciphers, #orgs-table .vw-users, #orgs-table .vw-ciphers { min-width: 35px; max-width: 40px; } +#orgs-table .vw-misc { + min-width: 65px; + max-width: 80px; +} #users-table .vw-attachments, #orgs-table .vw-attachments { min-width: 100px; max-width: 130px; diff --git a/src/static/scripts/admin_diagnostics.js b/src/static/scripts/admin_diagnostics.js index a7a574fc..5fbed2da 100644 --- a/src/static/scripts/admin_diagnostics.js +++ b/src/static/scripts/admin_diagnostics.js @@ -4,6 +4,7 @@ var dnsCheck = false; var timeCheck = false; +var ntpTimeCheck = false; var domainCheck = false; var httpsCheck = false; @@ -90,7 +91,8 @@ 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`; - supportString += `* Time Check: ${timeCheck}\n`; + supportString += `* Browser/Server Time Check: ${timeCheck}\n`; + supportString += `* Server/NTP Time Check: ${ntpTimeCheck}\n`; supportString += `* Domain Configuration Check: ${domainCheck}\n`; supportString += `* HTTPS Check: ${httpsCheck}\n`; supportString += `* Database type: ${dj.db_type}\n`; @@ -136,16 +138,17 @@ function copyToClipboard(event) { new BSN.Toast("#toastClipboardCopy").show(); } -function checkTimeDrift(browserUTC, serverUTC) { +function checkTimeDrift(utcTimeA, utcTimeB, statusPrefix) { const timeDrift = ( - Date.parse(serverUTC.replace(" ", "T").replace(" UTC", "")) - - Date.parse(browserUTC.replace(" ", "T").replace(" UTC", "")) + Date.parse(utcTimeA.replace(" ", "T").replace(" UTC", "")) - + Date.parse(utcTimeB.replace(" ", "T").replace(" UTC", "")) ) / 1000; - if (timeDrift > 20 || timeDrift < -20) { - document.getElementById("time-warning").classList.remove("d-none"); + if (timeDrift > 15 || timeDrift < -15) { + document.getElementById(`${statusPrefix}-warning`).classList.remove("d-none"); + return false; } else { - document.getElementById("time-success").classList.remove("d-none"); - timeCheck = true; + document.getElementById(`${statusPrefix}-success`).classList.remove("d-none"); + return true; } } @@ -195,7 +198,18 @@ function checkDns(dns_resolved) { function init(dj) { // Time check document.getElementById("time-browser-string").innerText = browserUTC; - checkTimeDrift(browserUTC, dj.server_time); + + // Check if we were able to fetch a valid NTP Time + // If so, compare both browser and server with NTP + // Else, compare browser and server. + if (dj.ntp_time.indexOf("UTC") !== -1) { + timeCheck = checkTimeDrift(dj.server_time, browserUTC, "time"); + checkTimeDrift(dj.ntp_time, browserUTC, "ntp-browser"); + ntpTimeCheck = checkTimeDrift(dj.ntp_time, dj.server_time, "ntp-server"); + } else { + timeCheck = checkTimeDrift(dj.server_time, browserUTC, "time"); + ntpTimeCheck = "n/a"; + } // Domain check const browserURL = location.href.toLowerCase(); diff --git a/src/static/scripts/admin_organizations.js b/src/static/scripts/admin_organizations.js index db4037b4..c885344e 100644 --- a/src/static/scripts/admin_organizations.js +++ b/src/static/scripts/admin_organizations.js @@ -54,7 +54,7 @@ document.addEventListener("DOMContentLoaded", (/*event*/) => { ], "pageLength": -1, // Default show all "columnDefs": [{ - "targets": 4, + "targets": [4,5], "searchable": false, "orderable": false }] diff --git a/src/static/scripts/admin_users.js b/src/static/scripts/admin_users.js index b4da0f97..95d4f074 100644 --- a/src/static/scripts/admin_users.js +++ b/src/static/scripts/admin_users.js @@ -238,7 +238,7 @@ document.addEventListener("DOMContentLoaded", (/*event*/) => { [-1, 2, 5, 10, 25, 50], ["All", 2, 5, 10, 25, 50] ], - "pageLength": 2, // Default show all + "pageLength": -1, // Default show all "columnDefs": [{ "targets": [1, 2], "type": "date-iso" diff --git a/src/static/scripts/datatables.css b/src/static/scripts/datatables.css index a19cb110..d22b2250 100644 --- a/src/static/scripts/datatables.css +++ b/src/static/scripts/datatables.css @@ -4,13 +4,19 @@ * * To rebuild or modify this file with the latest versions of the included * software please visit: - * https://datatables.net/download/#bs5/dt-1.13.1 + * https://datatables.net/download/#bs5/dt-1.13.2 * * Included libraries: - * DataTables 1.13.1 + * DataTables 1.13.2 */ @charset "UTF-8"; +:root { + --dt-row-selected: 13, 110, 253; + --dt-row-selected-text: 255, 255, 255; + --dt-row-selected-link: 9, 10, 11; +} + table.dataTable td.dt-control { text-align: center; cursor: pointer; @@ -126,7 +132,7 @@ div.dataTables_processing > div:last-child > div { width: 13px; height: 13px; border-radius: 50%; - background: rgba(13, 110, 253, 0.9); + background: 13 110 253; animation-timing-function: cubic-bezier(0, 1, 1, 0); } div.dataTables_processing > div:last-child > div:nth-child(1) { @@ -284,23 +290,28 @@ table.dataTable > tbody > tr { background-color: transparent; } table.dataTable > tbody > tr.selected > * { - box-shadow: inset 0 0 0 9999px rgba(13, 110, 253, 0.9); - color: white; + box-shadow: inset 0 0 0 9999px rgb(13, 110, 253); + box-shadow: inset 0 0 0 9999px rgb(var(--dt-row-selected)); + color: rgb(255, 255, 255); + color: rgb(var(--dt-row-selected-text)); } table.dataTable > tbody > tr.selected a { - color: #090a0b; + color: rgb(9, 10, 11); + color: rgb(var(--dt-row-selected-link)); } table.dataTable.table-striped > tbody > tr.odd > * { box-shadow: inset 0 0 0 9999px rgba(0, 0, 0, 0.05); } table.dataTable.table-striped > tbody > tr.odd.selected > * { box-shadow: inset 0 0 0 9999px rgba(13, 110, 253, 0.95); + box-shadow: inset 0 0 0 9999px rgba(var(--dt-row-selected), 0.95); } table.dataTable.table-hover > tbody > tr:hover > * { box-shadow: inset 0 0 0 9999px rgba(0, 0, 0, 0.075); } table.dataTable.table-hover > tbody > tr.selected:hover > * { box-shadow: inset 0 0 0 9999px rgba(13, 110, 253, 0.975); + box-shadow: inset 0 0 0 9999px rgba(var(--dt-row-selected), 0.975); } div.dataTables_wrapper div.dataTables_length label { @@ -374,9 +385,9 @@ div.dataTables_scrollFoot > .dataTables_scrollFootInner > table { @media screen and (max-width: 767px) { div.dataTables_wrapper div.dataTables_length, -div.dataTables_wrapper div.dataTables_filter, -div.dataTables_wrapper div.dataTables_info, -div.dataTables_wrapper div.dataTables_paginate { + div.dataTables_wrapper div.dataTables_filter, + div.dataTables_wrapper div.dataTables_info, + div.dataTables_wrapper div.dataTables_paginate { text-align: center; } div.dataTables_wrapper div.dataTables_paginate ul.pagination { diff --git a/src/static/scripts/datatables.js b/src/static/scripts/datatables.js index 1aeda982..9854358e 100644 --- a/src/static/scripts/datatables.js +++ b/src/static/scripts/datatables.js @@ -4,20 +4,20 @@ * * To rebuild or modify this file with the latest versions of the included * software please visit: - * https://datatables.net/download/#bs5/dt-1.13.1 + * https://datatables.net/download/#bs5/dt-1.13.2 * * Included libraries: - * DataTables 1.13.1 + * DataTables 1.13.2 */ -/*! DataTables 1.13.1 - * ©2008-2022 SpryMedia Ltd - datatables.net/license +/*! DataTables 1.13.2 + * ©2008-2023 SpryMedia Ltd - datatables.net/license */ /** * @summary DataTables * @description Paginate, search and order HTML tables - * @version 1.13.1 + * @version 1.13.2 * @author SpryMedia Ltd * @contact www.datatables.net * @copyright SpryMedia Ltd. @@ -1382,7 +1382,12 @@ var _isNumber = function ( d, decimalPoint, formatted ) { - var strType = typeof d === 'string'; + let type = typeof d; + var strType = type === 'string'; + + if ( type === 'number' || type === 'bigint') { + return true; + } // If empty return immediately so there must be a number if it is a // formatted string (this stops the string "k", or "kr", etc being detected @@ -6789,8 +6794,15 @@ if ( eventName !== null ) { var e = $.Event( eventName+'.dt' ); + var table = $(settings.nTable); - $(settings.nTable).trigger( e, args ); + table.trigger( e, args ); + + // If not yet attached to the document, trigger the event + // on the body directly to sort of simulate the bubble + if (table.parents('body').length === 0) { + $('body').trigger( e, args ); + } ret.push( e.result ); } @@ -7256,7 +7268,7 @@ pluck: function ( prop ) { - let fn = DataTable.util.get(prop); + var fn = DataTable.util.get(prop); return this.map( function ( el ) { return fn(el); @@ -8353,10 +8365,9 @@ $(document).on('plugin-init.dt', function (e, context) { var api = new _Api( context ); - - const namespace = 'on-plugin-init'; - const stateSaveParamsEvent = `stateSaveParams.${namespace}`; - const destroyEvent = `destroy.${namespace}`; + var namespace = 'on-plugin-init'; + var stateSaveParamsEvent = 'stateSaveParams.' + namespace; + var destroyEvent = 'destroy. ' + namespace; api.on( stateSaveParamsEvent, function ( e, settings, d ) { // This could be more compact with the API, but it is a lot faster as a simple @@ -8375,7 +8386,7 @@ }); api.on( destroyEvent, function () { - api.off(`${stateSaveParamsEvent} ${destroyEvent}`); + api.off(stateSaveParamsEvent + ' ' + destroyEvent); }); var loaded = api.state.loaded(); @@ -9697,7 +9708,7 @@ * @type string * @default Version number */ - DataTable.version = "1.13.1"; + DataTable.version = "1.13.2"; /** * Private data store, containing all of the settings objects that are @@ -14121,7 +14132,7 @@ * * @type string */ - build:"bs5/dt-1.13.1", + build:"bs5/dt-1.13.2", /** @@ -14830,10 +14841,17 @@ } if ( btnDisplay !== null ) { - node = $('', { + var tag = settings.oInit.pagingTag || 'a'; + var disabled = btnClass.indexOf(disabledClass) !== -1; + + + node = $('<'+tag+'>', { 'class': classes.sPageButton+' '+btnClass, 'aria-controls': settings.sTableId, + 'aria-disabled': disabled ? 'true' : null, 'aria-label': aria[ button ], + 'aria-role': 'link', + 'aria-current': btnClass === classes.sPageButtonActive ? 'page' : null, 'data-dt-idx': button, 'tabindex': tabIndex, 'id': idx === 0 && typeof button === 'string' ? @@ -14965,6 +14983,12 @@ if ( d !== 0 && (!d || d === '-') ) { return -Infinity; } + + let type = typeof d; + + if (type === 'number' || type === 'bigint') { + return d; + } // If a decimal place other than `.` is used, it needs to be given to the // function so we can detect it and replace with a `.` which is the only @@ -15647,7 +15671,6 @@ require('datatables.net')(root, $); } - return factory( $, root, root.document ); }; } @@ -15755,6 +15778,8 @@ DataTable.ext.renderer.pageButton.bootstrap = function ( settings, host, idx, bu } if ( btnDisplay ) { + var disabled = btnClass.indexOf('disabled') !== -1; + node = $('
  • ', { 'class': classes.sPageButton+' '+btnClass, 'id': idx === 0 && typeof button === 'string' ? @@ -15762,9 +15787,12 @@ DataTable.ext.renderer.pageButton.bootstrap = function ( settings, host, idx, bu null } ) .append( $('', { - 'href': '#', + 'href': disabled ? null : '#', 'aria-controls': settings.sTableId, + 'aria-disabled': disabled ? 'true' : null, 'aria-label': aria[ button ], + 'aria-role': 'link', + 'aria-current': btnClass === 'active' ? 'page' : null, 'data-dt-idx': button, 'tabindex': settings.iTabIndex, 'class': 'page-link' diff --git a/src/static/templates/admin/diagnostics.hbs b/src/static/templates/admin/diagnostics.hbs index de83ae11..a61d8992 100644 --- a/src/static/templates/admin/diagnostics.hbs +++ b/src/static/templates/admin/diagnostics.hbs @@ -144,10 +144,15 @@ Server: {{page_data.server_time_local}}
    Date & Time (UTC) - Ok - Error + Server/Browser Ok + Server/Browser Error + Server NTP Ok + Server NTP Error + Browser NTP Ok + Browser NTP Error
    + NTP: {{page_data.ntp_time}} Server: {{page_data.server_time}} Browser:
    diff --git a/src/static/templates/admin/organizations.hbs b/src/static/templates/admin/organizations.hbs index d95370c4..9dd86622 100644 --- a/src/static/templates/admin/organizations.hbs +++ b/src/static/templates/admin/organizations.hbs @@ -7,8 +7,9 @@ Organization Users - Items + Ciphers Attachments + Misc Actions @@ -37,8 +38,13 @@ Size: {{attachment_size}} {{/if}} + + Collections: {{collection_count}} + Groups: {{group_count}} + Events: {{event_count}} + - +
    {{/each}} diff --git a/src/static/templates/admin/users.hbs b/src/static/templates/admin/users.hbs index 744d9fb2..9d9c684d 100644 --- a/src/static/templates/admin/users.hbs +++ b/src/static/templates/admin/users.hbs @@ -8,7 +8,7 @@ User Created at Last Active - Items + Ciphers Attachments Organizations Actions @@ -63,14 +63,14 @@ {{#if TwoFactorEnabled}} - +
    {{/if}} - - +
    +
    {{#if user_enabled}} - +
    {{else}} - +
    {{/if}}