diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..2b29215 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,79 @@ +[package] +name = "libresignin" +version = "0.1.0" +authors = ["JesusPerez "] +edition = "2021" +description= "Singe Sing On Services for LibreCloud" +license-file = "LICENSE" +publish = false + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[profile.dev] +opt-level = 0 + +[profile.release] +opt-level = 3 + +[dependencies] +anyhow="1.0" +async-session = "3.0" +axum = { version = "0.4", features = ["headers"] } +axum-server = { version = "0.3", features = ["tls-rustls"] } +base64 = "0.13" +bytes = "1.1" +casbin = "2.0" +chrono = "0.4" +dotenv = "0.15" +envmnt = "0.9" +glob = "0.3" +hyper = { version = "0.14", features = ["full"] } +#reqwest = "0.11" + +bcrypt = "0.10" +tower-cookies = { version = "0.5", features = ["signed"] } +cookie = { version = "0.16", features = ["percent-encode"] } + +reqwest-middleware = "0.1" +reqwest-retry = "0.1" +reqwest-tracing = "0.2" + +cookie_store = "0.15" +reqwest_cookie_store = "0.2" +reqwest = { version = "0.11", features = ["rustls-tls","cookies","json"], default-features = false } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +thiserror = "1.0" +tracing = "0.1" +tracing-subscriber = { version="0.3", features = ["env-filter"] } +tokio = { version = "1.16", features = ["full"] } +tower = { version = "0.4", features = ["util", "timeout", "load-shed", "limit"] } +tower-http = { version = "0.2", features = ["fs", "cors", "trace", "add-extension", "auth", "compression-full"] } + +uuid = { version = "0.8", features = ["v4", "serde"] } + +tera = "1.15" + +headers = "0.3" +jsonwebtoken = "8.0" +once_cell = "1.8" + +redis = { version = "0.21", features = [ "tokio-comp", "cluster"] } +redis-graph = { version = "0.4", features = ['tokio-comp'] } +sqlx = {version = "0.5", default-features = false, features = ["macros","runtime-tokio-rustls","sqlite", "mysql", "postgres", "decimal", "chrono"]} +pretty_env_logger = "0.4" + +webenv = { version = "0.1.2", path = "../rust_lib/webenv" } +app_tools = { version = "0.1.0", path = "../rust_lib/utils/app_tools" } +app_env = { version = "0.1.0", path = "../rust_lib/defs/app_env" } +datastores = { version = "0.1.0", path = "../rust_lib/datastores/defs" } +connectors = { version = "0.1.0", path = "../rust_lib/datastores/connectors" } +app_auth = { version = "0.1.0", path = "../rust_lib/defs/app_auth" } +app_errors = { version = "0.1.0", path = "../rust_lib/defs/app_errors" } +# gql_playground = { version = "0.1.0", path = "../rust_lib/graphql/gql_playground" } +key_of_life = { path = "../rust_lib/key_of_life" } + +[dev-dependencies] +pretty_env_logger = "0.4" +tracing-subscriber = "0.3.6" +tracing-log = "0.1" diff --git a/src/defs.rs b/src/defs.rs new file mode 100644 index 0000000..a8b0023 --- /dev/null +++ b/src/defs.rs @@ -0,0 +1,98 @@ +// +/*! libresignin +*/ +// Copyright 2022, Jesús Pérez Lorenzo +// +// use serde::{Serialize, Deserialize}; +// use std::collections::{HashMap, BTreeMap}; +// use app_env::{appenv::AppEnv, AppStore}; +use app_env::{AppStore}; +use app_auth::{AuthStore}; + +use std::sync::Arc; +use tokio::sync::Mutex; +use connectors::defs::{AppDataConn}; + +/* +use kloud::utils::load_from_module; +use clds::clouds::defs::{ + CloudEnv, + Cloud, + SrvcsHostInfOut, + InfoStatus, +}; +use kloud::kloud::Kloud; + +#[derive(Clone,Default)] +pub struct CollsData { + pub klouds: KloudStore, +} + +impl CollsData { + pub fn new(env: AppEnv,verbose: isize) -> Self { + // dbg!(&env.contexts); + let (klouds_frmt, klouds_content) = load_from_module(env.to_owned(),"klouds"); + Self { + klouds: KloudStore::new( + Kloud::entries(&klouds_content,&klouds_frmt), + "klouds".to_owned(), + DataContext::default(), + verbose + ), + } + } + pub async fn get_klouds_entries(coll_map: CollsData) -> BTreeMap { + let mut result = BTreeMap::new(); + let cur = coll_map.klouds.entries.read(); + for (key,value) in cur.iter() { + result.insert(key.to_owned(), value.to_owned()); + } + result + } +} +*/ + +#[derive(Clone)] +pub struct AppDBs { +// pub colls: CollsData, + pub app: AppStore, +} + +#[derive(Clone)] +pub struct DataDBs { +// pub colls: CollsData, + pub app: AppStore, + pub auth: AuthStore, + pub conns: Arc>, +} + +/* +pub async fn load_cloud_env(cloud: &mut Cloud) { + let force: u8 = "-f".as_bytes()[0]; + cloud.env = CloudEnv::new(force,load_key().await); + cloud.providers = Cloud::load_providers().await; +} +*/ + +pub const KEY_PATH: &str = ".k"; +use key_of_life::get_key; + +pub async fn load_key() -> String { + let key_path = envmnt::get_or("KEY_PATH", KEY_PATH); + let key = get_key(&key_path,None).await; + if key.is_empty() { + std::process::exit(0x0100); + } + key +} +/* +pub type MapCheckInfo = BTreeMap>; + +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +pub struct KldCheck { + pub name: String, + pub liveness: HashMap, + pub apps: HashMap, + pub infos: Vec, +} +*/ \ No newline at end of file diff --git a/src/handlers.rs b/src/handlers.rs new file mode 100644 index 0000000..1319bad --- /dev/null +++ b/src/handlers.rs @@ -0,0 +1,9 @@ +// +/*! libresignin +*/ +// Copyright 2022, Jesús Pérez Lorenzo +// +pub mod jwt; +pub mod sessions; +pub mod router; +pub mod kratos; \ No newline at end of file diff --git a/src/handlers/jwt.rs b/src/handlers/jwt.rs new file mode 100644 index 0000000..5f2f65c --- /dev/null +++ b/src/handlers/jwt.rs @@ -0,0 +1,196 @@ +// +/*! libresignin +*/ +// Copyright 2022, Jesús Pérez Lorenzo +// + +//! Example JWT authorization/authentication. +//! +//! Run with +//! +//! ```not_rust +//! JWT_SECRET=secret cargo run -p example-jwt +//! ``` + +use axum::{ + async_trait, + extract::{FromRequest, RequestParts, TypedHeader}, + headers::{authorization::Bearer, Authorization}, + http::StatusCode, + response::{IntoResponse, Response}, + Json, +}; +use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation}; +use once_cell::sync::Lazy; +use serde::{Deserialize, Serialize}; +use serde_json::json; +use std::{fmt::Display}; + +// Quick instructions +// +// - get an authorization token: +// +// curl -s \ +// -w '\n' \ +// -H 'Content-Type: application/json' \ +// -d '{"client_id":"foo","client_secret":"bar"}' \ +// http://localhost:3000/authorize +// +// - visit the protected area using the authorized token +// +// curl -s \ +// -w '\n' \ +// -H 'Content-Type: application/json' \ +// -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUiLCJleHAiOjEwMDAwMDAwMDAwfQ.M3LAZmrzUkXDC1q5mSzFAs_kJrwuKz3jOoDmjJ0G4gM' \ +// http://localhost:3000/protected +// +// - try to visit the protected area using an invalid token +// +// curl -s \ +// -w '\n' \ +// -H 'Content-Type: application/json' \ +// -H 'Authorization: Bearer blahblahblah' \ +// http://localhost:3000/protected + +static KEYS: Lazy = Lazy::new(|| { + let secret = std::env::var("JWT_SECRET").expect("JWT_SECRET must be set"); + Keys::new(secret.as_bytes()) +}); + +pub async fn protected(claims: Claims) -> Result { + // Send the protected data to the user + Ok(format!( + "Welcome to the protected area :)\nYour data:\n{}", + claims + )) +} + +pub async fn getjwt() -> Result, AuthError> { + let claims = Claims { + sub: "b@b.com".to_owned(), + company: "ACME".to_owned(), + exp: 100000, + }; + // Create the authorization token + let token = encode(&Header::default(), &claims, &KEYS.encoding) + .map_err(|_| AuthError::TokenCreation)?; + + // Send the authorized token + Ok(Json(AuthBody::new(token))) +} + +pub async fn authorize(Json(payload): Json) -> Result, AuthError> { + // Check if the user sent the credentials + if payload.client_id.is_empty() || payload.client_secret.is_empty() { + return Err(AuthError::MissingCredentials); + } + // Here you can check the user credentials from a database + if payload.client_id != "foo" || payload.client_secret != "bar" { + return Err(AuthError::WrongCredentials); + } + let claims = Claims { + sub: "b@b.com".to_owned(), + company: "ACME".to_owned(), + exp: 100000, + }; + // Create the authorization token + let token = encode(&Header::default(), &claims, &KEYS.encoding) + .map_err(|_| AuthError::TokenCreation)?; + + // Send the authorized token + Ok(Json(AuthBody::new(token))) +} + +impl Display for Claims { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Email: {}\nCompany: {}", self.sub, self.company) + } +} + +impl AuthBody { + fn new(access_token: String) -> Self { + Self { + access_token, + token_type: "Bearer".to_string(), + } + } +} + +#[async_trait] +impl FromRequest for Claims +where + B: Send, +{ + type Rejection = AuthError; + + async fn from_request(req: &mut RequestParts) -> Result { + // Extract the token from the authorization header + let TypedHeader(Authorization(bearer)) = + TypedHeader::>::from_request(req) + .await + .map_err(|_| AuthError::InvalidToken)?; + // Decode the user data + dbg!("{:#?}",bearer.token()); + dbg!("{:#?}",decode::(bearer.token(), &KEYS.decoding, &Validation::default())); + let token_data = decode::(bearer.token(), &KEYS.decoding, &Validation::default()) + .map_err(|_| AuthError::InvalidToken)?; + + Ok(token_data.claims) + } +} + +impl IntoResponse for AuthError { + fn into_response(self) -> Response { + let (status, error_message) = match self { + AuthError::WrongCredentials => (StatusCode::UNAUTHORIZED, "Wrong credentials"), + AuthError::MissingCredentials => (StatusCode::BAD_REQUEST, "Missing credentials"), + AuthError::TokenCreation => (StatusCode::INTERNAL_SERVER_ERROR, "Token creation error"), + AuthError::InvalidToken => (StatusCode::BAD_REQUEST, "Invalid token"), + }; + let body = Json(json!({ + "error": error_message, + })); + (status, body).into_response() + } +} + +struct Keys { + encoding: EncodingKey, + decoding: DecodingKey, +} + +impl Keys { + fn new(secret: &[u8]) -> Self { + Self { + encoding: EncodingKey::from_secret(secret), + decoding: DecodingKey::from_secret(secret), + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Claims { + pub sub: String, + pub company: String, + pub exp: usize, +} + +#[derive(Debug, Serialize)] +pub struct AuthBody { + pub access_token: String, + pub token_type: String, +} + +#[derive(Debug, Deserialize)] +pub struct AuthPayload { + client_id: String, + client_secret: String, +} + +#[derive(Debug)] +pub enum AuthError { + WrongCredentials, + MissingCredentials, + TokenCreation, + InvalidToken, +} diff --git a/src/handlers/kratos.rs b/src/handlers/kratos.rs new file mode 100644 index 0000000..2e02277 --- /dev/null +++ b/src/handlers/kratos.rs @@ -0,0 +1,365 @@ +// +/*! libresignin +*/ +// Copyright 2022, Jesús Pérez Lorenzo +// + +use reqwest_middleware::{ClientBuilder, ClientWithMiddleware}; +use reqwest_retry::{RetryTransientMiddleware, policies::ExponentialBackoff}; +use reqwest_tracing::TracingMiddleware; +use serde::{Deserialize}; //, Serialize}; +use axum::{ + response::Html, + routing::get, + // routing::post, + Router, + //Json, + response::IntoResponse, + extract::{Extension,Path,Query}, + //http::{Request, header::HeaderMap, Method,StatusCode}, + http::{header::HeaderMap,StatusCode}, + // body::{Bytes, Body}, +}; +// use serde::{Deserialize, Serialize}; +use crate::defs::{DataDBs}; +use crate::utils::reqenv::ReqEnv; +use std::{ + collections::HashMap, + sync::Arc, +}; + +use reqwest_cookie_store::CookieStoreMutex; + +use crate::kratos::{ + get_root_url, + get_flow_csrf, + // load_idschema, + // KratosUser, + // KratosItemProps, + // KratosTraitItem, + // JsonMap, + load_traits, + set_kratos_user, + get_kratos_user, + set_kratos_login, + on_kratos_user, + logout_kratos_user, +// KratosSessionResponse, +// KratosReqId, +// KratosResponse, +}; + +// use cookie::{Cookie}; +// use tower_cookies::{Cookies}; + +pub async fn run(client: ClientWithMiddleware) { + client + .get("https://truelayer.com") + .header("foo", "bar") + .send() + .await + .unwrap(); +} + +// pub async fn registration_handler(cookies: Cookies,header: HeaderMap, Extension(dbs): Extension, Extension(req_cli): Extension>) -> Html<&'static str> { +pub async fn registration_handler(header: HeaderMap, Extension(dbs): Extension,Extension(req_cli): Extension>) -> impl IntoResponse { + let reqenv = ReqEnv::new(dbs.app, dbs.auth, header, "get", "/login", "html", "rt"); + let retry_policy = ExponentialBackoff::builder().build_with_max_retries(3); + let client = ClientBuilder::new(reqwest::Client::new()) + // Trace HTTP requests. See the tracing crate to make use of these traces. + .with(TracingMiddleware) + // Retry failed requests. + .with(RetryTransientMiddleware::new_with_policy(retry_policy)) + .build(); + + // run(client).await; + +// let reqwest_client = Client::builder().build().unwrap(); +// let client = ClientBuilder::new(reqwest_client) +// .with(LoggingMiddleware) +// .build(); +// let resp = client.get("https://truelayer.com").send().await.unwrap(); +// println!("TrueLayer page HTML: {}", resp.text().await.unwrap()); + + + // // dbg!("{:#?}",reqenv.websrvr()); + let root_url = get_root_url(reqenv.websrvr().signin.protocol,reqenv.websrvr().signin.root,reqenv.websrvr().signin.port); + let flow = String::from("self-service/registration"); + // //let flow = String::from("login"); + let query = String::from("/api?refresh=false&aal=&return_to="); + let traits = load_traits(reqenv.websrvr().signin.idschema_path); + // // dbg!("{:#?}",&traits); + let mut user_data = HashMap::new(); + user_data.insert("name.first".to_string(),"Jesús".to_string()); + user_data.insert("name.last".to_string(),"Pérez".to_string()); + user_data.insert("username".to_string(),"jesuspl".to_string()); + user_data.insert("password".to_string(),"19Ting22".to_string()); + user_data.insert("email".to_string(),"jpl@jesusperez.pro".to_string()); + let user = set_kratos_user("traits",user_data,traits); + // // println!("{}",&user); + let url=format!("{}/{}{}",&root_url,flow,query); + let html:String; + // match get_flow_csrf(reqenv.websrvr().signin,&req_cli,url.as_str()).await { + match get_flow_csrf("",reqenv.websrvr().signin,&req_cli,"","",url.as_str()).await { + Ok(reqid) => { + println!("flow: {}",&reqid.flow); + println!("csrf_token: {}",&reqid.csrf); + println!("cookie: {}",&reqid.cookie); + let query_id = format!("?flow={}",&reqid.flow); + let reg_url=format!("{}/{}{}",&root_url,flow,query_id); + match on_kratos_user("",reqenv.websrvr().signin,&req_cli,&reqid.csrf,&reqid.cookie,®_url,user).await { + Ok(res) => { + println!("Result {:?}",res.status); + html = format!( + r#" +

