|  | @ -3,7 +3,6 @@ use serde::de::DeserializeOwned; | 
			
		
	
		
		
			
				
					|  |  | use serde_json::Value; |  |  | use serde_json::Value; | 
			
		
	
		
		
			
				
					|  |  | use std::{env, time::Duration}; |  |  | use std::{env, time::Duration}; | 
			
		
	
		
		
			
				
					|  |  | 
 |  |  | 
 | 
			
		
	
		
		
			
				
					|  |  | 
 |  |  |  | 
			
		
	
		
		
			
				
					|  |  | use rocket::{ |  |  | use rocket::{ | 
			
		
	
		
		
			
				
					|  |  |     http::{Cookie, Cookies, SameSite}, |  |  |     http::{Cookie, Cookies, SameSite}, | 
			
		
	
		
		
			
				
					|  |  |     request::{self, FlashMessage, Form, FromRequest, Outcome, Request}, |  |  |     request::{self, FlashMessage, Form, FromRequest, Outcome, Request}, | 
			
		
	
	
		
		
			
				
					|  | @ -19,7 +18,7 @@ use crate::{ | 
			
		
	
		
		
			
				
					|  |  |     db::{backup_database, get_sql_server_version, models::*, DbConn, DbConnType}, |  |  |     db::{backup_database, get_sql_server_version, models::*, DbConn, DbConnType}, | 
			
		
	
		
		
			
				
					|  |  |     error::{Error, MapResult}, |  |  |     error::{Error, MapResult}, | 
			
		
	
		
		
			
				
					|  |  |     mail, |  |  |     mail, | 
			
		
	
		
		
			
				
					
					|  |  |     util::{format_naive_datetime_local, get_display_size, is_running_in_docker, get_reqwest_client}, |  |  |     util::{format_naive_datetime_local, get_display_size, get_reqwest_client, is_running_in_docker}, | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					|  |  |     CONFIG, |  |  |     CONFIG, | 
			
		
	
		
		
			
				
					|  |  | }; |  |  | }; | 
			
		
	
		
		
			
				
					|  |  | 
 |  |  | 
 | 
			
		
	
	
		
		
			
				
					|  | @ -64,11 +63,8 @@ static DB_TYPE: Lazy<&str> = Lazy::new(|| { | 
			
		
	
		
		
			
				
					|  |  |         .unwrap_or("Unknown") |  |  |         .unwrap_or("Unknown") | 
			
		
	
		
		
			
				
					|  |  | }); |  |  | }); | 
			
		
	
		
		
			
				
					|  |  | 
 |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  | static CAN_BACKUP: Lazy<bool> = Lazy::new(|| { |  |  | static CAN_BACKUP: Lazy<bool> = | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |     DbConnType::from_url(&CONFIG.database_url()) |  |  |     Lazy::new(|| DbConnType::from_url(&CONFIG.database_url()).map(|t| t == DbConnType::sqlite).unwrap_or(false)); | 
			
				
				
			
		
	
		
		
			
				
					|  |  |         .map(|t| t == DbConnType::sqlite) |  |  |  | 
			
		
	
		
		
			
				
					|  |  |         .unwrap_or(false) |  |  |  | 
			
		
	
		
		
			
				
					|  |  | }); |  |  |  | 
			
		
	
		
		
	
		
		
	
		
		
			
				
					|  |  | 
 |  |  | 
 | 
			
		
	
		
		
			
				
					|  |  | #[get("/")] |  |  | #[get("/")] | 
			
		
	
		
		
			
				
					|  |  | fn admin_disabled() -> &'static str { |  |  | fn admin_disabled() -> &'static str { | 
			
		
	
	
		
		
			
				
					|  | @ -141,7 +137,12 @@ fn admin_url(referer: Referer) -> String { | 
			
		
	
		
		
			
				
					|  |  | fn admin_login(flash: Option<FlashMessage>) -> ApiResult<Html<String>> { |  |  | fn admin_login(flash: Option<FlashMessage>) -> ApiResult<Html<String>> { | 
			
		
	
		
		
			
				
					|  |  |     // If there is an error, show it
 |  |  |     // If there is an error, show it
 | 
			
		
	
		
		
			
				
					|  |  |     let msg = flash.map(|msg| format!("{}: {}", msg.name(), msg.msg())); |  |  |     let msg = flash.map(|msg| format!("{}: {}", msg.name(), msg.msg())); | 
			
		
	
		
		
			
				
					
					|  |  |     let json = json!({"page_content": "admin/login", "version": VERSION, "error": msg, "urlpath": CONFIG.domain_path()}); |  |  |     let json = json!({ | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					|  |  |  |  |  |         "page_content": "admin/login", | 
			
		
	
		
		
			
				
					|  |  |  |  |  |         "version": VERSION, | 
			
		
	
		
		
			
				
					|  |  |  |  |  |         "error": msg, | 
			
		
	
		
		
			
				
					|  |  |  |  |  |         "urlpath": CONFIG.domain_path() | 
			
		
	
		
		
			
				
					|  |  |  |  |  |     }); | 
			
		
	
		
		
			
				
					|  |  | 
 |  |  | 
 | 
			
		
	
		
		
			
				
					|  |  |     // Return the page
 |  |  |     // Return the page
 | 
			
		
	
		
		
			
				
					|  |  |     let text = CONFIG.render_template(BASE_TEMPLATE, &json)?; |  |  |     let text = CONFIG.render_template(BASE_TEMPLATE, &json)?; | 
			
		
	
	
		
		
			
				
					|  | @ -165,10 +166,7 @@ fn post_admin_login( | 
			
		
	
		
		
			
				
					|  |  |     // If the token is invalid, redirect to login page
 |  |  |     // If the token is invalid, redirect to login page
 | 
			
		
	
		
		
			
				
					|  |  |     if !_validate_token(&data.token) { |  |  |     if !_validate_token(&data.token) { | 
			
		
	
		
		
			
				
					|  |  |         error!("Invalid admin token. IP: {}", ip.ip); |  |  |         error!("Invalid admin token. IP: {}", ip.ip); | 
			
		
	
		
		
			
				
					
					|  |  |         Err(Flash::error( |  |  |         Err(Flash::error(Redirect::to(admin_url(referer)), "Invalid admin token, please try again.")) | 
			
				
				
			
		
	
		
		
			
				
					|  |  |             Redirect::to(admin_url(referer)), |  |  |  | 
			
		
	
		
		
			
				
					|  |  |             "Invalid admin token, please try again.", |  |  |  | 
			
		
	
		
		
			
				
					|  |  |         )) |  |  |  | 
			
		
	
		
		
	
		
		
			
				
					|  |  |     } else { |  |  |     } else { | 
			
		
	
		
		
			
				
					|  |  |         // If the token received is valid, generate JWT and save it as a cookie
 |  |  |         // If the token received is valid, generate JWT and save it as a cookie
 | 
			
		
	
		
		
			
				
					|  |  |         let claims = generate_admin_claims(); |  |  |         let claims = generate_admin_claims(); | 
			
		
	
	
		
		
			
				
					|  | @ -328,7 +326,8 @@ fn get_users_json(_token: AdminToken, conn: DbConn) -> Json<Value> { | 
			
		
	
		
		
			
				
					|  |  | fn users_overview(_token: AdminToken, conn: DbConn) -> ApiResult<Html<String>> { |  |  | fn users_overview(_token: AdminToken, conn: DbConn) -> ApiResult<Html<String>> { | 
			
		
	
		
		
			
				
					|  |  |     let users = User::get_all(&conn); |  |  |     let users = User::get_all(&conn); | 
			
		
	
		
		
			
				
					|  |  |     let dt_fmt = "%Y-%m-%d %H:%M:%S %Z"; |  |  |     let dt_fmt = "%Y-%m-%d %H:%M:%S %Z"; | 
			
		
	
		
		
			
				
					
					|  |  |     let users_json: Vec<Value> = users.iter() |  |  |     let users_json: Vec<Value> = users | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					|  |  |  |  |  |         .iter() | 
			
		
	
		
		
			
				
					|  |  |         .map(|u| { |  |  |         .map(|u| { | 
			
		
	
		
		
			
				
					|  |  |             let mut usr = u.to_json(&conn); |  |  |             let mut usr = u.to_json(&conn); | 
			
		
	
		
		
			
				
					|  |  |             usr["cipher_count"] = json!(Cipher::count_owned_by_user(&u.uuid, &conn)); |  |  |             usr["cipher_count"] = json!(Cipher::count_owned_by_user(&u.uuid, &conn)); | 
			
		
	
	
		
		
			
				
					|  | @ -338,7 +337,7 @@ fn users_overview(_token: AdminToken, conn: DbConn) -> ApiResult<Html<String>> { | 
			
		
	
		
		
			
				
					|  |  |             usr["created_at"] = json!(format_naive_datetime_local(&u.created_at, dt_fmt)); |  |  |             usr["created_at"] = json!(format_naive_datetime_local(&u.created_at, dt_fmt)); | 
			
		
	
		
		
			
				
					|  |  |             usr["last_active"] = match u.last_active(&conn) { |  |  |             usr["last_active"] = match u.last_active(&conn) { | 
			
		
	
		
		
			
				
					|  |  |                 Some(dt) => json!(format_naive_datetime_local(&dt, dt_fmt)), |  |  |                 Some(dt) => json!(format_naive_datetime_local(&dt, dt_fmt)), | 
			
		
	
		
		
			
				
					
					|  |  |                 None => json!("Never") |  |  |                 None => json!("Never"), | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					|  |  |             }; |  |  |             }; | 
			
		
	
		
		
			
				
					|  |  |             usr |  |  |             usr | 
			
		
	
		
		
			
				
					|  |  |         }) |  |  |         }) | 
			
		
	
	
		
		
			
				
					|  | @ -423,7 +422,6 @@ fn update_user_org_type(data: Json<UserOrgTypeData>, _token: AdminToken, conn: D | 
			
		
	
		
		
			
				
					|  |  |     user_to_edit.save(&conn) |  |  |     user_to_edit.save(&conn) | 
			
		
	
		
		
			
				
					|  |  | } |  |  | } | 
			
		
	
		
		
			
				
					|  |  | 
 |  |  | 
 | 
			
		
	
		
		
			
				
					|  |  | 
 |  |  |  | 
			
		
	
		
		
			
				
					|  |  | #[post("/users/update_revision")] |  |  | #[post("/users/update_revision")] | 
			
		
	
		
		
			
				
					|  |  | fn update_revision_users(_token: AdminToken, conn: DbConn) -> EmptyResult { |  |  | fn update_revision_users(_token: AdminToken, conn: DbConn) -> EmptyResult { | 
			
		
	
		
		
			
				
					|  |  |     User::update_all_revisions(&conn) |  |  |     User::update_all_revisions(&conn) | 
			
		
	
	
		
		
			
				
					|  | @ -432,7 +430,8 @@ fn update_revision_users(_token: AdminToken, conn: DbConn) -> EmptyResult { | 
			
		
	
		
		
			
				
					|  |  | #[get("/organizations/overview")] |  |  | #[get("/organizations/overview")] | 
			
		
	
		
		
			
				
					|  |  | fn organizations_overview(_token: AdminToken, conn: DbConn) -> ApiResult<Html<String>> { |  |  | fn organizations_overview(_token: AdminToken, conn: DbConn) -> ApiResult<Html<String>> { | 
			
		
	
		
		
			
				
					|  |  |     let organizations = Organization::get_all(&conn); |  |  |     let organizations = Organization::get_all(&conn); | 
			
		
	
		
		
			
				
					
					|  |  |     let organizations_json: Vec<Value> = organizations.iter() |  |  |     let organizations_json: Vec<Value> = organizations | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					|  |  |  |  |  |         .iter() | 
			
		
	
		
		
			
				
					|  |  |         .map(|o| { |  |  |         .map(|o| { | 
			
		
	
		
		
			
				
					|  |  |             let mut org = o.to_json(); |  |  |             let mut org = o.to_json(); | 
			
		
	
		
		
			
				
					|  |  |             org["user_count"] = json!(UserOrganization::count_by_org(&o.uuid, &conn)); |  |  |             org["user_count"] = json!(UserOrganization::count_by_org(&o.uuid, &conn)); | 
			
		
	
	
		
		
			
				
					|  | @ -471,22 +470,13 @@ struct GitCommit { | 
			
		
	
		
		
			
				
					|  |  | fn get_github_api<T: DeserializeOwned>(url: &str) -> Result<T, Error> { |  |  | fn get_github_api<T: DeserializeOwned>(url: &str) -> Result<T, Error> { | 
			
		
	
		
		
			
				
					|  |  |     let github_api = get_reqwest_client(); |  |  |     let github_api = get_reqwest_client(); | 
			
		
	
		
		
			
				
					|  |  | 
 |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |     Ok(github_api |  |  |     Ok(github_api.get(url).timeout(Duration::from_secs(10)).send()?.error_for_status()?.json::<T>()?) | 
			
				
				
			
		
	
		
		
			
				
					|  |  |         .get(url) |  |  |  | 
			
		
	
		
		
			
				
					|  |  |         .timeout(Duration::from_secs(10)) |  |  |  | 
			
		
	
		
		
			
				
					|  |  |         .send()? |  |  |  | 
			
		
	
		
		
			
				
					|  |  |         .error_for_status()? |  |  |  | 
			
		
	
		
		
			
				
					|  |  |         .json::<T>()?) |  |  |  | 
			
		
	
		
		
	
		
		
			
				
					|  |  | } |  |  | } | 
			
		
	
		
		
			
				
					|  |  | 
 |  |  | 
 | 
			
		
	
		
		
			
				
					|  |  | fn has_http_access() -> bool { |  |  | fn has_http_access() -> bool { | 
			
		
	
		
		
			
				
					|  |  |     let http_access = get_reqwest_client(); |  |  |     let http_access = get_reqwest_client(); | 
			
		
	
		
		
			
				
					|  |  | 
 |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |     match http_access |  |  |     match http_access.head("https://github.com/dani-garcia/bitwarden_rs").timeout(Duration::from_secs(10)).send() { | 
			
				
				
			
		
	
		
		
			
				
					|  |  |         .head("https://github.com/dani-garcia/bitwarden_rs") |  |  |  | 
			
		
	
		
		
			
				
					|  |  |         .timeout(Duration::from_secs(10)) |  |  |  | 
			
		
	
		
		
			
				
					|  |  |         .send() |  |  |  | 
			
		
	
		
		
			
				
					|  |  |     { |  |  |  | 
			
		
	
		
		
	
		
		
			
				
					|  |  |         Ok(r) => r.status().is_success(), |  |  |         Ok(r) => r.status().is_success(), | 
			
		
	
		
		
			
				
					|  |  |         _ => false, |  |  |         _ => false, | 
			
		
	
		
		
			
				
					|  |  |     } |  |  |     } | 
			
		
	
	
		
		
			
				
					|  | @ -499,15 +489,14 @@ fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> ApiResu | 
			
		
	
		
		
			
				
					|  |  |     use std::net::ToSocketAddrs; |  |  |     use std::net::ToSocketAddrs; | 
			
		
	
		
		
			
				
					|  |  | 
 |  |  | 
 | 
			
		
	
		
		
			
				
					|  |  |     // Get current running versions
 |  |  |     // Get current running versions
 | 
			
		
	
		
		
			
				
					
					|  |  |     let web_vault_version: WebVaultVersion = match read_file_string(&format!("{}/{}", CONFIG.web_vault_folder(), "bwrs-version.json")) { |  |  |     let web_vault_version: WebVaultVersion = | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					|  |  |  |  |  |         match read_file_string(&format!("{}/{}", CONFIG.web_vault_folder(), "bwrs-version.json")) { | 
			
		
	
		
		
			
				
					|  |  |             Ok(s) => serde_json::from_str(&s)?, |  |  |             Ok(s) => serde_json::from_str(&s)?, | 
			
		
	
		
		
			
				
					
					|  |  |         _ => { |  |  |             _ => match read_file_string(&format!("{}/{}", CONFIG.web_vault_folder(), "version.json")) { | 
			
				
				
			
		
	
		
		
			
				
					|  |  |             match read_file_string(&format!("{}/{}", CONFIG.web_vault_folder(), "version.json")) { |  |  |  | 
			
		
	
		
		
	
		
		
			
				
					|  |  |                 Ok(s) => serde_json::from_str(&s)?, |  |  |                 Ok(s) => serde_json::from_str(&s)?, | 
			
		
	
		
		
			
				
					
					|  |  |                 _ => { |  |  |                 _ => WebVaultVersion { | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |                     WebVaultVersion{version: String::from("Version file missing")} |  |  |                     version: String::from("Version file missing"), | 
			
				
				
			
		
	
		
		
	
		
		
	
		
		
			
				
					|  |  |                 }, |  |  |                 }, | 
			
		
	
		
		
			
				
					|  |  |             } |  |  |  | 
			
		
	
		
		
			
				
					|  |  |             }, |  |  |             }, | 
			
		
	
		
		
			
				
					|  |  |         }; |  |  |         }; | 
			
		
	
		
		
			
				
					|  |  | 
 |  |  | 
 | 
			
		
	
	
		
		
			
				
					|  | @ -529,7 +518,8 @@ fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> ApiResu | 
			
		
	
		
		
			
				
					|  |  |     // TODO: Maybe we need to cache this using a LazyStatic or something. Github only allows 60 requests per hour, and we use 3 here already.
 |  |  |     // TODO: Maybe we need to cache this using a LazyStatic or something. Github only allows 60 requests per hour, and we use 3 here already.
 | 
			
		
	
		
		
			
				
					|  |  |     let (latest_release, latest_commit, latest_web_build) = if has_http_access { |  |  |     let (latest_release, latest_commit, latest_web_build) = if has_http_access { | 
			
		
	
		
		
			
				
					|  |  |         ( |  |  |         ( | 
			
		
	
		
		
			
				
					
					|  |  |             match get_github_api::<GitRelease>("https://api.github.com/repos/dani-garcia/bitwarden_rs/releases/latest") { |  |  |             match get_github_api::<GitRelease>("https://api.github.com/repos/dani-garcia/bitwarden_rs/releases/latest") | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					|  |  |  |  |  |             { | 
			
		
	
		
		
			
				
					|  |  |                 Ok(r) => r.tag_name, |  |  |                 Ok(r) => r.tag_name, | 
			
		
	
		
		
			
				
					|  |  |                 _ => "-".to_string(), |  |  |                 _ => "-".to_string(), | 
			
		
	
		
		
			
				
					|  |  |             }, |  |  |             }, | 
			
		
	
	
		
		
			
				
					|  | @ -545,7 +535,9 @@ fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> ApiResu | 
			
		
	
		
		
			
				
					|  |  |             if running_within_docker { |  |  |             if running_within_docker { | 
			
		
	
		
		
			
				
					|  |  |                 "-".to_string() |  |  |                 "-".to_string() | 
			
		
	
		
		
			
				
					|  |  |             } else { |  |  |             } else { | 
			
		
	
		
		
			
				
					
					|  |  |                 match get_github_api::<GitRelease>("https://api.github.com/repos/dani-garcia/bw_web_builds/releases/latest") { |  |  |                 match get_github_api::<GitRelease>( | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					|  |  |  |  |  |                     "https://api.github.com/repos/dani-garcia/bw_web_builds/releases/latest", | 
			
		
	
		
		
			
				
					|  |  |  |  |  |                 ) { | 
			
		
	
		
		
			
				
					|  |  |                     Ok(r) => r.tag_name.trim_start_matches('v').to_string(), |  |  |                     Ok(r) => r.tag_name.trim_start_matches('v').to_string(), | 
			
		
	
		
		
			
				
					|  |  |                     _ => "-".to_string(), |  |  |                     _ => "-".to_string(), | 
			
		
	
		
		
			
				
					|  |  |                 } |  |  |                 } | 
			
		
	
	
		
		
			
				
					|  | @ -557,7 +549,7 @@ fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> ApiResu | 
			
		
	
		
		
			
				
					|  |  | 
 |  |  | 
 | 
			
		
	
		
		
			
				
					|  |  |     let ip_header_name = match &ip_header.0 { |  |  |     let ip_header_name = match &ip_header.0 { | 
			
		
	
		
		
			
				
					|  |  |         Some(h) => h, |  |  |         Some(h) => h, | 
			
		
	
		
		
			
				
					
					|  |  |         _ => "" |  |  |         _ => "", | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					|  |  |     }; |  |  |     }; | 
			
		
	
		
		
			
				
					|  |  | 
 |  |  | 
 | 
			
		
	
		
		
			
				
					|  |  |     let diagnostics_json = json!({ |  |  |     let diagnostics_json = json!({ | 
			
		
	
	
		
		
			
				
					|  | 
 |