Skip to content

Commit

Permalink
Merge pull request #3288 from BlackDex/admin-interface-updates
Browse files Browse the repository at this point in the history
Some Admin Interface updates
  • Loading branch information
dani-garcia committed Feb 28, 2023
2 parents da8225a + f10e6b6 commit 4556f66
Show file tree
Hide file tree
Showing 13 changed files with 199 additions and 64 deletions.
64 changes: 49 additions & 15 deletions src/api/admin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -300,8 +300,9 @@ fn logout(cookies: &CookieJar<'_>) -> Redirect {

#[get("/users")]
async fn get_users_json(_token: AdminToken, mut conn: DbConn) -> Json<Value> {
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));
Expand All @@ -313,8 +314,9 @@ async fn get_users_json(_token: AdminToken, mut conn: DbConn) -> Json<Value> {

#[get("/users/overview")]
async fn users_overview(_token: AdminToken, mut conn: DbConn) -> ApiResult<Html<String>> {
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);
Expand Down Expand Up @@ -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<Html<String>> {
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);
Expand Down Expand Up @@ -525,10 +531,20 @@ struct GitCommit {
sha: String,
}

async fn get_github_api<T: DeserializeOwned>(url: &str) -> Result<T, Error> {
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<T: DeserializeOwned>(url: &str) -> Result<T, Error> {
let json_api = get_reqwest_client();

Ok(github_api.get(url).send().await?.error_for_status()?.json::<T>().await?)
Ok(json_api.get(url).send().await?.error_for_status()?.json::<T>().await?)
}

async fn has_http_access() -> bool {
Expand All @@ -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::<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
{
Ok(r) => r.tag_name,
_ => "-".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) => {
c.sha.truncate(8);
c.sha
Expand All @@ -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::<GitRelease>(
match get_json_api::<GitRelease>(
"https://api.github.com/repos/dani-garcia/bw_web_builds/releases/latest",
)
.await
Expand All @@ -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")]
async fn diagnostics(_token: AdminToken, ip_header: IpHeader, mut conn: DbConn) -> ApiResult<Html<String>> {
use chrono::prelude::*;
Expand Down Expand Up @@ -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) =
Expand Down Expand Up @@ -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()?;
Expand Down
11 changes: 11 additions & 0 deletions src/db/models/collection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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> {
db_run! { conn: {
collections::table
Expand Down
11 changes: 11 additions & 0 deletions src/db/models/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
org_uuid: &str,
user_org_uuid: &str,
Expand Down
11 changes: 11 additions & 0 deletions src/db/models/group.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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> {
db_run! { conn: {
groups::table
Expand Down
6 changes: 5 additions & 1 deletion src/static/scripts/admin.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
32 changes: 23 additions & 9 deletions src/static/scripts/admin_diagnostics.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

var dnsCheck = false;
var timeCheck = false;
var ntpTimeCheck = false;
var domainCheck = false;
var httpsCheck = false;

Expand Down Expand Up @@ -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`;
Expand Down Expand Up @@ -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;
}
}

Expand Down Expand Up @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion src/static/scripts/admin_organizations.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ document.addEventListener("DOMContentLoaded", (/*event*/) => {
],
"pageLength": -1, // Default show all
"columnDefs": [{
"targets": 4,
"targets": [4,5],
"searchable": false,
"orderable": false
}]
Expand Down
2 changes: 1 addition & 1 deletion src/static/scripts/admin_users.js
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,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"
Expand Down
29 changes: 20 additions & 9 deletions src/static/scripts/datatables.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down

0 comments on commit 4556f66

Please sign in to comment.