Register

+ login +
Status: {}
+ "#,&reqenv.websrvr_url(),res.status); + }, + Err(e) => { + eprintln!("{}",e); + html = format!( + r#" +

Login

+
Error: {}
+ "#,e); + } + } + }, + Err(e) => { + eprintln!("{}",e); + html = format!( + r#" +

Login

+
Error: {}
+ "#,e); + }, + } + Html(html) +} +pub async fn login_handler(header: HeaderMap, Extension(dbs): Extension,Extension(req_cli): Extension>) -> impl IntoResponse { + let reqenv = ReqEnv::new(dbs.app.to_owned(), dbs.auth.to_owned(), header.to_owned(), "get", "/login", "html", "rt"); + // let retry_policy = ExponentialBackoff::builder().build_with_max_retries(3); + // let client = ClientBuilder::new(reqwest::Client::new()) + // // Trace HTTP requests. See the tracing crate to make use of these traces. + // .with(TracingMiddleware) + // // Retry failed requests. + // .with(RetryTransientMiddleware::new_with_policy(retry_policy)) + // .build(); + + // run(client).await; + +// let reqwest_client = Client::builder().build().unwrap(); +// let client = ClientBuilder::new(reqwest_client) +// .with(LoggingMiddleware) +// .build(); +// let resp = client.get("https://truelayer.com").send().await.unwrap(); +// println!("TrueLayer page HTML: {}", resp.text().await.unwrap()); + + + // // dbg!("{:#?}",reqenv.websrvr()); + let flow = String::from("self-service/login"); + let query = String::from("/api?refresh=false&aal=&return_to="); + let root_url = get_root_url(reqenv.websrvr().signin.protocol,reqenv.websrvr().signin.root,reqenv.websrvr().signin.port); + let mut user_data = HashMap::new(); + user_data.insert("password".to_string(),"19Ting22".to_string()); + user_data.insert("password_identifier".to_string(),"jesuspl".to_string()); + let user = set_kratos_login(user_data); + let url=format!("{}/{}{}",&root_url,flow,query); + // match get_flow_csrf(reqenv.websrvr().signin,&req_cli,url.as_str()).await { + let html:String; + match get_flow_csrf("",reqenv.websrvr().signin,&req_cli,"","",url.as_str()).await { + Ok(reqid) => { + println!("flow: {}",&reqid.flow); + println!("csrf_token: {}",&reqid.csrf); + println!("cookie: {}",&reqid.cookie); + let query_id = format!("?flow={}",&reqid.flow); + let reg_url=format!("{}/{}{}",&root_url,flow,query_id); + match on_kratos_user("",reqenv.websrvr().signin,&req_cli,&reqid.csrf,&reqid.cookie,®_url,user).await { + Ok(res) => { + println!("Result {:?}",res.status); + // res.session_resp.session.identity.id, + // res.session_resp.session_token); + // return html.to_owned(); + if res.status == StatusCode::OK { + html = format!( + r#" +

