use serde::{Deserialize,Serialize}; use sqlx::Row; use futures::TryStreamExt; use anyhow::{anyhow,Context, Result}; use std::{ fmt, collections::HashMap, }; // use std::{ // // sync::Arc, // fmt::Debug, // // io::Write, // // fs, // // path::{Path, PathBuf}, // // io::{Error, ErrorKind}, // collections::HashMap, // }; // use async_session::{MemoryStore, Session, SessionStore}; //use tiitls_utils::logs::file; // use uuid::Uuid; // SID_UI_FILE, // UI_SETTINGS_FILE, // SidSettings, // Config as SessionsConfig, use std::num::ParseIntError; use crate::{ users::{ entries::{Entries,Entry}, UserData, UserStore, UserStatus, }, USERS_TABLENAME, tools::str_date_from_timestamp, }; const DISPLAY_SEPARATOR: &str = "="; fn default_user_status() -> UserStatus { UserStatus::default() } #[derive(Debug, Clone, PartialEq, PartialOrd, Serialize, Deserialize,Default)] pub struct User { pub id: i64, pub name: String, pub fullname: String, pub email: String, pub description: String, pub password: String, pub otp_enabled: bool, pub otp_verified: bool, pub otp_base32: String, pub otp_auth_url: String, pub otp_defs: String, pub roles: String, pub created: String, pub lastaccess: String, #[serde(default = "default_user_status")] pub status: UserStatus, pub items: String, pub isadmin: bool, } impl fmt::Display for User { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { // Write strictly the first element into the supplied output // stream: `f`. Returns `fmt::Result` which indicates whether the // operation succeeded or failed. Note that `write!` uses syntax which // is very similar to `println!`. let sep = DISPLAY_SEPARATOR; let content = format!( "ID{} {} Name{} {} FullName{} {} Description{} {} Email{} {} Password{} {} otp_enabled{} {} otp_verified{} {} otp_base32{} {} otp_auth_url{} {} otp_defs{} {} Roles{} {} Created{} {} Last access{} {} Status{} {} Items{} {} IsAdmin{} {}", sep, self.id, sep, self.name, sep, self.fullname, sep, self.description, sep, self.email, sep, self.password, sep, self.otp_enabled, sep, self.otp_verified, sep, self.otp_base32, sep, self.otp_auth_url, sep, self.otp_defs, sep, self.roles, sep, self.created, sep, self.lastaccess, sep, self.status, sep, self.items, sep, self.isadmin ); write!(f, "{}", content) } } impl Entry for User { fn from_line(line: &str) -> Result { let parts: Vec<&str> = line.split(":").map(|part| part.trim()).collect(); Ok(User { id: parts[0].to_string().parse::().unwrap_or_default(), name: parts[1].to_string(), fullname: parts[2].to_string(), description: parts[3].to_string(), email: parts[4].to_string(), password: parts[5].to_string(), otp_enabled: if parts[6] == "TRUE" { true } else { false}, otp_verified: if parts[7] == "TRUE" { true } else { false}, otp_base32: parts[8].to_string(), otp_auth_url: parts[9].to_string(), otp_defs: parts[10].to_string(), roles: parts[11].to_string(), created: parts[12].to_string(), lastaccess: parts[13].to_string(), status: UserStatus::from_str(&parts[14].to_string()), items: parts[15].to_string(), isadmin: if parts[16] == "TRUE" { true } else { false}, }) } } impl User { pub async fn add(self, store: &UserStore) -> Result { match store { UserStore::Sql(pool) => { let query_result = sqlx::query( format!("INSERT INTO {} ( name, fullname, email, description, password, otp_enabled, otp_verified, otp_base32, otp_auth_url, otp_defs, roles, created, lastaccess, status, items, isadmin ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", USERS_TABLENAME).as_str() ) .bind(self.name) .bind(self.fullname) .bind(self.email) .bind(self.description) .bind(self.password) .bind(self.otp_enabled) .bind(self.otp_verified) .bind(self.otp_base32) .bind(self.otp_auth_url) .bind(self.otp_defs) .bind(self.roles) .bind(self.created) .bind(self.lastaccess) .bind(format!("{}",self.status)) .bind(self.items) .bind(self.isadmin) .execute(pool).await?; Ok(query_result.last_insert_id().unwrap_or_default()) }, UserStore::File(file_path) => { // use itertools::Itertools; // let entries: Vec = concat(vec![ // Entries::new(Path::new(&file_path)).map(|user: User|{ // user.line_format() // }).collect(), // vec![ self.line_format()] // ]); // Entries::::write(Path::new(&file_path), &entries); let all: Vec = Entries::new(&file_path).collect(); let id = if all.len() > 0 { all[all.len()-1].id + 1 } else { 1 }; let mut new_user = self.to_owned(); new_user.id = id; let entries: Entries = Entries::new(&file_path); match entries.append(new_user.line_format()) { Ok(_) => Ok(id), Err(e) => { println!("Error add item to file: {}",e); Err(anyhow!("No data added")).context("User add") } } }, } } pub async fn select(field: &str, value: &str, human: bool, store: &UserStore) -> Result { match store { UserStore::Sql(pool) => { let query_str = format!("SELECT * FROM {} WHERE {} = ? ", USERS_TABLENAME, field); let mut stream = sqlx::query( &query_str ) .bind(value) //.map(|row: PgRow| { // map the row into a user-defined domain type //}) .fetch(pool); if let Some(row) = stream.try_next().await? { let str_status: String = row.try_get("status")?; let status = UserStatus::from_str(&str_status); let created = if human { let created: String = row.try_get("created")?; str_date_from_timestamp(&created) } else { row.try_get("created")? }; let lastaccess = if human { let lastaccess: String = row.try_get("lastaccess")?; str_date_from_timestamp(&lastaccess) } else { row.try_get("lastaccess")? }; Ok(Self{ id: row.try_get("id")?, name: row.try_get("name")?, fullname: row.try_get("fullname")?, email: row.try_get("email")?, description: row.try_get("description")?, password: row.try_get("password")?, otp_enabled: row.try_get("otp_enabled")?, otp_verified: row.try_get("otp_verified")?, otp_base32: row.try_get("otp_base32")?, otp_auth_url: row.try_get("otp_auth_url")?, otp_defs: row.try_get("otp_defs")?, roles: row.try_get("roles")?, created, lastaccess, status, items: row.try_get("items")?, isadmin: row.try_get("isadmin")?, }) } else { Err(anyhow!("No data found")).context("User select") } }, UserStore::File(file_path) => { if let Some(user) = Entries::::new(&file_path).find(|it| match field { "id" => it.id == value.parse::().unwrap_or_default(), "name" => it.name == value, "fullname" => it.fullname == value, "email" => it.email == value, "description" => it.description == value, "otp_base32" => it.otp_base32 == value, "roles" => it.roles == value, "items" => it.items == value, "isadmin" => match value { "TRUE" => it.isadmin, _ => !it.isadmin, }, _ => false, } ) { Ok(user) } else { Err(anyhow!("No data found")).context("User select") } }, } } pub async fn delete(id: i64, store: &UserStore) -> Result { match store { UserStore::Sql(pool) => { let query_str = format!("DELETE FROM {} WHERE id = ?", USERS_TABLENAME); let query_result = sqlx::query( &query_str ) .bind(id) .execute(pool).await?; Ok(query_result.rows_affected() > 0) }, UserStore::File(file_path) => { let new_entries: Vec = Entries::::new(&file_path).filter(|it| it.id != id).map(|user| user.line_format() ).collect(); let entries = Entries::::new(&file_path); match entries.write(&new_entries) { Ok(_) => Ok(true), Err(e) => { println!("Error data delete '{}': {}", id, e); Err(anyhow!("No data delete").context("User delete")) } } }, } } pub async fn update(self, store: &UserStore) -> anyhow::Result { match store { UserStore::Sql(pool) => { let query_str = format!("UPDATE {} SET name = ?, fullname = ?, email = ?, description = ?, password = ?, otp_enabled = ?, otp_verified = ?, otp_base32 = ?, otp_auth_url = ?, otp_defs = ?, roles = ?, created = ?, lastaccess = ?, status = ?, items = ?, isadmin = ? WHERE id = ? ", USERS_TABLENAME ); let query_result = sqlx::query( &query_str ) .bind(self.name) .bind(self.fullname) .bind(self.email) .bind(self.description) .bind(self.password) .bind(self.otp_enabled) .bind(self.otp_verified) .bind(self.otp_base32) .bind(self.otp_auth_url) .bind(self.otp_defs) .bind(self.roles) .bind(self.created) .bind(self.lastaccess) .bind(format!("{}",self.status)) .bind(self.items) .bind(self.isadmin) .bind(self.id) .execute(pool).await?; Ok(query_result.rows_affected() > 0) }, UserStore::File(file_path) => { let new_entries: Vec = Entries::new(&file_path).map(|user: User|{ if user.id == self.id { self.to_owned().line_format() } else { user.line_format() } }).collect(); let entries = Entries::::new(&file_path); match entries.write(&new_entries) { Ok(_) => Ok(true), Err(e) => { println!("Error data update '{}': {}", self.id, e); Err(anyhow!("No data updated").context("User update")) } } }, } } pub fn show(&self, sep: &str) { let content = if sep.is_empty() { format!( "{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}", self.id, self.name, self.fullname, self.description, self.email, self.password, self.otp_enabled, self.otp_verified, self.otp_base32, self.otp_auth_url, self.otp_defs, self.roles, self.created, self.lastaccess, self.status, self.items, self.isadmin ) } else { format!("{}",&self).replace(DISPLAY_SEPARATOR, sep) }; println!("{}\n_____________________________",content); } pub async fn list(store: &UserStore, human: bool, show: bool, sep: &str) -> anyhow::Result> { let mut usrs: Vec = Vec::new(); match store { UserStore::Sql(pool) => { let query_str = format!("SELECT * FROM {}", USERS_TABLENAME); let mut stream = sqlx::query( &query_str ) //.map(|row: PgRow| { // map the row into a user-defined domain type //}) .fetch(pool); while let Some(row) = stream.try_next().await? { let str_status: String = row.try_get("status")?; let status = UserStatus::from_str(&str_status); let created = if human { let created: String = row.try_get("created")?; str_date_from_timestamp(&created) } else { row.try_get("created")? }; let lastaccess = if human { let lastaccess: String = row.try_get("lastaccess")?; str_date_from_timestamp(&lastaccess) } else { row.try_get("lastaccess")? }; let user = Self{ id: row.try_get("id")?, name: row.try_get("name")?, fullname: row.try_get("fullname")?, email: row.try_get("email")?, description: row.try_get("description")?, password: row.try_get("password")?, otp_enabled: row.try_get("otp_enabled")?, otp_verified: row.try_get("otp_verified")?, otp_base32: row.try_get("otp_base32")?, otp_auth_url: row.try_get("otp_auth_url")?, otp_defs: row.try_get("otp_defs")?, roles: row.try_get("roles")?, created, lastaccess, status, items: row.try_get("items")?, isadmin: row.try_get("isadmin")?, }; if show { user.show(sep); } usrs.push(user); } Ok(usrs) }, UserStore::File(file_path) => { let all: Vec = Entries::new(&file_path).collect(); if show { for user in all.to_owned() { if show { user.show(sep); } } } Ok(all) }, // UserStore::None => Err(anyhow!("No store set")).context("Users list"), } } pub async fn count(store: &UserStore) -> anyhow::Result { match store { UserStore::Sql(pool) => { let query_str = format!("SELECT count(*) as total FROM {}", USERS_TABLENAME); let row = sqlx::query( &query_str ) .fetch_one(pool).await?; let total: i64 = row.try_get("total")?; Ok(total) }, UserStore::File(file_path) => { let all: Vec = Entries::new(&file_path).collect(); Ok(all.len() as i64) }, } } fn line_format(self) -> String { format!( "{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}", self.id, self.name, self.fullname, self.description, self.email, self.password, self.otp_enabled, self.otp_verified, self.otp_base32, self.otp_auth_url, self.otp_defs, self.roles, self.created, self.lastaccess, self.status, self.items, self.isadmin ) } pub fn hash_items(items: &str) -> HashMap { if items.is_empty() { HashMap::new() } else { serde_json::from_str(items).unwrap_or_else(|e|{ println!("Error to convert user items to json: {}",e); HashMap::new() }) } } pub fn items(&self) -> HashMap { Self::hash_items(&self.items) } pub fn json_items(items: HashMap) -> String { serde_json::to_string(&items).unwrap_or_else(|e|{ println!("Error to convert user items to string: {}",e); String::from("") }) } pub fn from_data(&mut self, user_data: UserData) { if !user_data.name.is_empty() { self.name = user_data.name.to_owned(); } if !user_data.fullname.is_empty() { self.fullname = user_data.fullname.to_owned(); } if !user_data.description.is_empty() { self.description = user_data.description.to_owned(); } if !user_data.email.is_empty() { self.email = user_data.email.to_owned(); } if !user_data.otp_code.is_empty() { self.otp_base32 = user_data.otp_code.to_owned(); } if !user_data.otp_url.is_empty() { self.otp_auth_url = user_data.otp_url.to_owned(); } if !user_data.roles.is_empty() { self.roles = user_data.roles.to_owned(); } if !user_data.items.is_empty() { let mut items_hash = self.items(); for (key,val) in user_data.items { items_hash.insert(key,val); } self.items = Self::json_items(items_hash); } } pub fn disable_totp(&mut self) { self.otp_base32 = String::from(""); self.otp_auth_url = String::from(""); self.otp_defs = String::from(""); self.otp_verified = false; self.otp_enabled = false; } pub fn from_user(&mut self, new_user: User) { if !new_user.name.is_empty() { self.name = new_user.name.to_owned(); } if !new_user.fullname.is_empty() { self.fullname = new_user.fullname.to_owned(); } if !new_user.description.is_empty() { self.description = new_user.description.to_owned(); } if !new_user.email.is_empty() { self.email = new_user.email.to_owned(); } if !new_user.password.is_empty() { self.password = new_user.password.to_owned(); } if new_user.otp_enabled { self.disable_totp(); } else { if !new_user.otp_base32.is_empty() { self.otp_base32 = new_user.otp_base32.to_owned(); } if !new_user.otp_auth_url.is_empty() { self.otp_auth_url = new_user.otp_auth_url.to_owned(); } if !new_user.otp_defs.is_empty() { self.otp_defs = new_user.otp_defs.to_owned(); } self.otp_verified = new_user.otp_verified; } if !new_user.roles.is_empty() { self.roles = new_user.roles.to_owned(); } if !new_user.items.is_empty() { self.items = new_user.items.to_owned(); } if new_user.status != self.status { self.status = new_user.status.to_owned(); } } pub fn session_data(&self) -> String { format!("{}|{}|{}|{}|{}|{}|{}", &self.id, &self.name, &self.email, &self.roles, &self.items, &self.isadmin, &self.status ) } pub fn estimate_password(word: &str) -> String { match zxcvbn::zxcvbn(word, &[]) { Ok(estimate) => { if let Some(feedback) = estimate.feedback() { let arr_suggestions: Vec = feedback.suggestions().iter().map(|s| format!("{}",s)).collect(); let suggestions = arr_suggestions.join("\n"); if let Some(warning) = feedback.warning() { let warning = format!("{}", warning); format!("{}|{}|{}",estimate.score(),suggestions,warning) } else { format!("{}|{}|",estimate.score(),suggestions) } } else { format!("{}||",estimate.score()) } }, Err(e) => { println!("Error password strength estimator: {}", e); String::from("-1|||") } } } pub fn password_score(word: &str) -> u8 { match zxcvbn::zxcvbn(word, &[]) { Ok(estimate) => { estimate.score() }, Err(e) => { println!("Error password strength estimator: {}", e); 0 } } } #[allow(dead_code)] pub fn has_auth_role(&self, auth_roles: Vec) -> bool { for role in auth_roles { if self.roles.contains(&role) { return true; } } return false; } }