chore: add reqtasks
This commit is contained in:
parent
bb02cfa772
commit
8fa4ca6fba
10
reqtasks/.gitignore
vendored
Normal file
10
reqtasks/.gitignore
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
/target
|
||||||
|
target
|
||||||
|
Cargo.lock
|
||||||
|
.cache
|
||||||
|
.temp
|
||||||
|
.env
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
|
logs
|
||||||
|
tmp
|
55
reqtasks/Cargo.toml
Normal file
55
reqtasks/Cargo.toml
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
[package]
|
||||||
|
name = "reqtasks"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["JesusPerez <jpl@jesusperez.pro>"]
|
||||||
|
edition = "2018"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1.0.40"
|
||||||
|
base64 = "0.13.0"
|
||||||
|
casbin = "2.0.7"
|
||||||
|
chrono = "0.4"
|
||||||
|
dotenv = "0.15.0"
|
||||||
|
envmnt = "0.9.0"
|
||||||
|
error-chain = "0.12.4"
|
||||||
|
glob = "0.3.0"
|
||||||
|
json = "0.12.4"
|
||||||
|
once_cell = "1.7.2"
|
||||||
|
parking_lot = "0.11.1"
|
||||||
|
rand = "0.8.3"
|
||||||
|
regex = "1.4.3"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_derive = "1.0.125"
|
||||||
|
serde_json = "1.0.64"
|
||||||
|
serde_yaml = "0.8.17"
|
||||||
|
slab = "0.4.3"
|
||||||
|
tempfile = "3.2.0"
|
||||||
|
tera = "1.8.0"
|
||||||
|
thiserror = "1.0.24"
|
||||||
|
toml = "0.5.8"
|
||||||
|
yaml-rust = "0.4"
|
||||||
|
tokio = { version = "1.5.0", features = ["full"] }
|
||||||
|
uuid = { version = "0.8", features = ["serde", "v4"] }
|
||||||
|
url = "2.2.1"
|
||||||
|
warp = { version = "0.3", features = ["default","websocket","tls","compression"] }
|
||||||
|
app_tools = { version = "0.1.0", path = "../../utils/app_tools" }
|
||||||
|
app_env = { version = "0.1.0", path = "../../defs/app_env" }
|
||||||
|
app_auth = { version = "0.1.0", path = "../../defs/app_auth" }
|
||||||
|
app_errors = { version = "0.1.0", path = "../../defs/app_errors" }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
pretty_env_logger = "0.4"
|
||||||
|
tracing-subscriber = "0.2.15"
|
||||||
|
tracing-log = "0.1"
|
||||||
|
serde_derive = "1.0.125"
|
||||||
|
handlebars = "3.0.0"
|
||||||
|
tokio = { version = "1.5.0", features = ["macros", "rt-multi-thread"] }
|
||||||
|
tokio-stream = { version = "0.1.5", features = ["net"] }
|
||||||
|
listenfd = "0.3"
|
||||||
|
envmnt = "0.9.0"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
envmnt = "0.9.0"
|
14
reqtasks/README.md
Normal file
14
reqtasks/README.md
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# ReqTasks handlers library
|
||||||
|
|
||||||
|
Handlers to manage request using [warp](https://github.com/seanmonstar/warp)
|
||||||
|
|
||||||
|
It use one struct **ReqTasks**
|
||||||
|
|
||||||
|
It can be extended or included in other types like <u>reqenv</u>
|
||||||
|
|
||||||
|
## Handlers
|
||||||
|
|
||||||
|
- Authentication
|
||||||
|
- Access: admin, session token.
|
||||||
|
- Render tamplates and pages
|
||||||
|
- Lang definitions
|
9
reqtasks/TODO.md
Normal file
9
reqtasks/TODO.md
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# ReqTasks handlers library
|
||||||
|
|
||||||
|
- [ ] Tests
|
||||||
|
|
||||||
|
- [ ] Manage tokens and certification
|
||||||
|
|
||||||
|
- [ ] Dynamic load profiles, modules, etc
|
||||||
|
|
||||||
|
- [ ] Track sessions, devices, etc. [server timing](https://w3c.github.io/server-timing/)
|
385
reqtasks/src/lib.rs
Normal file
385
reqtasks/src/lib.rs
Normal file
@ -0,0 +1,385 @@
|
|||||||
|
//
|
||||||
|
/*! 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,
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
#[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 `Config.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.config().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.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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user