Login

+ + +
Status: {}
+ "#, + &reqenv.websrvr_url(),res.session_resp.session.identity.id, res.session_resp.session_token, + &reqenv.websrvr_url(),res.session_resp.session.identity.id, res.session_resp.session_token, + res.status); + } else { + html = format!( + r#" +

Login

+
Status: {}
+ + "#,res.status,&reqenv.websrvr_url()); + } + }, + Err(e) => { + eprintln!("{}",e); + html = format!( + r#" +

Login

+ +
Error: {}
+ "#,&reqenv.websrvr_url(),e); + } + } + }, + Err(e) => { + eprintln!("{}",e); + html = format!( + r#" +

Login

+
Error: {}
+ "#,e); + }, + }; + Html(html) +} +pub async fn recovery_handler() -> Html<&'static str> { + Html("

Hello, World!

") +} +pub async fn whoami(id: String, token: String, + header: HeaderMap, dbs: DataDBs,req_cli: Arc) -> String { + let reqenv = ReqEnv::new(dbs.app, dbs.auth, header, "get", "/login", "html", "rt"); + let flow = String::from("self-service/login"); + let query = String::from("/api?refresh=false&aal=&return_to="); + let root_url = get_root_url(reqenv.websrvr().signin.protocol,reqenv.websrvr().signin.root,reqenv.websrvr().signin.port); + let url=format!("{}/{}{}",&root_url,flow,query); + let html:String; + // match get_flow_csrf(reqenv.websrvr().signin,&req_cli,url.as_str()).await { + match get_flow_csrf("",reqenv.websrvr().signin,&req_cli,"","",url.as_str()).await { + Ok(reqid) => { + println!("flow: {}",&reqid.flow); + println!("csrf_token: {}",&reqid.csrf); + println!("cookie: {}",&reqid.cookie); + let flow_check = String::from("sessions/whoami"); + let query_id = format!("?flow={}",&reqid.flow); + let reg_url=format!("{}/{}{}",&root_url,flow_check,query_id); + // match on_kratos_user(reqenv.websrvr().signin,&reqid.csrf,&id,®_url,user).await { + match get_kratos_user(&id,&token,reqenv.websrvr().signin,&req_cli,&reqid.csrf,&reqid.cookie,®_url).await { + Ok(res) => { + println!("Result {:?}",res.status); + if res.status == StatusCode::OK { + html = format!( + r#" +

whoami

+
Status: {}
+ + "#,res.status, + &reqenv.websrvr_url(),id,token) + } else { + html = format!( + r#" +

whoami

+
Status: {}
+ + "#,res.status, &reqenv.websrvr_url()) + } + }, + Err(e) => { + eprintln!("{}",e); + html = format!( + r#" +

whoami

+ +
Error: {}
+ "#,&reqenv.websrvr_url(),e); + } + } + }, + Err(e) => { + eprintln!("{}",e); + html = format!( + r#" +

whoami

+
Error: {}
+ "#,e); + }, + }; + html +} +#[derive(Deserialize)] +pub struct QueryToken { + pub token: String, +} +pub async fn whoami_handler(Path(id): Path, query: Query, + header: HeaderMap, Extension(dbs): Extension,Extension(req_cli): Extension>) -> impl IntoResponse { + Html(whoami(id,query.token.to_string(),header,dbs,req_cli).await) +} + pub async fn logout_handler(Path(id): Path, urlquery: Query, header: HeaderMap, Extension(dbs): Extension,Extension(req_cli): Extension>) -> impl IntoResponse { + let reqenv = ReqEnv::new(dbs.app, dbs.auth, header, "get", "/login", "html", "rt"); + let flow = String::from("self-service/login"); + let query = format!("/api?refresh=false&aal=&return_to=&query={}",urlquery.token.to_string()); + let root_url = get_root_url(reqenv.websrvr().signin.protocol,reqenv.websrvr().signin.root,reqenv.websrvr().signin.port); + let url=format!("{}/{}{}",&root_url,flow,query); + let html:String; + // match get_flow_csrf(reqenv.websrvr().signin,&req_cli,url.as_str()).await { + match get_flow_csrf("",reqenv.websrvr().signin,&req_cli,"","",url.as_str()).await { + Ok(reqid) => { + println!("flow: {}",&reqid.flow); + println!("csrf_token: {}",&reqid.csrf); + println!("cookie: {}",&reqid.cookie); + let query_id = format!("/api?{}&flow={}",&urlquery.token,&reqid.flow); + let reg_url=format!("{}/{}{}",&root_url,flow,query_id); + // match on_kratos_user(reqenv.websrvr().signin,&reqid.csrf,&id,®_url,user).await { + match logout_kratos_user(&id,&urlquery.token.to_string(),reqenv.websrvr().signin,&req_cli,&reqid.csrf,&reqid.cookie,®_url).await { + Ok(res) => { + println!("Result {:?}",res.status); + html = format!( + r#" +

Logout

+ login +
Status: {}
+ "#,reqenv.websrvr_url(),res.status); + }, + Err(e) => { + eprintln!("{}",e); + html = format!( + r#" +

Logout

+
Error: {}
+ "#,e); + } + }; + }, + Err(e) => { + eprintln!("{}",e); + html = format!( + r#" +

Logout

+
Error: {}
+ "#,e); + }, + }; + Html(html) +} +pub async fn verification_handler() -> Html<&'static str> { + Html("

Hello, World!

") +} +pub async fn settings_handler() -> Html<&'static str> { + Html("

Hello, World!

") +} +pub async fn welcome_handler() -> Html<&'static str> { + Html("

Hello, World!

