// /*! Zterton */ // Copyright 2021, Jesús Pérez Lorenzo // #![allow(clippy::needless_lifetimes)] use std::collections::HashMap; use std::fmt; use std::str::from_utf8; use casbin::prelude::*; use tera::Tera; use app_env::{ AppStore, appenv::AppEnv, config::{Config,WebServer}, module::Module, appdata::AppData }; use app_tools::{hash_from_data,read_path_file}; use app_auth::{ AuthStore, UserCtx, BEARER_PREFIX, AuthError, LoginRequest, }; use warp::{ http::{header::AUTHORIZATION,method::Method, HeaderMap, HeaderValue}, // Filter, }; // use salvo::{Request}; use app_errors::AppError; use std::result::Result; // use std::io::Result; /// `ReqTasks` is a facilitator container for http request parsing and response composition /// Manage info from `AppData` from env, vault, tera, ctx, etc. /// Evaluate token, session state /// Control some authorizations and permissions #[derive(Clone)] pub struct ReqTasks { /// AppData object pub app_data: AppData, // pub app_data: &'a AppData, /// Auth object pub auth_store: AuthStore, // pub auth_store: &'a AuthStore, /// Request objet pub header: HeaderMap, pub method: Method, pub path: String, /// It will allow to take decisions based in origin or source context. pub origin: String, pub key_module: String, } impl fmt::Display for ReqTasks { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{} {} {}", &self.path, &self.origin, &self.key_module) } } impl ReqTasks { #[must_use] pub fn new( app_db: AppStore, auth_store: AuthStore, header: HeaderMap, method: Method, path: &str, origin: &str, key_module: &str ) -> ReqTasks { let app_data = app_db.app_data.read(); // let auth_store: &'a AuthStore = &AuthStore { // users: auth_db.users.clone(), // sessions: auth_db.sessions.clone(), // enforcer: auth_db.enforcer.clone(), // }; ReqTasks { app_data: app_data.to_owned(), auth_store: auth_store.to_owned(), header, method, path: path.to_string(), origin: origin.to_string(), key_module: key_module.to_string(), } } /// Get `AppEnv` #[must_use] pub fn env(&self) -> AppEnv { self.app_data.env.to_owned() // self.app_data.env.lock().await.to_owned() } /// Get Tera #[must_use] pub fn tera(&self) -> tera::Tera { self.app_data.tera.to_owned() } // /// Get Vault // pub fn vault(&self) -> ServerVault { // self.app_data.vault.to_owned() // } /// Get Context (ctx) #[must_use] pub fn ctx(&self) -> tera::Context { self.app_data.ctx.to_owned() } /// Get `AppEnv` Config #[must_use] pub fn config(&self) -> Config { self.app_data.env.config.to_owned() } /// Get `AppEnv` Config #[must_use] pub fn websrvr(&self) -> WebServer { self.app_data.env.config.websrvrs[self.app_data.env.curr_web].to_owned() } #[must_use] pub fn module(&self) -> Module { self.app_data.env.get_module(&self.key_module) } /// Get `AppEnv` Config // #[must_use] // pub fn params(&self) -> HashMap { // self.req // .uri() // .query() // .map_or_else(HashMap::new, |v| { // url::form_urlencoded::parse(v.as_bytes()) // .into_owned() // .collect() // }) // } /// Get Lang list from header - accept-language /// get first one but only root part as first two characters (es in case of es-ES) /// if lang is not in `WebServer.langs`it will fallback to `Config.default_lang` #[must_use] pub fn lang(&self) -> String { #[allow(unused_assignments)] let mut lang = self.config().default_lang; let default_lang = self.config().default_lang; let langs = self.websrvr().langs; // As fallback set default_lang lang = default_lang; // Get langs list from header if let Some(languages) = self.header.get("accept-language") { match languages.to_str() { Ok(list_lngs) => { // Get only first one if let Some(full_lng) = list_lngs.split(',').collect::>().first() { // Get root_lng part let mut end: usize = 0; full_lng .chars() .into_iter() .take(2) .for_each(|x| end += x.len_utf8()); let root_lng = &full_lng[..end]; // Filter to check if is in `Config.langs` let list: Vec = langs .iter() .filter(|lng| lng.as_str() == root_lng) .cloned() .collect(); if let Some(lng) = list.get(0) { // Set if it is found and available lang = lng.to_owned(); } } } Err(e) => println!("Error: {}", e), } } // println!("Lang: {}",&lang); lang } /// Get `Schema` Config pub async fn is_admin(&self, ky: String) -> bool { self.env().appkey == ky } /// Render a template with context and load `data_path` from `root_path` in case is not empty /// it use a `data_hash` for context with data if it is not empty /// if errors it will render a `fallback_template` #[allow(clippy::too_many_arguments,clippy::missing_errors_doc)] pub async fn render_template( &self, ctx: &mut tera::Context, root_path: &str, template_name: &str, fallback_template: &str, data_path: &str, data_hash: &mut HashMap, app_module: &str, ) -> Result { let tera: &mut Tera = &mut self.tera(); //.to_owned(); if ! app_module.is_empty() { AppData::load_modules(&self.env(), &app_module,ctx); } Ok(if data_path.is_empty() { tera .render(&template_name, &ctx) .map_err(|e| AppError::ErrorInternalServerError(format!("Error: {}", e)))? } else { let render_fallback = |e| -> Result { println!("Error parsing data {}:\n{}", &data_path, e); Ok(tera .render(fallback_template, &tera::Context::new()) .map_err(|e| AppError::ErrorInternalServerError(format!("Template error {}",e)))?) }; match hash_from_data(format!("{}/{}", &root_path, &data_path).as_str(), ctx, data_hash, true) { Ok(_) => { let mut need_reload = false; for (key, value) in data_hash.iter() { if key.as_str() == "need_reload" { need_reload = true; } else { ctx.insert(key, value); } } if need_reload { match read_path_file(&root_path, &template_name, "content") { Ok(tpl) => { println!("reload {}",&template_name); tera.add_raw_templates(vec![(&template_name, tpl)]) .map_err(|e| AppError::ErrorInternalServerError(format!("Error reload: {}", e)))? }, Err(e) => return render_fallback(e), }; } tera .render(&template_name, &ctx) .map_err(|e| AppError::ErrorInternalServerError(format!("Error render: {}", e)))? } Err(e) => { println!("Error parsing data {}:\n{}", &data_path, e); return render_fallback(e); } } }) } #[allow(clippy::too_many_arguments,clippy::missing_errors_doc)] pub async fn render_page( &self, ctx: &mut tera::Context, root_path: &str, template_name: &str, fallback_template: &str, data_path: &str, data_hash: &mut HashMap, app_module: &str, ) -> Result { let tera = &self.tera(); let res = match self .render_template( ctx, root_path, template_name, fallback_template, data_path, data_hash, app_module, ) .await { Ok(res) => res, Err(e) => { println!( "Error render template + data {} + {}:\n{}", &template_name, &data_path, e ); tera .render(&fallback_template, &tera::Context::new()) .map_err(|e| AppError::ErrorInternalServerError(format!("Template error {}",e)))? } }; // Ok(HttpResponse::Ok().content_type("text/html").body(res)) Ok(res) } #[allow(clippy::missing_errors_doc)] pub async fn render_to( &self, template_name: &str, data_path: &str, data_hash: &mut HashMap, app_module: &str, ) -> Result { let app_env = &self.env(); // From Default Context let mut ctx = self.ctx().to_owned(); let fallback_template = "index.html"; self .render_page( &mut ctx, app_env.get_curr_websrvr_config().templates_path.as_str(), template_name, fallback_template, data_path, data_hash, app_module, ) .await } #[allow(clippy::missing_errors_doc)] pub fn token_from_header(&self) -> anyhow::Result { if let Some(header) = self.header.get(AUTHORIZATION) { let auth_header = match from_utf8(header.as_bytes()) { Ok(v) => v, Err(_) => return Err(AuthError::NoAuthHeaderFoundError.into()), }; if !auth_header.starts_with(BEARER_PREFIX) { return Err(AuthError::InvalidAuthHeaderFormatError.into()); } let without_prefix = auth_header.trim_start_matches(BEARER_PREFIX); Ok(without_prefix.to_owned()) } else { Err(AuthError::NoAuthHeaderFoundError.into()) } } #[allow(clippy::missing_errors_doc)] pub async fn token_session(&self, login: &LoginRequest) -> anyhow::Result { // dbg!("{}",login); if let Some(user) = &self.auth_store.users .read() .await .iter() .find(|(_, v)| v.name == *login.name) // .filter(|(_, v)| *v.name == name) // .nth(0) { let shdw = &self.auth_store.shadows.read().await; if let Some(usr_shdw) = shdw.get(&String::from(user.0)) { // println!("{}",&usr_shdw.passwd); // println!("{}",&login.passwd); // println!("{}",&user.0); if usr_shdw.passwd == login.passwd { // println!("OK "); let token = uuid::Uuid::new_v4().to_string(); self.auth_store.sessions.write().await.insert(token.clone(), String::from(user.0)); return Ok(token); } } Err(AuthError::UserNotFoundError.into()) //shdw.get(usr_key); } else { Err(AuthError::UserNotFoundError.into()) } } #[allow(clippy::missing_errors_doc)] pub async fn user_authentication(&self) -> anyhow::Result { let token = self.token_from_header().unwrap_or_else(|e| { println!("{}",e); String::from("")}); self.check_authentication(token).await } #[allow(clippy::missing_errors_doc)] pub async fn check_authentication(&self, token: String) -> anyhow::Result { if let Some(user_id) = self.auth_store.sessions.read().await.get(&token) { if let Some(user) = self.auth_store.users.read().await.get(user_id) { // println!("{} - {} - {}",&user.role.as_str(), &self.path.as_str(), &self.method.as_str()); match self.auth_store.enforcer.enforce( (&user.role.as_str(), &self.path.as_str(), &self.method.as_str()) ) { Ok(authorized) => { if authorized { Ok(UserCtx { user_id: user.user_id.to_owned(), token }) } else { Err(AuthError::UnauthorizedError.into()) } } Err(e) => { eprintln!("error during authorization: {}", e); Err(AuthError::AuthorizationError.into()) } } } else { Err(AuthError::InvalidTokenError.into()) } } else { Err(AuthError::InvalidTokenError.into()) } } }