391 lines
12 KiB
Rust
391 lines
12 KiB
Rust
//
|
|
/*! 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<HeaderValue>,
|
|
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<HeaderValue>,
|
|
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<String,String> {
|
|
// 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::<Vec<_>>().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<String> = 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<String, String>,
|
|
app_module: &str,
|
|
) -> Result<String,AppError> {
|
|
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<String,AppError> {
|
|
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<String, String>,
|
|
app_module: &str,
|
|
) -> Result<String,AppError> {
|
|
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<String, String>,
|
|
app_module: &str,
|
|
) -> Result<String,AppError> {
|
|
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<String> {
|
|
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<String> {
|
|
// 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<UserCtx> {
|
|
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<UserCtx> {
|
|
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())
|
|
}
|
|
}
|
|
}
|