") +} + +pub fn def_handlers(web_router: Router) -> Router { + web_router + .route("/login", get(login_handler)) + .route("/registration", get(registration_handler)) + .route("/recovery", get(recovery_handler)) + .route("/whoami/:id", get(whoami_handler)) + .route("/logout/:id", get(logout_handler)) + .route("/verification", get(verification_handler)) + .route("/settings", get(settings_handler)) + .route("/welcome", get(welcome_handler)) +} \ No newline at end of file diff --git a/src/handlers/router.rs b/src/handlers/router.rs new file mode 100644 index 0000000..30b8fc8 --- /dev/null +++ b/src/handlers/router.rs @@ -0,0 +1,68 @@ +// +/*! libresignin +*/ +// Copyright 2022, Jesús Pérez Lorenzo +// +use axum::{ + response::Html, + routing::get, + routing::post, + Router, + // extract::{self,Extension,Path,Query}, + extract::{Extension,Path}, + http::{header::HeaderMap, StatusCode}, + // body::{Bytes, Body}, + response::IntoResponse, + BoxError, +}; +use std::{ + borrow::Cow, +}; +use crate::defs::{DataDBs}; +use crate::utils::reqenv::ReqEnv; + +// pub async fn html_handler(Path(path): Path,query: Option>, Extension(dbs): Extension) -> Html<&'static str> { +// pub async fn html_handler(Path(path): Path,Extension(dbs): Extension) -> Html<&'static str> { +pub async fn html_handler(header: HeaderMap, Path(name): Path,Extension(dbs): Extension) -> Html<&'static str> { + let reqenv = ReqEnv::new(dbs.app, dbs.auth, header, "get", "/hello", "html", "rt"); + dbg!("{:#?}",reqenv.user_authentication().await); +// dbg!("{:#?}",headers); +//dbg!("{:#?}",name); +//dbg!("{:#?}",reqenv.config()); +// dbg!("{:#?}",query); +// dbg!("{:#?}",dbs.auth.users); + Html("

Hello, World!

") +} +pub async fn alive_handler() -> Html<&'static str> { + Html("ok") +} +pub async fn ready_handler() -> Html<&'static str> { + Html("ok") +} +pub async fn handle_error(error: BoxError) -> impl IntoResponse { + if error.is::() { + return (StatusCode::REQUEST_TIMEOUT, Cow::from("request timed out")); + } + if error.is::() { + return ( + StatusCode::SERVICE_UNAVAILABLE, + Cow::from("service is overloaded, try again later"), + ); + } + ( + StatusCode::INTERNAL_SERVER_ERROR, + Cow::from(format!("Unhandled internal error: {}", error)), + ) +} +pub fn router_handlers(web_router: Router) -> Router { + let mut webrouter = web_router.to_owned(); + webrouter = crate::handlers::kratos::def_handlers(webrouter); + webrouter + .route("/getjwt", get(crate::handlers::jwt::getjwt)) + .route("/protected", get(crate::handlers::jwt::protected)) + .route("/authorize", post(crate::handlers::jwt::authorize)) + .route("/session", get(crate::handlers::sessions::handler)) + .route("/hello/:name", get(html_handler)) + .route("/health/alive", get(alive_handler)) + .route("/health/ready", get(ready_handler)) +} diff --git a/src/handlers/sessions.rs b/src/handlers/sessions.rs new file mode 100644 index 0000000..bfe9ad7 --- /dev/null +++ b/src/handlers/sessions.rs @@ -0,0 +1,133 @@ +// +/*! libresignin +*/ +// Copyright 2022, Jesús Pérez Lorenzo +// +use async_session::{MemoryStore, Session, SessionStore as _}; +use axum::{ + async_trait, + extract::{Extension, FromRequest, RequestParts, TypedHeader}, + headers::Cookie, + http::{ + self, + header::{HeaderMap, HeaderValue}, + StatusCode, + }, + response::IntoResponse, + // routing::get, + // AddExtensionLayer, Router, +}; +use serde::{Deserialize, Serialize}; +use std::fmt::Debug; +use uuid::Uuid; + +pub async fn handler(user_id: UserIdFromSession) -> impl IntoResponse { + let (headers, user_id, create_cookie) = match user_id { + UserIdFromSession::FoundUserId(user_id) => (HeaderMap::new(), user_id, false), + UserIdFromSession::CreatedFreshUserId(new_user) => { + let mut headers = HeaderMap::new(); + headers.insert(http::header::SET_COOKIE, new_user.cookie); + (headers, new_user.user_id, true) + } + }; + + tracing::debug!("handler: user_id={:?} send_headers={:?}", user_id, headers); + + ( + headers, + format!( + "user_id={:?} session_cookie_name={} create_new_session_cookie={}", + user_id, crate::AXUM_SESSION_COOKIE_NAME, create_cookie + ), + ) +} + +pub struct FreshUserId { + pub user_id: UserId, + pub cookie: HeaderValue, +} + +pub enum UserIdFromSession { + FoundUserId(UserId), + CreatedFreshUserId(FreshUserId), +} + +#[async_trait] +impl FromRequest for UserIdFromSession +where + B: Send, +{ + type Rejection = (StatusCode, &'static str); + + async fn from_request(req: &mut RequestParts) -> Result { + let Extension(store) = Extension::::from_request(req) + .await + .expect("`MemoryStore` extension missing"); + + let cookie = Option::>::from_request(req) + .await + .unwrap(); + + let session_cookie = cookie + .as_ref() + .and_then(|cookie| cookie.get(crate::AXUM_SESSION_COOKIE_NAME)); + + // return the new created session cookie for client + if session_cookie.is_none() { + let user_id = UserId::new(); + let mut session = Session::new(); + session.insert("user_id", user_id).unwrap(); + let cookie = store.store_session(session).await.unwrap().unwrap(); + return Ok(Self::CreatedFreshUserId(FreshUserId { + user_id, + cookie: HeaderValue::from_str( + format!("{}={}", crate::AXUM_SESSION_COOKIE_NAME, cookie).as_str(), + ) + .unwrap(), + })); + } + + tracing::debug!( + "UserIdFromSession: got session cookie from user agent, {}={}", + crate::AXUM_SESSION_COOKIE_NAME, + session_cookie.unwrap() + ); + // continue to decode the session cookie + let user_id = if let Some(session) = store + .load_session(session_cookie.unwrap().to_owned()) + .await + .unwrap() + { + if let Some(user_id) = session.get::("user_id") { + tracing::debug!( + "UserIdFromSession: session decoded success, user_id={:?}", + user_id + ); + user_id + } else { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + "No `user_id` found in session", + )); + } + } else { + tracing::debug!( + "UserIdFromSession: err session not exists in store, {}={}", + crate::AXUM_SESSION_COOKIE_NAME, + session_cookie.unwrap() + ); + return Err((StatusCode::BAD_REQUEST, "No session found for cookie")); + }; + + Ok(Self::FoundUserId(user_id)) + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy)] +pub struct UserId(Uuid); + +impl UserId { + fn new() -> Self { + Self(Uuid::new_v4()) + } +} diff --git a/src/kratos.rs b/src/kratos.rs new file mode 100644 index 0000000..046c504 --- /dev/null +++ b/src/kratos.rs @@ -0,0 +1,633 @@ +// +/*! libresignin +*/ +// Copyright 2022, Jesús Pérez Lorenzo +// + +// use axum::{ +// response::Html, +// routing::get, +// routing::post, +// Router, +// extract::{self,Extension,Path,Query}, +// http::{Request, header::HeaderMap, Method}, +// body::{Bytes, Body}, +// }; +use std::collections::HashMap; +use serde::{Deserialize}; //, Serialize}; +use reqwest::header::{ + HeaderMap, + HeaderValue, + USER_AGENT, AUTHORIZATION, SET_COOKIE, COOKIE, ACCEPT, CONTENT_TYPE +}; +use reqwest::StatusCode; +//use reqwest::cookie::Cookie; +// use tower_cookies::{Cookie, Cookies}; +use anyhow::{anyhow,Result}; +use reqwest_cookie_store::CookieStoreMutex; +use app_env::config::SigninServer; +use std::sync::Arc; + +const PASSWORD_FIELD: &str = "password"; + +// use crate::defs::{DataDBs}; +// use crate::utils::reqenv::ReqEnv; +/* +fn parse_url(text: &str, lang: &str) -> reqwest::Url { + let mut url = reqwest::Url::parse("https://translate.google.com/translate_tts?ie=UTF-8&total=1&idx=0&client=tw-ob").unwrap(); + url.query_pairs_mut() + .append_pair("tl", lang) + .append_pair("q", text) + .append_pair("textlen", &text.len().to_string()) + .finish(); + url +} + + +fn get_url_flow(base: String, flow: String, query: String ) -> String { + format!("{}/self-service/{}/browser{}",base,flow,query) +} +*/ +fn default_empty() -> String { + "/".to_string() +} +fn default_array_empty() -> Vec { + Vec::new() +} +fn default_false() -> bool { + false +} +fn default_true() -> bool { + true +} + +// TODO load Traits from json file "identity.schema.json" .properties.traits +// What about additionalProperties +pub type JsonMap = serde_json::Map; + +#[derive(Debug, Deserialize,Default)] +#[allow(dead_code)] +pub struct KratosItemProps { + pub title: String, + #[serde(alias = "type")] + pub typ: String, +} + +#[derive(Debug, Deserialize,Default)] +#[allow(dead_code)] +pub struct KratosTraitItem { + #[serde(alias = "type")] + pub typ: String, + pub properties: Option>, +} + +#[derive(Debug, Deserialize,Default)] +#[allow(dead_code)] +pub struct KratosUser { + pub traits: HashMap::, + pub password: String, +} +#[derive(Debug, Deserialize,Default)] +#[allow(dead_code)] +pub struct KratosMetaLabel { + id: String, + text: String, + #[serde(alias = "type")] + typ: String, + context: String, // {} +} +#[derive(Debug, Deserialize,Default)] +#[allow(dead_code)] +pub struct KratosMeta { + label: KratosMetaLabel, +} +fn default_kratos_meta() -> KratosMeta { + KratosMeta::default() +} +#[derive(Debug, Deserialize,Default)] +#[allow(dead_code)] +pub struct KratosAttrib { + name: String, + #[serde(alias = "type")] + typ: String, + #[serde(default = "default_empty")] + value: String, + #[serde(default = "default_false")] + required: bool, + #[serde(default = "default_true")] + disabled: bool, + node_type: String, + #[serde(default = "default_array_empty")] + messages: Vec, + #[serde(default = "default_kratos_meta")] + meta: KratosMeta, +} +#[derive(Debug, Deserialize,Default)] +#[allow(dead_code)] +pub struct KratosNode { + #[serde(alias = "type")] + typ: String, + group: String, + attributes: KratosAttrib, +} +#[derive(Debug, Deserialize,Default)] +#[allow(dead_code)] +pub struct KratosUi { + action: String, + method: String, + nodes: Vec, +} +#[derive(Debug, Deserialize,Default)] +#[allow(dead_code)] +pub struct KratosHeader { + #[serde(alias = "set-cookie")] + cookie: String, +} +#[derive(Debug, Deserialize,Default)] +#[allow(dead_code)] +pub struct KratosIdentity { + pub created_at: String, + pub id: String, + // pub recovery_addresses: Vec, + pub schema_id: String, + pub schema_url: String, + pub state: String, + pub state_changed_at: String, + //pub traits: Vec, + pub updated_at: String, + //pub verifiable_addresses: Vec, +} +#[derive(Debug, Deserialize,Default)] +#[allow(dead_code)] +pub struct KratosSession { + pub id: String, + pub active: bool, + pub expires_at: String, + pub authenticated_at: String, + pub issued_at: String, + pub identity: KratosIdentity, + // pub authentication_methods: Vec, +} +fn default_default_session() -> KratosSession { + KratosSession::default() +} +#[derive(Debug, Deserialize,Default)] +#[allow(dead_code)] +pub struct KratosSessionResponse { + #[serde(default = "default_empty")] + pub session_token: String, + #[serde(default = "default_default_session")] + pub session: KratosSession, +} + +#[derive(Debug, Deserialize,Default)] +#[allow(dead_code)] +pub struct KratosResponse { + id: String, + #[serde(alias = "type")] + typ: String, + expires_at: String, + issued_at: String, + request_url: String, + ui: KratosUi, +} +#[derive(Debug, Deserialize,Default)] +#[allow(dead_code)] +pub struct KratosReqId { + pub flow: String, + pub csrf: String, + pub cookie: String, +} +#[derive(Debug,Default)] +#[allow(dead_code)] +pub struct OnKratosReq { + pub status: StatusCode, + pub cookie: String, + pub session_resp: KratosSessionResponse, +} + +fn load_idschema(path: String) -> JsonMap { + let str_data = std::fs::read_to_string(&path) + .unwrap_or_else(|e| { + eprintln!("Error read idschema {}: {}",&path,e); + String::from("") + }); + if str_data.is_empty() { + JsonMap::default() + } else { + serde_json::from_str(&str_data).unwrap_or_else(|e| { + eprintln!("Error loading idschema {}: {}",&path,e); + JsonMap::default() + }) + } +} + +fn get_item_props(val: &serde_json::Value) -> HashMap:: { + let mut props: HashMap:: = HashMap::new(); + if let Some(data_obj)= val["properties"].as_object() { + for (ky,val) in data_obj { + props.insert(ky.to_owned(), KratosItemProps { + title: format!("{}",val["title"]).replace('"',""), + typ: format!("{}",val["type"]).replace('"',""), + }); + } + } + props +} +pub fn load_traits(path: String) -> HashMap:: { + let idschema: JsonMap = load_idschema(path); + // dbg!("{:#?}",&idschema["properties"]["traits"]["properties"]); + let mut traits: HashMap:: = HashMap::new(); + if let Some(data_obj)= idschema["properties"]["traits"]["properties"].as_object() { + for (ky,val) in data_obj { + let value = format!("{}",val["type"]).replace('"',""); + match value.as_str() { + "object" => { + traits.insert(ky.to_owned(),KratosTraitItem { + typ: value, + properties: Some(get_item_props(val)), + }); + }, + "string" => { + traits.insert(ky.to_owned(),KratosTraitItem { + typ: value, + properties: None, + }); + }, + _ => { + println!("Type {} undefined",value.as_str()); + }, + } + } + }; + traits +} +pub fn set_kratos_user(mode: &str, data: HashMap::,traits: HashMap::) -> String { + let mut user_data = String::from("{"); + for (ky,def) in traits { + match def.typ.as_str() { + "object" => { + if let Some(props) = def.properties { + if mode == "json" { + user_data += &format!("\"{}\": ",ky); + user_data += &"{"; + } + for (it_ky,_it_def) in props { + let key = format!("{}.{}",ky,it_ky); + if let Some(value) = data.get(&key) { + if mode == "json" { + user_data += &format!("\"{}\": \"{}\",",it_ky,value); + } else { + user_data += &format!("\"{}.{}.{}\": \"{}\",",mode,ky,it_ky,value); + } + } + } + if mode == "json" { + user_data += &"}#,"; + user_data = user_data.replace(",}#","}"); + } + } + }, + _ => { + if let Some(value) = data.get(&ky) { + if mode == "json" { + user_data += &format!("\"{}\": \"{}\",",ky,value); + } else { + user_data += &format!("\"{}.{}\": \"{}\",",mode,ky,value); + } + } + }, + } + } + if let Some(passwd) = data.get(PASSWORD_FIELD) { + user_data += &format!("\"{}\":\"{}\",",PASSWORD_FIELD,passwd); + user_data += &format!("\"method\":\"{}\",",PASSWORD_FIELD); + } + user_data += &"}#"; + user_data = user_data.replace(",}#","}"); + user_data +} +pub fn set_kratos_login(data: HashMap::) -> String { + let mut user_data = String::from("{"); + for (ky,val) in data { + user_data += &format!("\"{}\": \"{}\",",ky,val); + } + user_data += &format!("\"method\":\"{}\",",PASSWORD_FIELD); + user_data += &"}#"; + user_data = user_data.replace(",}#","}"); + user_data +} +pub fn get_req_cli(cfg: SigninServer,cookie_store: &Arc) -> Result { +//pub fn get_req_cli(cfg: SigninServer) -> Result { + let mut req_cli = reqwest::Client::builder(); + if cfg.conn_timeout > 0 { + req_cli = req_cli.connect_timeout(std::time::Duration::from_millis(cfg.conn_timeout)); + } + if cfg.timeout > 0 { + req_cli = req_cli.timeout(std::time::Duration::from_millis(cfg.timeout)); + } + //match req_cli.cookie_provider(std::sync::Arc::clone(cookie_store)).build() { + //match req_cli.cookie_store(true).build() { + match req_cli.build() { + Ok(reqcli) => Ok(reqcli), + Err(e) => Err(anyhow!("Error get req client builder {}", &e)), + } +} +pub fn create_headers(id: &str,token: &str,csrf_token: &str, cookie: &str) -> Result { + let mut headers = HeaderMap::new(); + headers.insert(USER_AGENT, HeaderValue::from_static("reqwest")); + headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); + headers.insert(ACCEPT, HeaderValue::from_static("application/json")); + // if cookie != "" { + // headers.insert(COOKIE, HeaderValue::from_str(cookie) + // .unwrap_or(HeaderValue::from_static("")) + // ); + // // headers.insert(SET_COOKIE, HeaderValue::from_str(cookie) + // // .unwrap_or(HeaderValue::from_static("")) + // // ); + // } + if token != "" { + headers.insert(AUTHORIZATION, HeaderValue::from_str(&format!("Bearer {}",token)) + .unwrap_or(HeaderValue::from_static("")) + ); + } + // if csrf_token != "" { + // headers.insert("X-CSRF-Token", + // HeaderValue::from_str(csrf_token).unwrap_or(HeaderValue::from_static("")) + // ); + // } + if id != "" { + headers.insert("x-kratos-authenticated-identity-id", + HeaderValue::from_str(id).unwrap_or(HeaderValue::from_static("")) + ); + } + // dbg!("{:#?}",&headers); + // dbg!("{:#?}",&headers); + Ok(headers) +} +pub fn write_cookies(cfg: SigninServer,cookie_store: &Arc) -> Result<()> { + let mut writer = std::fs::File::create(cfg.cookies_path.to_owned()) + .map(std::io::BufWriter::new)?; + match cookie_store.lock() { + Ok(store) => { + store.save_json(&mut writer).unwrap_or_else(|e|{ + println!("Error save cookies to {}: {}",cfg.cookies_path,e); + }); + }, + Err(e) => { println!("Error save cookies: {}",e);}, + }; + Ok(()) +} +pub fn get_root_url(protocol: String, base: String, port: usize) -> String { + format!("{}://{}:{}",protocol,base,port) +} +pub fn write_store_cookie(cfg: SigninServer, cookie_store: &Arc) { + let _ = write_cookies(cfg.to_owned(), cookie_store); + match cookie_store.lock() { + Ok(store) => { + for c in store.iter_any() { + let json_data = serde_json::to_string(&c).unwrap(); + println!("c: {:#?}", c); + println!("{}", json_data); + } + }, + Err(e) => { println!("Error save cookies: {}",e);}, + }; +} +pub async fn get_flow_csrf(token: &str,cfg: SigninServer, cookie_store: &Arc, csrf_token: &str, cookie: &str, url: &str) -> Result { +// pub async fn get_flow_csrf(token: &str,cfg: SigninServer, csrf_token: &str, cookie: &str, url: &str) -> Result { + let reqid: KratosReqId; + let req_cli = get_req_cli(cfg.to_owned(), cookie_store)?; + let headers = create_headers("",token,csrf_token,cookie)?; + match req_cli + .get(url) + // .bearer_auth(token.access_token().secret()) + .headers(headers) + .send() + .await { + Ok(data) => { + let flow: String; + let csrf: String; + let cookie: String; + if let Some(ckie) = data.headers().get(SET_COOKIE) { + let str_cookie = &format!("{:?}",&ckie).replace("\"",""); + let (cookie_val,_) = str_cookie.split_once(";").unwrap_or_else(||("","")); + cookie=cookie_val.to_owned(); + } else { + cookie = String::from(""); + } + //dbg!("{:#?}",&data); + if ! url.contains("logout") { // && data.status() == StatusCode::OK { + match data.json::().await { + Ok(response) => { + // dbg!("{:#?}",&response); + //let (_url_path,url_flow) = &response.ui.action.as_str().split_once("flow=").unwrap_or_else(||("","")); + //flow=url_flow.to_string(); + flow=response.id; + if let Some(req_csrf_token) = response.ui.nodes.iter().filter(|n| n.attributes.name.as_str() == "csrf_token").collect::>().into_iter().nth(0) { + csrf=req_csrf_token.attributes.value.to_string(); + } else { + csrf=String::from(""); + } + reqid = KratosReqId { flow, csrf, cookie }; + }, + Err(e) => { + return Err(anyhow!("Error get flow json: {}", &e)); + } + }; + } else { + match data.json::().await { + Ok(response) => { + dbg!("{:#?}",&response); + //let (_url_path,url_flow) = &response.ui.action.as_str().split_once("flow=").unwrap_or_else(||("","")); + //flow=url_flow.to_string(); + // flow=response.id; + // if let Some(req_csrf_token) = response.ui.nodes.iter().filter(|n| n.attributes.name.as_str() == "csrf_token").collect::>().into_iter().nth(0) { + // csrf=req_csrf_token.attributes.value.to_string(); + // } else { + // csrf=String::from(""); + // } + reqid = KratosReqId { flow: String::from(""), csrf: String::from(""), cookie: String::from("") }; + }, + Err(e) => { + return Err(anyhow!("Error get flow json: {}", &e)); + } + }; + } + }, + Err(e) => { + return Err(anyhow!("Error get flow {}", &e)); + }, + }; + Ok(reqid) +} +pub async fn on_kratos_user(token: &str,cfg: SigninServer, cookie_store: &Arc, csrf_token: &str, cookie: &str, url: &str, data: String,) -> Result { +// pub async fn on_kratos_user(token: &str, cfg: SigninServer, csrf_token: &str, cookie: &str,url: &str, data: String,) -> Result { + // let mut body_data = String::from("{"); + // // body_data += &format!("\"csrf_token\":\"{}\",#{}",csrf_token,data); + // body_data += &format!("\"csrf_token\":\"{}\",#{}",csrf_token,data); + // body_data = body_data.replace("#{",""); + let body_data = String::from(data); + println!("{}",&body_data); + let json_data: serde_json::Value = serde_json::from_str(&body_data).unwrap_or_else(|e|{ + println!("Error json: {}",e); + serde_json::Value::default() + }); + let req_cli = get_req_cli(cfg.to_owned(), cookie_store)?; + let headers = create_headers("",token,csrf_token,cookie)?; + println!("URL: {}",&url); + let status: StatusCode; + let cookie: String; + let session_resp: KratosSessionResponse; + match req_cli + .post(url) + .headers(headers) + .json(&json_data) + .send() + .await { + Ok(res) => { + if let Some(ckie) = res.headers().get(SET_COOKIE) { + let str_cookie = &format!("{:?}",&ckie).replace("\"",""); + dbg!("{:#?}",&ckie); + let (cookie_val,_) = str_cookie.split_once(";").unwrap_or_else(||("","")); + cookie=cookie_val.to_owned(); + } else { + cookie = String::from(""); + } + status = res.status(); + if url.contains("login") || url.contains("whoami") { + // match res.json::().await { + match res.json::().await { + Ok(data) => { + // dbg!("{:#?}",&data); + // session_resp = KratosSessionResponse::default(); + session_resp = data; + }, + Err(e) => { + return Err(anyhow!("Error on kratos response session json {}", &e)); + } + } + } else { + session_resp = KratosSessionResponse::default(); + } + }, + Err(e) => { + println!("Error get :{}",e); + return Err(anyhow!("Error on kratos {}", &StatusCode::BAD_REQUEST)); + }, + // match response.status() { + // reqwest::StatusCode::OK => { + // println!("Success! {:?}"); + // }, + // reqwest::StatusCode::UNAUTHORIZED => { + // println!("Need to grab a new token"); + // }, + // _ => { + // panic!("Uh oh! Something unexpected happened."); + // }, + }; + Ok(OnKratosReq { status,cookie,session_resp }) +} +pub async fn get_kratos_user(id: &str,token: &str,cfg: SigninServer,cookie_store: &Arc, csrf_token: &str, cookie: &str, url: &str) -> Result { +//pub async fn get_kratos_user(id: &str,token: &str,cfg: SigninServer, csrf_token: &str, cookie: &str, url: &str) -> Result { + // let reqid: KratosReqId; + let req_cli = get_req_cli(cfg.to_owned(), cookie_store)?; + let headers = create_headers(id,token,csrf_token,cookie)?; + // dbg!("{:#?}",&headers); + let status: StatusCode; + let cookie = String::from(""); + let session_resp: KratosSessionResponse; + match req_cli + .get(url) + .headers(headers) + .send() + .await { + Ok(res) => { + // dbg!("{:#?}",&res); + status = res.status(); + match res.json::().await { + //match res.json::().await { + Ok(data) => { + // dbg!("{:#?}",&data); + session_resp = KratosSessionResponse::default(); + // if url.contains("logout") { + // session_resp = KratosSessionResponse::default(); + // } else { + // match res.json::().await { + // Ok(session) => session_resp = KratosSessionResponse { + // session_token: token.to_owned(), + // session, + }, + Err(e) => { + return Err(anyhow!("Error get kratos session json {}", &e)); + } + } + }, + Err(e) => { + println!("Error get :{}",e); + return Err(anyhow!("Error get kratos user {}", &StatusCode::BAD_REQUEST)); + }, + }; + Ok(OnKratosReq { status,cookie,session_resp }) +} +pub async fn logout_kratos_user(id: &str,token: &str,cfg: SigninServer, cookie_store: &Arc, csrf_token: &str, cookie: &str, url: &str) -> Result { + // let reqid: KratosReqId; + let mut body_data = String::from("{"); + body_data += &format!("\"session_token\":\"{}\"",&token); + // body_data += &format!("\"ory_kratos_session\":\"{}\"",&token); + body_data += "}"; + println!("{}",&body_data); + let json_data: serde_json::Value = serde_json::from_str(&body_data).unwrap_or_else(|e|{ + println!("Error json: {}",e); + serde_json::Value::default() + }); + let req_cli = get_req_cli(cfg.to_owned(), cookie_store)?; + let headers = create_headers(id,token,csrf_token,cookie)?; + // dbg!("{:#?}",&headers); + let status: StatusCode; + let cookie = String::from(""); + let session_resp: KratosSessionResponse; + match req_cli + .delete(url) + // .get(url) + .headers(headers) + .json(&json_data) + .send() + .await { + Ok(res) => { + dbg!("{:#?}",&res); + status = res.status(); + if status == StatusCode::OK { + match res.json::().await { + //match res.json::().await { + Ok(data) => { + dbg!("{:#?}",&data); + session_resp = KratosSessionResponse::default(); + }, + Err(e) => { + return Err(anyhow!("Error parse logout kratos session json {}", &e)); + }, + } + } else { + session_resp = KratosSessionResponse::default(); + } +// if url.contains("logout") { + // } else { + // match res.json::().await { + // Ok(session) => session_resp = KratosSessionResponse { + // session_token: token.to_owned(), + // session, + // }, + // Err(e) => { + // return Err(anyhow!("Error get kratos session json {}", &e)); + // } + // }; + // } + }, + Err(e) => { + println!("Error get :{}",e); + return Err(anyhow!("Error logout kratos user {}", &StatusCode::BAD_REQUEST)); + }, + }; + Ok(OnKratosReq { status,cookie,session_resp }) +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..b0feed9 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,413 @@ +// +/*! libresignin +*/ +// Copyright 2022, Jesús Pérez Lorenzo +// + +//! Run with +//! +//! ```not_rust +//! cargo run -p example-static-file-server +//! ``` +//! https://rustrepo.com/repo/TrueLayer-reqwest-middleware +//! https://github.com/AscendingCreations/AxumCSRF +//! + +use std::{ + sync::Arc, + time::Duration, +}; +use tokio::sync::Mutex; + +use axum::{ + error_handling::HandleErrorLayer, + http::{StatusCode, header::HeaderValue, Method}, + routing::get_service, + AddExtensionLayer, + Router, +}; +use axum_server::tls_rustls::RustlsConfig; +use tower_http::{ + services::ServeDir, + trace::TraceLayer, + cors::{CorsLayer, Origin}, +}; +use tower::{ServiceBuilder}; + +use app_env::{ + AppStore, + appenv::AppEnv, + appinfo::AppInfo, + appdata::AppData, + config::{Config} +}; +use connectors::defs::{AppDataConn}; +use app_auth::AuthStore; +use anyhow::{Result}; + +//use crate::defs::{DataDBs,CollsData}; +use crate::defs::{DataDBs}; + +// static WEBSERVER: AtomicUsize = AtomicUsize::new(0); +const PKG_VERSION: &'static str = env!("CARGO_PKG_VERSION"); +const APP_NAME: &'static str = "libresignin"; +// const PKG_VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION"); +const PKG_NAME: &'static str = env!("CARGO_PKG_NAME"); +const PKG_AUTHORS: &'static str = env!("CARGO_PKG_AUTHORS"); +const PKG_DESCRIPTION: &'static str = env!("CARGO_PKG_DESCRIPTION"); + +const AXUM_SESSION_COOKIE_NAME: &str = "axum_session"; + +pub mod defs; +pub mod kratos; +pub mod handlers; +pub mod utils; +pub mod state; + +pub type BxDynResult = std::result::Result>; + +async fn create_auth_store(app_env: &AppEnv,verbose: isize) -> AuthStore { + let config = app_env.get_curr_websrvr_config(); + let model_path = config.st_auth_model_path(); + let policy_path = config.st_auth_policy_path(); + AuthStore::new(&config,AuthStore::create_enforcer(model_path,policy_path).await,verbose) +} +async fn up_web_server(webpos: usize) -> Result<()> { // WebSettings> { + let debug = envmnt::get_isize("DEBUG",0); + let verbose = envmnt::get_isize("WEB_SERVER_VERBOSE", 0); + let mut app_env = AppEnv::default(); + app_env.curr_web=webpos; + if verbose > 0 { + println!("Web services: init {} ___________ ", chrono::Utc::now().timestamp()); + } + app_env.info = AppInfo::new( + APP_NAME, + format!("web: {}",&webpos), + format!("version: {}",PKG_VERSION), + format!("authors: {}",PKG_AUTHORS), + format!("{}",PKG_DESCRIPTION), + ); + webenv::init_app(&mut app_env,verbose).await.unwrap_or_else(|e| + panic!("Error loadding app environment {}",e) + ); + let config = app_env.get_curr_websrvr_config(); + // let webserver_status = WEBSERVER.load(Ordering::Relaxed); + let zterton_env = envmnt::get_or(format!("_{}",&config.name).as_str(), "UNKNOWN"); + // if webserver_status != 0 { + if zterton_env != "UNKNOWN" { + if verbose > 0 { + println!("{} web services at {}",APP_NAME,&zterton_env); + } + return Ok(()); + } + // WEBSERVER.store(1,Ordering::Relaxed); + // TODO pass root file-name frmt from AppEnv Config + if verbose > 0 { + println!("Loading webserver: {} ({})",&config.name,&app_env.curr_web); + } + let (app, socket) = webenv::start_web(&mut app_env).await; + if verbose > 0 { + println!("Load app store ..."); + } + // `MemoryStore` just used as an example. Don't use this in production. + let mem_store = async_session::MemoryStore::new(); + + let app_store = AppStore::new(AppData::new(app_env.to_owned(),verbose)); + // As static casbin + if verbose > 0 { + println!("Load auth store ..."); + } + let auth_store = create_auth_store(&app_env,verbose).await; + if verbose > 0 { + println!("Load data store ..."); + } + let app_data_conn = AppDataConn::new(APP_NAME.to_string(),app_env.config.datastores_settings.to_owned(),"").await; + let cookie_store = { + let file = std::fs::File::open(&config.signin.cookies_path) + .map(std::io::BufReader::new) + .unwrap_or_else(|e| + panic!("Error creating cookie path {}: {}",config.signin.cookies_path,e) + ); + cookie_store::CookieStore::load_json(file) + .unwrap_or_else(|e| + panic!("Error unable to load path {}: {}",config.signin.cookies_path,e) + ) + }; + let cookie_store = reqwest_cookie_store::CookieStoreMutex::new(cookie_store); + let cookie_store = std::sync::Arc::new(cookie_store); + let req_cli = reqwest::Client::builder() +// .timeout(std::time::Duration::from_millis(500)) +// .connect_timeout(std::time::Duration::from_millis(100)) + .cookie_store(true) + .build().unwrap_or_else(|e| + panic!("Error creating reqwest client: {}",e) + ); + let data_dbs = DataDBs { + // colls: CollsData::new(app_env.to_owned(),verbose), + app: app_store.to_owned(), + auth: auth_store.to_owned(), + conns: Arc::new(Mutex::from(app_data_conn)), + }; + if verbose > 0 { + println!("Load web filters ..."); + } + + // let us get some static boxes from config values: + let log_name = app_env.config.st_log_name(); + // Path for static files + let html_path: &str; + if config.html_path.is_empty() { + html_path = "/"; + } else { + html_path = config.st_html_path(); + } + let html_url: String; + if config.html_url.is_empty() { + html_url = String::from("/"); + } else { + html_url = config.html_url.to_owned(); + } + // // If not graphQL comment/remove next line + // let gql_path = config.st_gql_req_path(); + // // If not graphiQL comment/remove next line Interface GiQL + // let giql_path = config.st_giql_req_path(); + + // let origins: < http::HeaderValue> = config.allow_origin.iter().map(AsRef::as_ref).collect(); + let mut origins: Vec = Vec::new(); + for itm in config.allow_origin.to_owned() { + match HeaderValue::from_str(itm.as_str()) { + Ok(val) => origins.push(val), + Err(e) => println!("error {} with {} header for allow_origin",e,itm), + } + } +/* + let cors = warp::cors() + //.allow_any_origin() + .allow_origins(origins) + //.allow_origins(vec![app_env.config.allow_origin.as_str(), "https://localhost:8000"]) + .allow_credentials(true) + .allow_header("content-type") + .allow_header("Authorization") + .allow_methods(&[Method::GET, Method::POST, Method::DELETE]); + + let auth_api = + // Auth routes for login & logout REQUIRED + app_auth_filters::auth(app_store.clone(),auth_store.clone(),cors.clone()); // .with(cors.clone()); + + let gqli_api = + // If not graphiQL comment/remove next line Interface GiQL MUST BEFORE graphql post with schema + // app_api.to_owned() + graphql::graphiql(gql_path, giql_path, data_dbs.clone()).await; + if giql_path.len() > 0 && verbose > 0 { + println!( + "GraphiQL url: {}://{}:{}/{}", + &app.protocol, &app.host, &app.port, &giql_path + ); + } + let mut cloud = Cloud::default(); + env_cloud("*", &mut cloud.env).await?; + load_cloud_env(&mut cloud).await; + // If not graphQL comment/remove next line + let gql_api=graphql::graphql(gql_path, data_dbs.clone(),cors.clone()).await; //.with(cors.clone()); + // // Add ALL ENTITIES to work with here + let kloud_api = filters::CollFilters::new(&config.prefix) + .filters_config(data_dbs.clone(),cloud.clone(),cors.clone()); + + let file_api = app_file_filters::files(app_store.clone(),auth_store.clone()).with(cors.clone()); + // Path for static files, better to be LAST + let fs_api = warp::fs::dir(html_path).with(warp::compression::gzip()); + + let home_api = filters::CollFilters::new(&config.prefix) + .filters_home(data_dbs.clone(),cloud.clone(),cors.clone(),"info"); + + let app_api = auth_api + .or(gqli_api).or(gql_api) + .or(home_api) + .or(kloud_api) + .or(file_api) + .or(fs_api) + .recover(move | error: warp::Rejection| handle_rejection(error, app_store.clone())) + .boxed(); + // Wrap routes with log to get info + let routes = app_api.with(warp::log(log_name)); + //let routes = app_api.with(cors).with(warp::log(log_name)); +*/ + println!( + "Starting http server: {}://{}:{}", + &app.protocol, &app.host, &app.port + ); + envmnt::set(APP_NAME, format!("{}:{}",&app.host,&app.port)); + if debug > 0 { + println!("Web services: done {} __________ ",chrono::Utc::now().timestamp()); + } + let mut web_router = Router::new(); + web_router = crate::handlers::router::router_handlers(web_router); + if !config.html_path.is_empty() { + web_router = web_router.nest( + html_url.as_str(), + get_service(ServeDir::new(html_path)).handle_error(|error: std::io::Error| async move { + ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Unhandled internal error: {}", error), + ) + }), + ); + } + if config.allow_origin.len() > 0 { + web_router = web_router.layer(CorsLayer::new() + .allow_origin(Origin::list(origins)) + .allow_methods(vec![Method::GET, Method::POST]) + ); + } + web_router = web_router.layer(AddExtensionLayer::new(mem_store)); + web_router = web_router.layer(AddExtensionLayer::new(data_dbs)); + web_router = web_router.layer(AddExtensionLayer::new(cookie_store)); + web_router = web_router.layer(TraceLayer::new_for_http()); + web_router = web_router.layer(tower_cookies::CookieManagerLayer::new()); + web_router = web_router.layer( + ServiceBuilder::new() + // Handle errors from middleware + .layer(HandleErrorLayer::new(handlers::router::handle_error)) + .load_shed() + .concurrency_limit(1024) + .timeout(Duration::from_secs(10)) + .layer(TraceLayer::new_for_http()) + .layer(AddExtensionLayer::new(crate::state::SharedState::default())) + .into_inner(), + ); + if app.protocol.clone().as_str() == "http" { + let _ = axum::Server::bind(&socket) + .serve(web_router.into_make_service()) + .await + .expect("server failed"); + // let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); + // tracing_subscriber::fmt::init(); + // tracing::debug!("listening on {}", addr); + //let _ = axum::Server::bind(&addr) + // let _ = axum::Server::bind(&socket) + // .serve(app.into_make_service()) + // .await; + // .unwrap(); + } else { + match RustlsConfig::from_pem_file( + format!("{}/ssl/{}", &config.resources_path, "fullchain.pem") + ,format!("{}/ssl/{}", &config.resources_path,"privkey.pem") + ).await { + Ok(ssl_cfg) => { + let _ = axum_server::bind_rustls(socket,ssl_cfg) + .serve(web_router.into_make_service()) + .await + .expect("server failed"); + }, + Err(e) => { + println!("SSL Certifcates error: {}",e); + return Ok(()); + } + }; + } + Ok(()) +} +// fn get_args() -> (String,String) { +// let args: Vec = std::env::args().collect(); +// let mut arg_cfg_path = String::from(""); +// let mut arg_env_path = String::from(""); +// args.iter().enumerate().for_each(|(idx,arg)| { +// if arg == "-c" { +// arg_cfg_path=args[idx+1].to_owned(); +// } else if arg == "-e" { +// arg_env_path=args[idx+1].to_owned(); +// } +// }); +// (arg_cfg_path,arg_env_path) +// } +//async fn get_app_env(arg_cfg_path: String,verbose: isize) -> Result<(Cloud,AppEnv)> { +// let mut cloud = Cloud::default(); +// load_cloud_env(&mut cloud).await; +// async fn get_app_env(arg_cfg_path: String,verbose: isize) -> Result { +// let mut app_env = AppEnv::default(); +// let config_content = Config::load_file_content(verbose,&arg_cfg_path); +// if ! config_content.contains("run_mode") { +// Err(anyhow!("Run mode not found in config {}", &arg_cfg_path)) +// } else { +// app_env.config = Config::new(config_content,verbose); +// // Ok((cloud,app_env)) +// Ok(app_env) +// } +// } +/* +async fn set_reqenv(app_env: &AppEnv,verbose: isize) -> ReqEnv { + let app_store = AppStore::new(AppData::new(app_env.to_owned(),verbose)); + let auth_store = create_auth_store(&app_env,verbose).await; + let mut headers = HeaderMap::new(); + headers.insert(http::header::HOST, "localhost".parse().unwrap()); + ReqEnv::new( + app_store, + auth_store, + headers, + Method::GET, + "/config", "config", "kloud" + ) +} +*/ +#[tokio::main] +async fn main() -> BxDynResult<()> { //std::io::Result<()> { + let args: Vec = std::env::args().collect(); + if args.len() > 1 { + match args[1].as_str() { + "-h" | "--help" => { + println!("{} USAGE: -c config-toml -e env.file",PKG_NAME); + return Ok(()); + }, + "-v" | "--version" => { + println!("{} version: {}",PKG_NAME,PKG_VERSION); + return Ok(()); + }, + _ => println!("{}",PKG_NAME), + } + } + let debug=envmnt::get_isize("DEBUG",0); + let mut arg_cfg_path = String::from(""); + let mut arg_env_path = String::from(""); + args.iter().enumerate().for_each(|(idx,arg)| { + if arg == "-c" { + arg_cfg_path=args[idx+1].to_owned(); + } else if arg == "-e" { + arg_env_path=args[idx+1].to_owned(); + } + }); + if !arg_env_path.is_empty() { + let env_path = std::path::Path::new(&arg_env_path); + dotenv::from_path(env_path)?; + } + pretty_env_logger::init(); + let config_content = Config::load_file_content(debug, &arg_cfg_path); + if !config_content.contains("run_mode") { + panic!("Error no run_mode found or config path incomplete"); + } + let config = Config::new(config_content,debug); + let app_data_conn = AppDataConn::new(APP_NAME.to_string(),config.datastores_settings.to_owned(),"").await; + if config.datastores_settings.len() > 0 { + if !app_data_conn.check_connections(config.datastores_settings.to_owned()).await { + println!("Error checking app data store connections"); + } + } + // Set the RUST_LOG, if it hasn't been explicitly defined + if std::env::var_os("RUST_LOG").is_none() { + std::env::set_var( + "RUST_LOG", + "example_static_file_server=debug,tower_http=debug", + ) + } + if config.run_websrvrs { + for (pos,it) in config.websrvrs.iter().enumerate() { + if debug > 1 { + println!("{} -> {}",it.name,pos); + } + //tokio::join!(async move { + let handle = tokio::spawn(async move { up_web_server(pos).await }); + let _ = handle.await; + tokio::time::sleep(tokio::time::Duration::from_secs(3)).await; + } + } + Ok(()) +} diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..60ce555 --- /dev/null +++ b/src/state.rs @@ -0,0 +1,66 @@ +use axum::{ + body::Bytes, + extract::{ContentLengthLimit, Extension, Path}, + handler::Handler, + http::StatusCode, + routing::{get}, + Router, +}; +use std::{ + collections::HashMap, + sync::{Arc, RwLock}, +}; +use tower_http::{ + compression::CompressionLayer,// trace::TraceLayer, +}; + +pub type SharedState = Arc>; + +#[derive(Default)] +pub struct State { + pub db: HashMap, +} + +async fn kv_get( + Path(key): Path, + Extension(state): Extension, +) -> Result { + let db = &state.read().unwrap().db; + + if let Some(value) = db.get(&key) { + Ok(value.clone()) + } else { + Err(StatusCode::NOT_FOUND) + } +} + +async fn kv_set( + Path(key): Path, + ContentLengthLimit(bytes): ContentLengthLimit, // ~5mb + Extension(state): Extension, +) { + state.write().unwrap().db.insert(key, bytes); +} + +async fn list_keys(Extension(state): Extension) -> String { + let db = &state.read().unwrap().db; + + db.keys() + .map(|key| key.to_string()) + .collect::>() + .join("\n") +} + +pub fn router_handlers(web_router: Router) -> Router { + let mut webrouter = web_router.to_owned(); + webrouter = crate::handlers::kratos::def_handlers(webrouter); + webrouter + .route( + "/:key", + // Add compression to `kv_get` + get(kv_get.layer(CompressionLayer::new())) + // But don't compress `kv_set` + .post(kv_set), + ) + .route("/keys", get(list_keys)) +} \ No newline at end of file diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..88a2b02 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,7 @@ +// +/*! libresignin +*/ +// Copyright 2022, Jesús Pérez Lorenzo +// +pub mod reqtasks; +pub mod reqenv; \ No newline at end of file diff --git a/src/utils/reqenv.rs b/src/utils/reqenv.rs new file mode 100644 index 0000000..d50c6d6 --- /dev/null +++ b/src/utils/reqenv.rs @@ -0,0 +1,141 @@ +// +/*! libresignin +*/ +// Copyright 2022, Jesús Pérez Lorenzo +// +//use std::collections::HashMap; +use std::fmt; +//use std::str::from_utf8; +//use tera::Tera; + +use axum::{ + // async_trait, + // extract::{Extension, FromRequest, RequestParts, TypedHeader}, + // extract::{Extension,Path,Query}, + // headers::Cookie, + http::{ +// self, + header::{HeaderMap, HeaderValue}, +// Method, +// StatusCode, + }, +// response::IntoResponse, +}; +use crate::utils::reqtasks::ReqTasks; +use app_env::{ + appenv::AppEnv, + config::{Config, WebServer}, + module::Module, + AppStore, + // AppData, +}; +use app_auth::{ + AuthStore, + User, + UserCtx, + LoginRequest, + // BEARER_PREFIX, + // AuthError, +}; + +/// `ReqEnv` includes ReqTasks as core type +/// it is a kind of wrapping type +/// to declare: +/// - auth methods locally +/// - other attributes +/// - other request tasks methods +/// +#[derive(Clone)] +pub struct ReqEnv { + pub req: ReqTasks, + +} + +impl fmt::Display for ReqEnv { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} {} {}", &self.req.path, &self.req.origin, &self.req.key_module) + } +} + +impl ReqEnv { + pub fn new( + app_db: AppStore, + auth_store: AuthStore, + header: HeaderMap, + method: &str, + path: &str, + origin: &str, + key_module: &str + ) -> Self { + 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(), + // }; + Self { + req: ReqTasks { + app_data: app_data.to_owned(), + auth_store: auth_store.to_owned(), + header, + method: method.to_string(), + path: format!("{}{}",key_module,path).to_string(), + origin: format!("{}{}",key_module,origin).to_string(), + key_module: key_module.to_string(), + }, + } + } + /// Get `AppEnv` + #[must_use] + pub fn env(&self) -> AppEnv { + self.req.env() + } + /// Get Tera + #[must_use] + pub fn tera(&self) -> tera::Tera { + self.req.tera() + } + /// Get Context (ctx) + #[must_use] + pub fn ctx(&self) -> tera::Context { + self.req.ctx() + } + /// Get `AppEnv` Config + #[must_use] + pub fn config(&self) -> Config { + self.req.config() + } + #[must_use] + pub fn websrvr(&self) -> WebServer { + self.req.config().websrvrs[self.req.env().curr_web].to_owned() + } + #[must_use] + pub fn websrvr_url(&self) -> String { + let config=self.req.config().websrvrs[self.req.env().curr_web].to_owned(); + format!("{}://{}:{}", config.srv_protocol, config.srv_host, config.srv_port) + } + #[must_use] + pub fn module(&self) -> Module { + self.req.module() + } + #[must_use] + pub fn lang(&self) -> String { + self.req.lang() + } + #[allow(clippy::missing_errors_doc)] + pub fn token_from_header(&self) -> anyhow::Result { + self.req.token_from_header() + } + #[allow(clippy::missing_errors_doc)] + pub async fn token_session(&self, login: &LoginRequest) -> anyhow::Result { + self.req.token_session(login).await + } + #[allow(clippy::missing_errors_doc)] + pub async fn user_authentication(&self) -> anyhow::Result { + self.req.user_authentication().await + } + #[allow(clippy::missing_errors_doc)] + pub async fn get_user(&self) -> User { + self.req.get_user().await + } +} \ No newline at end of file diff --git a/src/utils/reqtasks.rs b/src/utils/reqtasks.rs new file mode 100644 index 0000000..55a4b85 --- /dev/null +++ b/src/utils/reqtasks.rs @@ -0,0 +1,453 @@ +// +/*! libresignin +*/ +// Copyright 2022, Jesús Pérez Lorenzo +// + +#![allow(clippy::needless_lifetimes)] + +use axum::{ + // async_trait, + // extract::{Extension, FromRequest, RequestParts, TypedHeader}, + // extract::{Extension,Path,Query}, + // headers::Cookie, + http::{ +// self, + header::{HeaderMap}, //, HeaderValue}, +// Method, +// StatusCode, + }, +// response::IntoResponse, +}; + +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, + User, + UserCtx, + BEARER_PREFIX, + AuthError, + LoginRequest, +}; + +// 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: String, + 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: &str, + 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: method.to_string(), + 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; + if let Some(has_reload) = ctx.get("need_reload") { + if has_reload == "true" { + need_reload=true; + } + } + 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| { + if envmnt::get_isize("DEBUG", 0) > 0 { + println!("{}",e); + } + String::from("") + }); + println!("token: {}",token); + self.check_authentication(token).await + } + #[allow(clippy::missing_errors_doc)] + pub async fn user_role(&self) -> String { + let token = self.token_from_header().unwrap_or_else(|e| { + if envmnt::get_isize("DEBUG", 0) > 0 { + println!("{}",e); + } + String::from("") + }); + if token.is_empty() { + return String::from(""); + } + let role: String; + 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) { + role = format!("{}",user.role); + } else { + role = String::from(""); + } + } else { + role = String::from(""); + } + role + } + #[allow(clippy::missing_errors_doc)] + pub async fn get_user(&self) -> User { + let token = self.token_from_header().unwrap_or_else(|e| { + if envmnt::get_isize("DEBUG", 0) > 0 { + println!("{}",e); + } + String::from("") + }); + if !token.is_empty() { + 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) { + return user.to_owned() + } + } + } + User::default() + } + #[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()) + } + } +}