diff --git a/src/api/admin.rs b/src/api/admin.rs index dcafaa06..8678e0d9 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -23,7 +23,7 @@ pub fn routes() -> Vec { routes![ admin_login, - get_users, + get_users_json, post_admin_login, admin_page, invite_user, @@ -36,6 +36,9 @@ pub fn routes() -> Vec { delete_config, backup_db, test_smtp, + users_overview, + organizations_overview, + diagnostics, ] } @@ -118,7 +121,9 @@ fn _validate_token(token: &str) -> bool { struct AdminTemplateData { page_content: String, version: Option<&'static str>, - users: Vec, + users: Option>, + organizations: Option>, + diagnostics: Option, config: Value, can_backup: bool, logged_in: bool, @@ -126,15 +131,59 @@ struct AdminTemplateData { } impl AdminTemplateData { - fn new(users: Vec) -> Self { + fn new() -> Self { Self { - page_content: String::from("admin/page"), + page_content: String::from("admin/settings"), version: VERSION, - users, config: CONFIG.prepare_json(), can_backup: *CAN_BACKUP, logged_in: true, urlpath: CONFIG.domain_path(), + users: None, + organizations: None, + diagnostics: None, + } + } + + fn users(users: Vec) -> Self { + Self { + page_content: String::from("admin/users"), + version: VERSION, + users: Some(users), + config: CONFIG.prepare_json(), + can_backup: *CAN_BACKUP, + logged_in: true, + urlpath: CONFIG.domain_path(), + organizations: None, + diagnostics: None, + } + } + + fn organizations(organizations: Vec) -> Self { + Self { + page_content: String::from("admin/organizations"), + version: VERSION, + organizations: Some(organizations), + config: CONFIG.prepare_json(), + can_backup: *CAN_BACKUP, + logged_in: true, + urlpath: CONFIG.domain_path(), + users: None, + diagnostics: None, + } + } + + fn diagnostics(diagnostics: Value) -> Self { + Self { + page_content: String::from("admin/diagnostics"), + version: VERSION, + organizations: None, + config: CONFIG.prepare_json(), + can_backup: *CAN_BACKUP, + logged_in: true, + urlpath: CONFIG.domain_path(), + users: None, + diagnostics: Some(diagnostics), } } @@ -144,11 +193,8 @@ impl AdminTemplateData { } #[get("/", rank = 1)] -fn admin_page(_token: AdminToken, conn: DbConn) -> ApiResult> { - let users = User::get_all(&conn); - let users_json: Vec = users.iter().map(|u| u.to_json(&conn)).collect(); - - let text = AdminTemplateData::new(users_json).render()?; +fn admin_page(_token: AdminToken, _conn: DbConn) -> ApiResult> { + let text = AdminTemplateData::new().render()?; Ok(Html(text)) } @@ -195,13 +241,29 @@ fn logout(mut cookies: Cookies) -> Result { } #[get("/users")] -fn get_users(_token: AdminToken, conn: DbConn) -> JsonResult { +fn get_users_json(_token: AdminToken, conn: DbConn) -> JsonResult { let users = User::get_all(&conn); let users_json: Vec = users.iter().map(|u| u.to_json(&conn)).collect(); Ok(Json(Value::Array(users_json))) } +#[get("/users/overview")] +fn users_overview(_token: AdminToken, conn: DbConn) -> ApiResult> { + let users = User::get_all(&conn); + let users_json: Vec = users.iter() + .map(|u| { + let mut usr = u.to_json(&conn); + if let Some(ciphers) = Cipher::count_owned_by_user(&u.uuid, &conn) { + usr["cipher_count"] = json!(ciphers); + }; + usr + }).collect(); + + let text = AdminTemplateData::users(users_json).render()?; + Ok(Html(text)) +} + #[post("/users//delete")] fn delete_user(uuid: String, _token: AdminToken, conn: DbConn) -> EmptyResult { let user = match User::find_by_uuid(&uuid, &conn) { @@ -242,6 +304,50 @@ fn update_revision_users(_token: AdminToken, conn: DbConn) -> EmptyResult { User::update_all_revisions(&conn) } +#[get("/organizations/overview")] +fn organizations_overview(_token: AdminToken, conn: DbConn) -> ApiResult> { + let organizations = Organization::get_all(&conn); + let organizations_json: Vec = organizations.iter().map(|o| o.to_json()).collect(); + + let text = AdminTemplateData::organizations(organizations_json).render()?; + Ok(Html(text)) +} + +#[derive(Deserialize, Serialize, Debug)] +#[allow(non_snake_case)] +pub struct WebVaultVersion { + version: String, +} + +#[get("/diagnostics")] +fn diagnostics(_token: AdminToken, _conn: DbConn) -> ApiResult> { + use std::net::ToSocketAddrs; + use chrono::prelude::*; + use crate::util::read_file_string; + + let vault_version_path = format!("{}/{}", CONFIG.web_vault_folder(), "version.json"); + let vault_version_str = read_file_string(&vault_version_path)?; + let web_vault_version: WebVaultVersion = serde_json::from_str(&vault_version_str)?; + + let github_ips = ("github.com", 0).to_socket_addrs().map(|mut i| i.next()); + let dns_resolved = match github_ips { + Ok(Some(a)) => a.ip().to_string() , + _ => "Could not resolve domain name.".to_string(), + }; + + let dt = Utc::now(); + let server_time = dt.format("%Y-%m-%d %H:%M:%S").to_string(); + + let diagnostics_json = json!({ + "dns_resolved": dns_resolved, + "server_time": server_time, + "web_vault_version": web_vault_version.version, + }); + + let text = AdminTemplateData::diagnostics(diagnostics_json).render()?; + Ok(Html(text)) +} + #[post("/config", data = "")] fn post_config(data: Json, _token: AdminToken) -> EmptyResult { let data: ConfigBuilder = data.into_inner(); diff --git a/src/api/web.rs b/src/api/web.rs index 7f47ae7c..a97a1c96 100644 --- a/src/api/web.rs +++ b/src/api/web.rs @@ -78,6 +78,7 @@ fn static_files(filename: String) -> Result, Error> { match filename.as_ref() { "mail-github.png" => Ok(Content(ContentType::PNG, include_bytes!("../static/images/mail-github.png"))), "logo-gray.png" => Ok(Content(ContentType::PNG, include_bytes!("../static/images/logo-gray.png"))), + "shield-white.png" => Ok(Content(ContentType::PNG, include_bytes!("../static/images/shield-white.png"))), "error-x.svg" => Ok(Content(ContentType::SVG, include_bytes!("../static/images/error-x.svg"))), "hibp.png" => Ok(Content(ContentType::PNG, include_bytes!("../static/images/hibp.png"))), diff --git a/src/config.rs b/src/config.rs index 9434c39a..e5b46d37 100644 --- a/src/config.rs +++ b/src/config.rs @@ -700,7 +700,10 @@ where reg!("admin/base"); reg!("admin/login"); - reg!("admin/page"); + reg!("admin/settings"); + reg!("admin/users"); + reg!("admin/organizations"); + reg!("admin/diagnostics"); // And then load user templates to overwrite the defaults // Use .hbs extension for the files diff --git a/src/db/models/cipher.rs b/src/db/models/cipher.rs index 1e717ca4..94d7d1ec 100644 --- a/src/db/models/cipher.rs +++ b/src/db/models/cipher.rs @@ -355,6 +355,14 @@ impl Cipher { .load::(&**conn).expect("Error loading ciphers") } + pub fn count_owned_by_user(user_uuid: &str, conn: &DbConn) -> Option { + ciphers::table + .filter(ciphers::user_uuid.eq(user_uuid)) + .count() + .first::(&**conn) + .ok() + } + pub fn find_by_org(org_uuid: &str, conn: &DbConn) -> Vec { ciphers::table .filter(ciphers::organization_uuid.eq(org_uuid)) diff --git a/src/db/models/organization.rs b/src/db/models/organization.rs index 442d1969..8ce476c6 100644 --- a/src/db/models/organization.rs +++ b/src/db/models/organization.rs @@ -255,6 +255,10 @@ impl Organization { .first::(&**conn) .ok() } + + pub fn get_all(conn: &DbConn) -> Vec { + organizations::table.load::(&**conn).expect("Error loading organizations") + } } impl UserOrganization { diff --git a/src/static/images/logo-gray.png b/src/static/images/logo-gray.png index b045df54..70658e18 100644 Binary files a/src/static/images/logo-gray.png and b/src/static/images/logo-gray.png differ diff --git a/src/static/images/shield-white.png b/src/static/images/shield-white.png new file mode 100644 index 00000000..3400efe7 Binary files /dev/null and b/src/static/images/shield-white.png differ diff --git a/src/static/templates/admin/base.hbs b/src/static/templates/admin/base.hbs index e3948d06..a7270b9c 100644 --- a/src/static/templates/admin/base.hbs +++ b/src/static/templates/admin/base.hbs @@ -29,16 +29,79 @@ width: 48px; height: 48px; } + + .navbar img { + height: 24px; + width: auto; + } + + -