mirror of
				https://github.com/dani-garcia/vaultwarden.git
				synced 2025-10-31 10:18:19 +02:00 
			
		
		
		
	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
This commit is contained in:
		| @@ -300,8 +300,9 @@ fn logout(cookies: &CookieJar<'_>) -> Redirect { | |||||||
|  |  | ||||||
| #[get("/users")] | #[get("/users")] | ||||||
| async fn get_users_json(_token: AdminToken, mut conn: DbConn) -> Json<Value> { | async fn get_users_json(_token: AdminToken, mut conn: DbConn) -> Json<Value> { | ||||||
|     let mut users_json = Vec::new(); |     let users = User::get_all(&mut conn).await; | ||||||
|     for u in 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; |         let mut usr = u.to_json(&mut conn).await; | ||||||
|         usr["UserEnabled"] = json!(u.enabled); |         usr["UserEnabled"] = json!(u.enabled); | ||||||
|         usr["CreatedAt"] = json!(format_naive_datetime_local(&u.created_at, DT_FMT)); |         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<Value> { | |||||||
|  |  | ||||||
| #[get("/users/overview")] | #[get("/users/overview")] | ||||||
| async fn users_overview(_token: AdminToken, mut conn: DbConn) -> ApiResult<Html<String>> { | async fn users_overview(_token: AdminToken, mut conn: DbConn) -> ApiResult<Html<String>> { | ||||||
|     let mut users_json = Vec::new(); |     let users = User::get_all(&mut conn).await; | ||||||
|     for u in 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; |         let mut usr = u.to_json(&mut conn).await; | ||||||
|         usr["cipher_count"] = json!(Cipher::count_owned_by_user(&u.uuid, &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); |         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")] | #[get("/organizations/overview")] | ||||||
| async fn organizations_overview(_token: AdminToken, mut conn: DbConn) -> ApiResult<Html<String>> { | async fn organizations_overview(_token: AdminToken, mut conn: DbConn) -> ApiResult<Html<String>> { | ||||||
|     let mut organizations_json = Vec::new(); |     let organizations = Organization::get_all(&mut conn).await; | ||||||
|     for o in 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(); |         let mut org = o.to_json(); | ||||||
|         org["user_count"] = json!(UserOrganization::count_by_org(&o.uuid, &mut conn).await); |         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["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_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)); |         org["attachment_size"] = json!(get_display_size(Attachment::size_by_org(&o.uuid, &mut conn).await as i32)); | ||||||
|         organizations_json.push(org); |         organizations_json.push(org); | ||||||
| @@ -525,10 +531,20 @@ struct GitCommit { | |||||||
|     sha: String, |     sha: String, | ||||||
| } | } | ||||||
|  |  | ||||||
| async fn get_github_api<T: DeserializeOwned>(url: &str) -> Result<T, Error> { | #[derive(Deserialize)] | ||||||
|     let github_api = get_reqwest_client(); | struct TimeApi { | ||||||
|  |     year: u16, | ||||||
|  |     month: u8, | ||||||
|  |     day: u8, | ||||||
|  |     hour: u8, | ||||||
|  |     minute: u8, | ||||||
|  |     seconds: u8, | ||||||
|  | } | ||||||
|  |  | ||||||
|     Ok(github_api.get(url).send().await?.error_for_status()?.json::<T>().await?) | async fn get_json_api<T: DeserializeOwned>(url: &str) -> Result<T, Error> { | ||||||
|  |     let json_api = get_reqwest_client(); | ||||||
|  |  | ||||||
|  |     Ok(json_api.get(url).send().await?.error_for_status()?.json::<T>().await?) | ||||||
| } | } | ||||||
|  |  | ||||||
| async fn has_http_access() -> bool { | 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 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 { |     if has_http_access { | ||||||
|         ( |         ( | ||||||
|             match get_github_api::<GitRelease>("https://api.github.com/repos/dani-garcia/vaultwarden/releases/latest") |             match get_json_api::<GitRelease>("https://api.github.com/repos/dani-garcia/vaultwarden/releases/latest") | ||||||
|                 .await |                 .await | ||||||
|             { |             { | ||||||
|                 Ok(r) => r.tag_name, |                 Ok(r) => r.tag_name, | ||||||
|                 _ => "-".to_string(), |                 _ => "-".to_string(), | ||||||
|             }, |             }, | ||||||
|             match get_github_api::<GitCommit>("https://api.github.com/repos/dani-garcia/vaultwarden/commits/main").await |             match get_json_api::<GitCommit>("https://api.github.com/repos/dani-garcia/vaultwarden/commits/main").await { | ||||||
|             { |  | ||||||
|                 Ok(mut c) => { |                 Ok(mut c) => { | ||||||
|                     c.sha.truncate(8); |                     c.sha.truncate(8); | ||||||
|                     c.sha |                     c.sha | ||||||
| @@ -567,7 +582,7 @@ async fn get_release_info(has_http_access: bool, running_within_docker: bool) -> | |||||||
|             if running_within_docker { |             if running_within_docker { | ||||||
|                 "-".to_string() |                 "-".to_string() | ||||||
|             } else { |             } else { | ||||||
|                 match get_github_api::<GitRelease>( |                 match get_json_api::<GitRelease>( | ||||||
|                     "https://api.github.com/repos/dani-garcia/bw_web_builds/releases/latest", |                     "https://api.github.com/repos/dani-garcia/bw_web_builds/releases/latest", | ||||||
|                 ) |                 ) | ||||||
|                 .await |                 .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::<TimeApi>("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")] | #[get("/diagnostics")] | ||||||
| async fn diagnostics(_token: AdminToken, ip_header: IpHeader, mut conn: DbConn) -> ApiResult<Html<String>> { | async fn diagnostics(_token: AdminToken, ip_header: IpHeader, mut conn: DbConn) -> ApiResult<Html<String>> { | ||||||
|     use chrono::prelude::*; |     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 |     // Check if we are able to resolve DNS entries | ||||||
|     let dns_resolved = match ("github.com", 0).to_socket_addrs().map(|mut i| i.next()) { |     let dns_resolved = match ("github.com", 0).to_socket_addrs().map(|mut i| i.next()) { | ||||||
|         Ok(Some(a)) => a.ip().to_string(), |         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) = |     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_arch": std::env::consts::ARCH, | ||||||
|         "host_os":  std::env::consts::OS, |         "host_os":  std::env::consts::OS, | ||||||
|         "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 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()?; |     let text = AdminTemplateData::new("admin/diagnostics", diagnostics_json).render()?; | ||||||
|   | |||||||
| @@ -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::<i64>(conn) | ||||||
|  |                 .ok() | ||||||
|  |                 .unwrap_or(0) | ||||||
|  |         }} | ||||||
|  |     } | ||||||
|  |  | ||||||
|     pub async fn find_by_uuid_and_org(uuid: &str, org_uuid: &str, conn: &mut DbConn) -> Option<Self> { |     pub async fn find_by_uuid_and_org(uuid: &str, org_uuid: &str, conn: &mut DbConn) -> Option<Self> { | ||||||
|         db_run! { conn: { |         db_run! { conn: { | ||||||
|             collections::table |             collections::table | ||||||
|   | |||||||
| @@ -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::<i64>(conn) | ||||||
|  |                 .ok() | ||||||
|  |                 .unwrap_or(0) | ||||||
|  |         }} | ||||||
|  |     } | ||||||
|  |  | ||||||
|     pub async fn find_by_org_and_user_org( |     pub async fn find_by_org_and_user_org( | ||||||
|         org_uuid: &str, |         org_uuid: &str, | ||||||
|         user_org_uuid: &str, |         user_org_uuid: &str, | ||||||
|   | |||||||
| @@ -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::<i64>(conn) | ||||||
|  |                 .ok() | ||||||
|  |                 .unwrap_or(0) | ||||||
|  |         }} | ||||||
|  |     } | ||||||
|  |  | ||||||
|     pub async fn find_by_uuid(uuid: &str, conn: &mut DbConn) -> Option<Self> { |     pub async fn find_by_uuid(uuid: &str, conn: &mut DbConn) -> Option<Self> { | ||||||
|         db_run! { conn: { |         db_run! { conn: { | ||||||
|             groups::table |             groups::table | ||||||
|   | |||||||
							
								
								
									
										6
									
								
								src/static/scripts/admin.css
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								src/static/scripts/admin.css
									
									
									
									
										vendored
									
									
								
							| @@ -25,10 +25,14 @@ img { | |||||||
|     min-width: 85px; |     min-width: 85px; | ||||||
|     max-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; |     min-width: 35px; | ||||||
|     max-width: 40px; |     max-width: 40px; | ||||||
| } | } | ||||||
|  | #orgs-table .vw-misc { | ||||||
|  |     min-width: 65px; | ||||||
|  |     max-width: 80px; | ||||||
|  | } | ||||||
| #users-table .vw-attachments, #orgs-table .vw-attachments { | #users-table .vw-attachments, #orgs-table .vw-attachments { | ||||||
|     min-width: 100px; |     min-width: 100px; | ||||||
|     max-width: 130px; |     max-width: 130px; | ||||||
|   | |||||||
							
								
								
									
										32
									
								
								src/static/scripts/admin_diagnostics.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										32
									
								
								src/static/scripts/admin_diagnostics.js
									
									
									
									
										vendored
									
									
								
							| @@ -4,6 +4,7 @@ | |||||||
|  |  | ||||||
| var dnsCheck = false; | var dnsCheck = false; | ||||||
| var timeCheck = false; | var timeCheck = false; | ||||||
|  | var ntpTimeCheck = false; | ||||||
| var domainCheck = false; | var domainCheck = false; | ||||||
| var httpsCheck = false; | var httpsCheck = false; | ||||||
|  |  | ||||||
| @@ -90,7 +91,8 @@ async function generateSupportString(event, dj) { | |||||||
|     supportString += `* Internet access: ${dj.has_http_access}\n`; |     supportString += `* Internet access: ${dj.has_http_access}\n`; | ||||||
|     supportString += `* Internet access via a proxy: ${dj.uses_proxy}\n`; |     supportString += `* Internet access via a proxy: ${dj.uses_proxy}\n`; | ||||||
|     supportString += `* DNS Check: ${dnsCheck}\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 += `* Domain Configuration Check: ${domainCheck}\n`; | ||||||
|     supportString += `* HTTPS Check: ${httpsCheck}\n`; |     supportString += `* HTTPS Check: ${httpsCheck}\n`; | ||||||
|     supportString += `* Database type: ${dj.db_type}\n`; |     supportString += `* Database type: ${dj.db_type}\n`; | ||||||
| @@ -136,16 +138,17 @@ function copyToClipboard(event) { | |||||||
|     new BSN.Toast("#toastClipboardCopy").show(); |     new BSN.Toast("#toastClipboardCopy").show(); | ||||||
| } | } | ||||||
|  |  | ||||||
| function checkTimeDrift(browserUTC, serverUTC) { | function checkTimeDrift(utcTimeA, utcTimeB, statusPrefix) { | ||||||
|     const timeDrift = ( |     const timeDrift = ( | ||||||
|         Date.parse(serverUTC.replace(" ", "T").replace(" UTC", "")) - |         Date.parse(utcTimeA.replace(" ", "T").replace(" UTC", "")) - | ||||||
|         Date.parse(browserUTC.replace(" ", "T").replace(" UTC", "")) |         Date.parse(utcTimeB.replace(" ", "T").replace(" UTC", "")) | ||||||
|     ) / 1000; |     ) / 1000; | ||||||
|     if (timeDrift > 20 || timeDrift < -20) { |     if (timeDrift > 15 || timeDrift < -15) { | ||||||
|         document.getElementById("time-warning").classList.remove("d-none"); |         document.getElementById(`${statusPrefix}-warning`).classList.remove("d-none"); | ||||||
|  |         return false; | ||||||
|     } else { |     } else { | ||||||
|         document.getElementById("time-success").classList.remove("d-none"); |         document.getElementById(`${statusPrefix}-success`).classList.remove("d-none"); | ||||||
|         timeCheck = true; |         return true; | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -195,7 +198,18 @@ function checkDns(dns_resolved) { | |||||||
| function init(dj) { | function init(dj) { | ||||||
|     // Time check |     // Time check | ||||||
|     document.getElementById("time-browser-string").innerText = browserUTC; |     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 |     // Domain check | ||||||
|     const browserURL = location.href.toLowerCase(); |     const browserURL = location.href.toLowerCase(); | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								src/static/scripts/admin_organizations.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								src/static/scripts/admin_organizations.js
									
									
									
									
										vendored
									
									
								
							| @@ -54,7 +54,7 @@ document.addEventListener("DOMContentLoaded", (/*event*/) => { | |||||||
|         ], |         ], | ||||||
|         "pageLength": -1, // Default show all |         "pageLength": -1, // Default show all | ||||||
|         "columnDefs": [{ |         "columnDefs": [{ | ||||||
|             "targets": 4, |             "targets": [4,5], | ||||||
|             "searchable": false, |             "searchable": false, | ||||||
|             "orderable": false |             "orderable": false | ||||||
|         }] |         }] | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								src/static/scripts/admin_users.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								src/static/scripts/admin_users.js
									
									
									
									
										vendored
									
									
								
							| @@ -238,7 +238,7 @@ document.addEventListener("DOMContentLoaded", (/*event*/) => { | |||||||
|             [-1, 2, 5, 10, 25, 50], |             [-1, 2, 5, 10, 25, 50], | ||||||
|             ["All", 2, 5, 10, 25, 50] |             ["All", 2, 5, 10, 25, 50] | ||||||
|         ], |         ], | ||||||
|         "pageLength": 2, // Default show all |         "pageLength": -1, // Default show all | ||||||
|         "columnDefs": [{ |         "columnDefs": [{ | ||||||
|             "targets": [1, 2], |             "targets": [1, 2], | ||||||
|             "type": "date-iso" |             "type": "date-iso" | ||||||
|   | |||||||
							
								
								
									
										29
									
								
								src/static/scripts/datatables.css
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										29
									
								
								src/static/scripts/datatables.css
									
									
									
									
										vendored
									
									
								
							| @@ -4,13 +4,19 @@ | |||||||
|  * |  * | ||||||
|  * 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/#bs5/dt-1.13.1 |  *   https://datatables.net/download/#bs5/dt-1.13.2 | ||||||
|  * |  * | ||||||
|  * Included libraries: |  * Included libraries: | ||||||
|  *   DataTables 1.13.1 |  *   DataTables 1.13.2 | ||||||
|  */ |  */ | ||||||
|  |  | ||||||
| @charset "UTF-8"; | @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 { | table.dataTable td.dt-control { | ||||||
|   text-align: center; |   text-align: center; | ||||||
|   cursor: pointer; |   cursor: pointer; | ||||||
| @@ -126,7 +132,7 @@ div.dataTables_processing > div:last-child > div { | |||||||
|   width: 13px; |   width: 13px; | ||||||
|   height: 13px; |   height: 13px; | ||||||
|   border-radius: 50%; |   border-radius: 50%; | ||||||
|   background: rgba(13, 110, 253, 0.9); |   background: 13 110 253; | ||||||
|   animation-timing-function: cubic-bezier(0, 1, 1, 0); |   animation-timing-function: cubic-bezier(0, 1, 1, 0); | ||||||
| } | } | ||||||
| div.dataTables_processing > div:last-child > div:nth-child(1) { | div.dataTables_processing > div:last-child > div:nth-child(1) { | ||||||
| @@ -284,23 +290,28 @@ table.dataTable > tbody > tr { | |||||||
|   background-color: transparent; |   background-color: transparent; | ||||||
| } | } | ||||||
| table.dataTable > tbody > tr.selected > * { | table.dataTable > tbody > tr.selected > * { | ||||||
|   box-shadow: inset 0 0 0 9999px rgba(13, 110, 253, 0.9); |   box-shadow: inset 0 0 0 9999px rgb(13, 110, 253); | ||||||
|   color: white; |   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 { | 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 > * { | table.dataTable.table-striped > tbody > tr.odd > * { | ||||||
|   box-shadow: inset 0 0 0 9999px rgba(0, 0, 0, 0.05); |   box-shadow: inset 0 0 0 9999px rgba(0, 0, 0, 0.05); | ||||||
| } | } | ||||||
| table.dataTable.table-striped > tbody > tr.odd.selected > * { | 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(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 > * { | table.dataTable.table-hover > tbody > tr:hover > * { | ||||||
|   box-shadow: inset 0 0 0 9999px rgba(0, 0, 0, 0.075); |   box-shadow: inset 0 0 0 9999px rgba(0, 0, 0, 0.075); | ||||||
| } | } | ||||||
| table.dataTable.table-hover > tbody > tr.selected:hover > * { | 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(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 { | div.dataTables_wrapper div.dataTables_length label { | ||||||
| @@ -374,9 +385,9 @@ div.dataTables_scrollFoot > .dataTables_scrollFootInner > table { | |||||||
|  |  | ||||||
| @media screen and (max-width: 767px) { | @media screen and (max-width: 767px) { | ||||||
|   div.dataTables_wrapper div.dataTables_length, |   div.dataTables_wrapper div.dataTables_length, | ||||||
| div.dataTables_wrapper div.dataTables_filter, |   div.dataTables_wrapper div.dataTables_filter, | ||||||
| div.dataTables_wrapper div.dataTables_info, |   div.dataTables_wrapper div.dataTables_info, | ||||||
| div.dataTables_wrapper div.dataTables_paginate { |   div.dataTables_wrapper div.dataTables_paginate { | ||||||
|     text-align: center; |     text-align: center; | ||||||
|   } |   } | ||||||
|   div.dataTables_wrapper div.dataTables_paginate ul.pagination { |   div.dataTables_wrapper div.dataTables_paginate ul.pagination { | ||||||
|   | |||||||
							
								
								
									
										64
									
								
								src/static/scripts/datatables.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										64
									
								
								src/static/scripts/datatables.js
									
									
									
									
										vendored
									
									
								
							| @@ -4,20 +4,20 @@ | |||||||
|  * |  * | ||||||
|  * To rebuild or modify this file with the latest versions of the included |  * To rebuild or modify this file with the latest versions of the included | ||||||
|  * software please visit: |  * software please visit: | ||||||
|  *   https://datatables.net/download/#bs5/dt-1.13.1 |  *   https://datatables.net/download/#bs5/dt-1.13.2 | ||||||
|  * |  * | ||||||
|  * Included libraries: |  * Included libraries: | ||||||
|  *   DataTables 1.13.1 |  *   DataTables 1.13.2 | ||||||
|  */ |  */ | ||||||
|  |  | ||||||
| /*! DataTables 1.13.1 | /*! DataTables 1.13.2 | ||||||
|  * ©2008-2022 SpryMedia Ltd - datatables.net/license |  * ©2008-2023 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.13.1 |  * @version     1.13.2 | ||||||
|  * @author      SpryMedia Ltd |  * @author      SpryMedia Ltd | ||||||
|  * @contact     www.datatables.net |  * @contact     www.datatables.net | ||||||
|  * @copyright   SpryMedia Ltd. |  * @copyright   SpryMedia Ltd. | ||||||
| @@ -1382,7 +1382,12 @@ | |||||||
| 	 | 	 | ||||||
| 	 | 	 | ||||||
| 	var _isNumber = function ( d, decimalPoint, formatted ) { | 	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 | 		// 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 | 		// formatted string (this stops the string "k", or "kr", etc being detected | ||||||
| @@ -6789,8 +6794,15 @@ | |||||||
| 	 | 	 | ||||||
| 		if ( eventName !== null ) { | 		if ( eventName !== null ) { | ||||||
| 			var e = $.Event( eventName+'.dt' ); | 			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 ); | 			ret.push( e.result ); | ||||||
| 		} | 		} | ||||||
| @@ -7256,7 +7268,7 @@ | |||||||
| 	 | 	 | ||||||
| 		pluck: function ( prop ) | 		pluck: function ( prop ) | ||||||
| 		{ | 		{ | ||||||
| 			let fn = DataTable.util.get(prop); | 			var fn = DataTable.util.get(prop); | ||||||
| 	 | 	 | ||||||
| 			return this.map( function ( el ) { | 			return this.map( function ( el ) { | ||||||
| 				return fn(el); | 				return fn(el); | ||||||
| @@ -8353,10 +8365,9 @@ | |||||||
| 	 | 	 | ||||||
| 	$(document).on('plugin-init.dt', function (e, context) { | 	$(document).on('plugin-init.dt', function (e, context) { | ||||||
| 		var api = new _Api( context ); | 		var api = new _Api( context ); | ||||||
| 		 | 		var namespace = 'on-plugin-init'; | ||||||
| 		const namespace = 'on-plugin-init'; | 		var stateSaveParamsEvent = 'stateSaveParams.' + namespace; | ||||||
| 		const stateSaveParamsEvent = `stateSaveParams.${namespace}`; | 		var destroyEvent = 'destroy. ' + namespace; | ||||||
| 		const destroyEvent = `destroy.${namespace}`; |  | ||||||
| 	 | 	 | ||||||
| 		api.on( stateSaveParamsEvent, function ( e, settings, d ) { | 		api.on( stateSaveParamsEvent, function ( e, settings, d ) { | ||||||
| 			// This could be more compact with the API, but it is a lot faster as a simple | 			// 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.on( destroyEvent, function () { | ||||||
| 			api.off(`${stateSaveParamsEvent} ${destroyEvent}`); | 			api.off(stateSaveParamsEvent + ' ' + destroyEvent); | ||||||
| 		}); | 		}); | ||||||
| 	 | 	 | ||||||
| 		var loaded = api.state.loaded(); | 		var loaded = api.state.loaded(); | ||||||
| @@ -9697,7 +9708,7 @@ | |||||||
| 	 *  @type string | 	 *  @type string | ||||||
| 	 *  @default Version number | 	 *  @default Version number | ||||||
| 	 */ | 	 */ | ||||||
| 	DataTable.version = "1.13.1"; | 	DataTable.version = "1.13.2"; | ||||||
| 	 | 	 | ||||||
| 	/** | 	/** | ||||||
| 	 * Private data store, containing all of the settings objects that are | 	 * Private data store, containing all of the settings objects that are | ||||||
| @@ -14121,7 +14132,7 @@ | |||||||
| 		 * | 		 * | ||||||
| 		 *  @type string | 		 *  @type string | ||||||
| 		 */ | 		 */ | ||||||
| 		build:"bs5/dt-1.13.1", | 		build:"bs5/dt-1.13.2", | ||||||
| 	 | 	 | ||||||
| 	 | 	 | ||||||
| 		/** | 		/** | ||||||
| @@ -14830,10 +14841,17 @@ | |||||||
| 							} | 							} | ||||||
| 	 | 	 | ||||||
| 							if ( btnDisplay !== null ) { | 							if ( btnDisplay !== null ) { | ||||||
| 								node = $('<a>', { | 								var tag = settings.oInit.pagingTag || 'a'; | ||||||
|  | 								var disabled = btnClass.indexOf(disabledClass) !== -1; | ||||||
|  | 			 | ||||||
|  | 	 | ||||||
|  | 								node = $('<'+tag+'>', { | ||||||
| 										'class': classes.sPageButton+' '+btnClass, | 										'class': classes.sPageButton+' '+btnClass, | ||||||
| 										'aria-controls': settings.sTableId, | 										'aria-controls': settings.sTableId, | ||||||
|  | 										'aria-disabled': disabled ? 'true' : null, | ||||||
| 										'aria-label': aria[ button ], | 										'aria-label': aria[ button ], | ||||||
|  | 										'aria-role': 'link', | ||||||
|  | 										'aria-current': btnClass === classes.sPageButtonActive ? 'page' : null, | ||||||
| 										'data-dt-idx': button, | 										'data-dt-idx': button, | ||||||
| 										'tabindex': tabIndex, | 										'tabindex': tabIndex, | ||||||
| 										'id': idx === 0 && typeof button === 'string' ? | 										'id': idx === 0 && typeof button === 'string' ? | ||||||
| @@ -14965,6 +14983,12 @@ | |||||||
| 		if ( d !== 0 && (!d || d === '-') ) { | 		if ( d !== 0 && (!d || d === '-') ) { | ||||||
| 			return -Infinity; | 			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 | 		// 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 | 		// function so we can detect it and replace with a `.` which is the only | ||||||
| @@ -15647,7 +15671,6 @@ | |||||||
| 				require('datatables.net')(root, $); | 				require('datatables.net')(root, $); | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
|  |  | ||||||
| 			return factory( $, root, root.document ); | 			return factory( $, root, root.document ); | ||||||
| 		}; | 		}; | ||||||
| 	} | 	} | ||||||
| @@ -15755,6 +15778,8 @@ DataTable.ext.renderer.pageButton.bootstrap = function ( settings, host, idx, bu | |||||||
| 				} | 				} | ||||||
|  |  | ||||||
| 				if ( btnDisplay ) { | 				if ( btnDisplay ) { | ||||||
|  | 					var disabled = btnClass.indexOf('disabled') !== -1; | ||||||
|  |  | ||||||
| 					node = $('<li>', { | 					node = $('<li>', { | ||||||
| 							'class': classes.sPageButton+' '+btnClass, | 							'class': classes.sPageButton+' '+btnClass, | ||||||
| 							'id': idx === 0 && typeof button === 'string' ? | 							'id': idx === 0 && typeof button === 'string' ? | ||||||
| @@ -15762,9 +15787,12 @@ DataTable.ext.renderer.pageButton.bootstrap = function ( settings, host, idx, bu | |||||||
| 								null | 								null | ||||||
| 						} ) | 						} ) | ||||||
| 						.append( $('<a>', { | 						.append( $('<a>', { | ||||||
| 								'href': '#', | 								'href': disabled ? null : '#', | ||||||
| 								'aria-controls': settings.sTableId, | 								'aria-controls': settings.sTableId, | ||||||
|  | 								'aria-disabled': disabled ? 'true' : null, | ||||||
| 								'aria-label': aria[ button ], | 								'aria-label': aria[ button ], | ||||||
|  | 								'aria-role': 'link', | ||||||
|  | 								'aria-current': btnClass === 'active' ? 'page' : null, | ||||||
| 								'data-dt-idx': button, | 								'data-dt-idx': button, | ||||||
| 								'tabindex': settings.iTabIndex, | 								'tabindex': settings.iTabIndex, | ||||||
| 								'class': 'page-link' | 								'class': 'page-link' | ||||||
|   | |||||||
| @@ -144,10 +144,15 @@ | |||||||
|                         <span><b>Server:</b> {{page_data.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 bg-success d-none" id="time-success" title="Server and browser times are within 20 seconds of each other.">Ok</span> |                         <span class="badge bg-success d-none" id="time-success" title="Server and browser times are within 15 seconds of each other.">Server/Browser Ok</span> | ||||||
|                         <span class="badge bg-danger d-none" id="time-warning" title="Server and browser times are more than 20 seconds apart.">Error</span> |                         <span class="badge bg-danger d-none" id="time-warning" title="Server and browser times are more than 15 seconds apart.">Server/Browser Error</span> | ||||||
|  |                         <span class="badge bg-success d-none" id="ntp-server-success" title="Server and NTP times are within 15 seconds of each other.">Server NTP Ok</span> | ||||||
|  |                         <span class="badge bg-danger d-none" id="ntp-server-warning" title="Server and NTP times are more than 15 seconds apart.">Server NTP Error</span> | ||||||
|  |                         <span class="badge bg-success d-none" id="ntp-browser-success" title="Browser and NTP times are within 15 seconds of each other.">Browser NTP Ok</span> | ||||||
|  |                         <span class="badge bg-danger d-none" id="ntp-browser-warning" title="Browser and NTP times are more than 15 seconds apart.">Browser NTP Error</span> | ||||||
|                     </dt> |                     </dt> | ||||||
|                     <dd class="col-sm-7"> |                     <dd class="col-sm-7"> | ||||||
|  |                         <span id="ntp-time" class="d-block"><b>NTP:</b> <span id="ntp-server-string">{{page_data.ntp_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-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> | ||||||
|   | |||||||
| @@ -7,8 +7,9 @@ | |||||||
|                     <tr> |                     <tr> | ||||||
|                         <th class="vw-org-details">Organization</th> |                         <th class="vw-org-details">Organization</th> | ||||||
|                         <th class="vw-users">Users</th> |                         <th class="vw-users">Users</th> | ||||||
|                         <th class="vw-items">Items</th> |                         <th class="vw-ciphers">Ciphers</th> | ||||||
|                         <th class="vw-attachments">Attachments</th> |                         <th class="vw-attachments">Attachments</th> | ||||||
|  |                         <th class="vw-misc">Misc</th> | ||||||
|                         <th class="vw-actions">Actions</th> |                         <th class="vw-actions">Actions</th> | ||||||
|                     </tr> |                     </tr> | ||||||
|                 </thead> |                 </thead> | ||||||
| @@ -37,8 +38,13 @@ | |||||||
|                             <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> | ||||||
|  |                             <span class="d-block"><strong>Collections:</strong> {{collection_count}}</span> | ||||||
|  |                             <span class="d-block"><strong>Groups:</strong> {{group_count}}</span> | ||||||
|  |                             <span class="d-block"><strong>Events:</strong> {{event_count}}</span> | ||||||
|  |                         </td> | ||||||
|                         <td class="text-end px-0 small"> |                         <td class="text-end px-0 small"> | ||||||
|                             <button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-delete-organization data-vw-org-uuid="{{jsesc Id no_quote}}" data-vw-org-name="{{jsesc Name no_quote}}" data-vw-billing-email="{{jsesc BillingEmail no_quote}}">Delete Organization</button> |                             <button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-delete-organization data-vw-org-uuid="{{jsesc Id no_quote}}" data-vw-org-name="{{jsesc Name no_quote}}" data-vw-billing-email="{{jsesc BillingEmail no_quote}}">Delete Organization</button><br> | ||||||
|                         </td> |                         </td> | ||||||
|                     </tr> |                     </tr> | ||||||
|                     {{/each}} |                     {{/each}} | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ | |||||||
|                         <th class="vw-account-details">User</th> |                         <th class="vw-account-details">User</th> | ||||||
|                         <th class="vw-created-at">Created at</th> |                         <th class="vw-created-at">Created at</th> | ||||||
|                         <th class="vw-last-active">Last Active</th> |                         <th class="vw-last-active">Last Active</th> | ||||||
|                         <th class="vw-items">Items</th> |                         <th class="vw-ciphers">Ciphers</th> | ||||||
|                         <th class="vw-attachments">Attachments</th> |                         <th class="vw-attachments">Attachments</th> | ||||||
|                         <th class="vw-organizations">Organizations</th> |                         <th class="vw-organizations">Organizations</th> | ||||||
|                         <th class="vw-actions">Actions</th> |                         <th class="vw-actions">Actions</th> | ||||||
| @@ -63,14 +63,14 @@ | |||||||
|                         <td class="text-end px-0 small"> |                         <td class="text-end px-0 small"> | ||||||
|                             <span data-vw-user-uuid="{{jsesc Id no_quote}}" data-vw-user-email="{{jsesc Email no_quote}}"> |                             <span data-vw-user-uuid="{{jsesc Id no_quote}}" data-vw-user-email="{{jsesc Email no_quote}}"> | ||||||
|                                 {{#if TwoFactorEnabled}} |                                 {{#if TwoFactorEnabled}} | ||||||
|                                 <button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-remove2fa>Remove all 2FA</button> |                                 <button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-remove2fa>Remove all 2FA</button><br> | ||||||
|                                 {{/if}} |                                 {{/if}} | ||||||
|                                 <button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-deauth-user>Deauthorize sessions</button> |                                 <button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-deauth-user>Deauthorize sessions</button><br> | ||||||
|                                 <button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-delete-user>Delete User</button> |                                 <button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-delete-user>Delete User</button><br> | ||||||
|                                 {{#if user_enabled}} |                                 {{#if user_enabled}} | ||||||
|                                 <button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-disable-user>Disable User</button> |                                 <button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-disable-user>Disable User</button><br> | ||||||
|                                 {{else}} |                                 {{else}} | ||||||
|                                 <button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-enable-user>Enable User</button> |                                 <button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-enable-user>Enable User</button><br> | ||||||
|                                 {{/if}} |                                 {{/if}} | ||||||
|                             </span> |                             </span> | ||||||
|                         </td> |                         </td> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user