chore: add code

This commit is contained in:
Jesús Pérez Lorenzo 2022-02-22 20:31:44 +00:00
parent f71f0cc03b
commit 814021eeac
13 changed files with 2661 additions and 0 deletions

79
Cargo.toml Normal file
View File

@ -0,0 +1,79 @@
[package]
name = "libresignin"
version = "0.1.0"
authors = ["JesusPerez <jpl@jesusperez.pro>"]
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"

98
src/defs.rs Normal file
View File

@ -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<Kloud>,
}
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<String,Kloud> {
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<Mutex<AppDataConn>>,
}
/*
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<String,Vec<SrvcsHostInfOut>>;
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
pub struct KldCheck {
pub name: String,
pub liveness: HashMap<String, MapCheckInfo>,
pub apps: HashMap<String, MapCheckInfo>,
pub infos: Vec<InfoStatus>,
}
*/

9
src/handlers.rs Normal file
View File

@ -0,0 +1,9 @@
//
/*! libresignin
*/
// Copyright 2022, Jesús Pérez Lorenzo
//
pub mod jwt;
pub mod sessions;
pub mod router;
pub mod kratos;

196
src/handlers/jwt.rs Normal file
View File

@ -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<Keys> = 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<String, AuthError> {
// Send the protected data to the user
Ok(format!(
"Welcome to the protected area :)\nYour data:\n{}",
claims
))
}
pub async fn getjwt() -> Result<Json<AuthBody>, 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<AuthPayload>) -> Result<Json<AuthBody>, 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<B> FromRequest<B> for Claims
where
B: Send,
{
type Rejection = AuthError;
async fn from_request(req: &mut RequestParts<B>) -> Result<Self, Self::Rejection> {
// Extract the token from the authorization header
let TypedHeader(Authorization(bearer)) =
TypedHeader::<Authorization<Bearer>>::from_request(req)
.await
.map_err(|_| AuthError::InvalidToken)?;
// Decode the user data
dbg!("{:#?}",bearer.token());
dbg!("{:#?}",decode::<Claims>(bearer.token(), &KEYS.decoding, &Validation::default()));
let token_data = decode::<Claims>(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,
}

365
src/handlers/kratos.rs Normal file
View File

@ -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<DataDBs>, Extension(req_cli): Extension<Arc<CookieStoreMutex>>) -> Html<&'static str> {
pub async fn registration_handler(header: HeaderMap, Extension(dbs): Extension<DataDBs>,Extension(req_cli): Extension<Arc<CookieStoreMutex>>) -> 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,&reg_url,user).await {
Ok(res) => {
println!("Result {:?}",res.status);
html = format!(
r#"
<h1>Register</h1>
<a href='{}/login'>login</a>
<div>Status: {} </div>
"#,&reqenv.websrvr_url(),res.status);
},
Err(e) => {
eprintln!("{}",e);
html = format!(
r#"
<h1>Login</h1>
<div>Error: {} </div>
"#,e);
}
}
},
Err(e) => {
eprintln!("{}",e);
html = format!(
r#"
<h1>Login</h1>
<div>Error: {} </div>
"#,e);
},
}
Html(html)
}
pub async fn login_handler(header: HeaderMap, Extension(dbs): Extension<DataDBs>,Extension(req_cli): Extension<Arc<CookieStoreMutex>>) -> 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,&reg_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#"
<h1>Login</h1>
<div><a href='{}/whoami/{}?token={}'>who am i ?</a></div>
<div><a href='{}/logout/{}?token={}'>logout</a></div>
<div>Status: {} </div>
"#,
&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#"
<h1>Login</h1>
<div>Status: {} </div>
<div><a href='{}/registration'>Registration</a></div>
"#,res.status,&reqenv.websrvr_url());
}
},
Err(e) => {
eprintln!("{}",e);
html = format!(
r#"
<h1>Login</h1>
<div><a href='{}/registration'>Registration</a></div>
<div>Error: {} </div>
"#,&reqenv.websrvr_url(),e);
}
}
},
Err(e) => {
eprintln!("{}",e);
html = format!(
r#"
<h1>Login</h1>
<div>Error: {} </div>
"#,e);
},
};
Html(html)
}
pub async fn recovery_handler() -> Html<&'static str> {
Html("<h1>Hello, World!</h1>")
}
pub async fn whoami(id: String, token: String,
header: HeaderMap, dbs: DataDBs,req_cli: Arc<CookieStoreMutex>) -> 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,&reg_url,user).await {
match get_kratos_user(&id,&token,reqenv.websrvr().signin,&req_cli,&reqid.csrf,&reqid.cookie,&reg_url).await {
Ok(res) => {
println!("Result {:?}",res.status);
if res.status == StatusCode::OK {
html = format!(
r#"
<h1>whoami</h1>
<div>Status: {}</div>
<div><a href='{}/logout/{}?token={}'>logout</a></div>
"#,res.status,
&reqenv.websrvr_url(),id,token)
} else {
html = format!(
r#"
<h1>whoami</h1>
<div>Status: {}</div>
<div><a href='{}/login'>login</a></div>
"#,res.status, &reqenv.websrvr_url())
}
},
Err(e) => {
eprintln!("{}",e);
html = format!(
r#"
<h1>whoami</h1>
<div><a href='{}/login'>Login</a></div>
<div>Error: {} </div>
"#,&reqenv.websrvr_url(),e);
}
}
},
Err(e) => {
eprintln!("{}",e);
html = format!(
r#"
<h1>whoami</h1>
<div>Error: {} </div>
"#,e);
},
};
html
}
#[derive(Deserialize)]
pub struct QueryToken {
pub token: String,
}
pub async fn whoami_handler(Path(id): Path<String>, query: Query<QueryToken>,
header: HeaderMap, Extension(dbs): Extension<DataDBs>,Extension(req_cli): Extension<Arc<CookieStoreMutex>>) -> impl IntoResponse {
Html(whoami(id,query.token.to_string(),header,dbs,req_cli).await)
}
pub async fn logout_handler(Path(id): Path<String>, urlquery: Query<QueryToken>, header: HeaderMap, Extension(dbs): Extension<DataDBs>,Extension(req_cli): Extension<Arc<CookieStoreMutex>>) -> 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,&reg_url,user).await {
match logout_kratos_user(&id,&urlquery.token.to_string(),reqenv.websrvr().signin,&req_cli,&reqid.csrf,&reqid.cookie,&reg_url).await {
Ok(res) => {
println!("Result {:?}",res.status);
html = format!(
r#"
<h1>Logout</h1>
<a href='{}/login'>login</a>
<div>Status: {}</div>
"#,reqenv.websrvr_url(),res.status);
},
Err(e) => {
eprintln!("{}",e);
html = format!(
r#"
<h1>Logout</h1>
<div>Error: {} </div>
"#,e);
}
};
},
Err(e) => {
eprintln!("{}",e);
html = format!(
r#"
<h1>Logout</h1>
<div>Error: {} </div>
"#,e);
},
};
Html(html)
}
pub async fn verification_handler() -> Html<&'static str> {
Html("<h1>Hello, World!</h1>")
}
pub async fn settings_handler() -> Html<&'static str> {
Html("<h1>Hello, World!</h1>")
}
pub async fn welcome_handler() -> Html<&'static str> {
Html("<h1>Hello, World!</h1>")
}
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))
}

68
src/handlers/router.rs Normal file
View File

@ -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<String>,query: Option<Query<String>>, Extension(dbs): Extension<DataDBs>) -> Html<&'static str> {
// pub async fn html_handler(Path(path): Path<String>,Extension(dbs): Extension<DataDBs>) -> Html<&'static str> {
pub async fn html_handler(header: HeaderMap, Path(name): Path<String>,Extension(dbs): Extension<DataDBs>) -> 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("<h1>Hello, World!</h1>")
}
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::<tower::timeout::error::Elapsed>() {
return (StatusCode::REQUEST_TIMEOUT, Cow::from("request timed out"));
}
if error.is::<tower::load_shed::error::Overloaded>() {
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))
}

133
src/handlers/sessions.rs Normal file
View File

@ -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<B> FromRequest<B> for UserIdFromSession
where
B: Send,
{
type Rejection = (StatusCode, &'static str);
async fn from_request(req: &mut RequestParts<B>) -> Result<Self, Self::Rejection> {
let Extension(store) = Extension::<MemoryStore>::from_request(req)
.await
.expect("`MemoryStore` extension missing");
let cookie = Option::<TypedHeader<Cookie>>::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::<UserId>("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())
}
}

633
src/kratos.rs Normal file
View File

@ -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<String> {
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<String, serde_json::Value>;
#[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<HashMap::<String,KratosItemProps>>,
}
#[derive(Debug, Deserialize,Default)]
#[allow(dead_code)]
pub struct KratosUser {
pub traits: HashMap::<String,KratosTraitItem>,
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<String>,
#[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<KratosNode>,
}
#[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<serde_json::Value>,
pub schema_id: String,
pub schema_url: String,
pub state: String,
pub state_changed_at: String,
//pub traits: Vec<serde_json::Value>,
pub updated_at: String,
//pub verifiable_addresses: Vec<serde_json::Value>,
}
#[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<serde_json::Value>,
}
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::<String,KratosItemProps> {
let mut props: HashMap::<String,KratosItemProps> = 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::<String,KratosTraitItem> {
let idschema: JsonMap = load_idschema(path);
// dbg!("{:#?}",&idschema["properties"]["traits"]["properties"]);
let mut traits: HashMap::<String,KratosTraitItem> = 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::<String,String>,traits: HashMap::<String,KratosTraitItem>) -> 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,String>) -> 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<CookieStoreMutex>) -> Result<reqwest::Client> {
//pub fn get_req_cli(cfg: SigninServer) -> Result<reqwest::Client> {
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<HeaderMap> {
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<CookieStoreMutex>) -> 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<CookieStoreMutex>) {
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<CookieStoreMutex>, csrf_token: &str, cookie: &str, url: &str) -> Result<KratosReqId> {
// pub async fn get_flow_csrf(token: &str,cfg: SigninServer, csrf_token: &str, cookie: &str, url: &str) -> Result<KratosReqId> {
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::<KratosResponse>().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::<Vec<&KratosNode>>().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::<serde_json::Value>().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::<Vec<&KratosNode>>().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<CookieStoreMutex>, csrf_token: &str, cookie: &str, url: &str, data: String,) -> Result<OnKratosReq> {
// pub async fn on_kratos_user(token: &str, cfg: SigninServer, csrf_token: &str, cookie: &str,url: &str, data: String,) -> Result<OnKratosReq> {
// 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::<serde_json::Value>().await {
match res.json::<KratosSessionResponse>().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<CookieStoreMutex>, csrf_token: &str, cookie: &str, url: &str) -> Result<OnKratosReq> {
//pub async fn get_kratos_user(id: &str,token: &str,cfg: SigninServer, csrf_token: &str, cookie: &str, url: &str) -> Result<OnKratosReq> {
// 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::<serde_json::Value>().await {
//match res.json::<KratosSessionResponse>().await {
Ok(data) => {
// dbg!("{:#?}",&data);
session_resp = KratosSessionResponse::default();
// if url.contains("logout") {
// session_resp = KratosSessionResponse::default();
// } else {
// match res.json::<KratosSession>().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<CookieStoreMutex>, csrf_token: &str, cookie: &str, url: &str) -> Result<OnKratosReq> {
// 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::<serde_json::Value>().await {
//match res.json::<KratosSessionResponse>().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::<KratosSession>().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 })
}

413
src/main.rs Normal file
View File

@ -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<T> = std::result::Result<T, Box<dyn std::error::Error>>;
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<HeaderValue> = 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<String> = 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<AppEnv> {
// 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<String> = 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(())
}

66
src/state.rs Normal file
View File

@ -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<RwLock<State>>;
#[derive(Default)]
pub struct State {
pub db: HashMap<String, Bytes>,
}
async fn kv_get(
Path(key): Path<String>,
Extension(state): Extension<SharedState>,
) -> Result<Bytes, StatusCode> {
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<String>,
ContentLengthLimit(bytes): ContentLengthLimit<Bytes, { 1024 * 5_000 }>, // ~5mb
Extension(state): Extension<SharedState>,
) {
state.write().unwrap().db.insert(key, bytes);
}
async fn list_keys(Extension(state): Extension<SharedState>) -> String {
let db = &state.read().unwrap().db;
db.keys()
.map(|key| key.to_string())
.collect::<Vec<String>>()
.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))
}

7
src/utils.rs Normal file
View File

@ -0,0 +1,7 @@
//
/*! libresignin
*/
// Copyright 2022, Jesús Pérez Lorenzo
//
pub mod reqtasks;
pub mod reqenv;

141
src/utils/reqenv.rs Normal file
View File

@ -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<HeaderValue>,
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<String> {
self.req.token_from_header()
}
#[allow(clippy::missing_errors_doc)]
pub async fn token_session(&self, login: &LoginRequest) -> anyhow::Result<String> {
self.req.token_session(login).await
}
#[allow(clippy::missing_errors_doc)]
pub async fn user_authentication(&self) -> anyhow::Result<UserCtx> {
self.req.user_authentication().await
}
#[allow(clippy::missing_errors_doc)]
pub async fn get_user(&self) -> User {
self.req.get_user().await
}
}

453
src/utils/reqtasks.rs Normal file
View File

@ -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<String,String> {
// self.req
// .uri()
// .query()
// .map_or_else(HashMap::new, |v| {
// url::form_urlencoded::parse(v.as_bytes())
// .into_owned()
// .collect()
// })
// }
/// Get Lang list from header - accept-language
/// get first one but only root part as first two characters (es in case of es-ES)
/// if lang is not in `WebServer.langs`it will fallback to `Config.default_lang`
#[must_use]
pub fn lang(&self) -> String {
#[allow(unused_assignments)]
let mut lang = self.config().default_lang;
let default_lang = self.config().default_lang;
let langs = self.websrvr().langs;
// As fallback set default_lang
lang = default_lang;
// Get langs list from header
if let Some(languages) = self.header.get("accept-language") {
match languages.to_str() {
Ok(list_lngs) => {
// Get only first one
if let Some(full_lng) = list_lngs.split(',').collect::<Vec<_>>().first() {
// Get root_lng part
let mut end: usize = 0;
full_lng
.chars()
.into_iter()
.take(2)
.for_each(|x| end += x.len_utf8());
let root_lng = &full_lng[..end];
// Filter to check if is in `Config.langs`
let list: Vec<String> = langs
.iter()
.filter(|lng| lng.as_str() == root_lng)
.cloned()
.collect();
if let Some(lng) = list.get(0) {
// Set if it is found and available
lang = lng.to_owned();
}
}
}
Err(e) => println!("Error: {}", e),
}
}
// println!("Lang: {}",&lang);
lang
}
/// Get `Schema` Config
pub async fn is_admin(&self, ky: String) -> bool {
self.env().appkey == ky
}
/// Render a template with context and load `data_path` from `root_path` in case is not empty
/// it use a `data_hash` for context with data if it is not empty
/// if errors it will render a `fallback_template`
#[allow(clippy::too_many_arguments,clippy::missing_errors_doc)]
pub async fn render_template(
&self,
ctx: &mut tera::Context,
root_path: &str,
template_name: &str,
fallback_template: &str,
data_path: &str,
data_hash: &mut HashMap<String, String>,
app_module: &str,
) -> Result<String,AppError> {
let tera: &mut Tera = &mut self.tera(); //.to_owned();
if ! app_module.is_empty() {
AppData::load_modules(&self.env(), &app_module,ctx);
}
Ok(if data_path.is_empty() {
tera
.render(&template_name, &ctx)
.map_err(|e| AppError::ErrorInternalServerError(format!("Error: {}", e)))?
} else {
let render_fallback = |e| -> Result<String,AppError> {
println!("Error parsing data {}:\n{}", &data_path, e);
Ok(tera
.render(fallback_template, &tera::Context::new())
.map_err(|e| AppError::ErrorInternalServerError(format!("Template error {}",e)))?)
};
match hash_from_data(format!("{}/{}", &root_path, &data_path).as_str(), ctx, data_hash, true) {
Ok(_) => {
let mut need_reload = false;
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<String, String>,
app_module: &str,
) -> Result<String,AppError> {
let tera = &self.tera();
let res = match self
.render_template(
ctx,
root_path,
template_name,
fallback_template,
data_path,
data_hash,
app_module,
)
.await
{
Ok(res) => res,
Err(e) => {
println!(
"Error render template + data {} + {}:\n{}",
&template_name, &data_path, e
);
tera
.render(&fallback_template, &tera::Context::new())
.map_err(|e| AppError::ErrorInternalServerError(format!("Template error {}",e)))?
}
};
// Ok(HttpResponse::Ok().content_type("text/html").body(res))
Ok(res)
}
#[allow(clippy::missing_errors_doc)]
pub async fn render_to(
&self,
template_name: &str,
data_path: &str,
data_hash: &mut HashMap<String, String>,
app_module: &str,
) -> Result<String,AppError> {
let app_env = &self.env();
// From Default Context
let mut ctx = self.ctx().to_owned();
let fallback_template = "index.html";
self
.render_page(
&mut ctx,
app_env.get_curr_websrvr_config().templates_path.as_str(),
template_name,
fallback_template,
data_path,
data_hash,
app_module,
)
.await
}
#[allow(clippy::missing_errors_doc)]
pub fn token_from_header(&self) -> anyhow::Result<String> {
if let Some(header) = self.header.get("authorization") {
let auth_header = match from_utf8(header.as_bytes()) {
Ok(v) => v,
Err(_) => return Err(AuthError::NoAuthHeaderFoundError.into()),
};
if !auth_header.starts_with(BEARER_PREFIX) {
return Err(AuthError::InvalidAuthHeaderFormatError.into());
}
let without_prefix = auth_header.trim_start_matches(BEARER_PREFIX);
Ok(without_prefix.to_owned())
} else {
Err(AuthError::NoAuthHeaderFoundError.into())
}
}
#[allow(clippy::missing_errors_doc)]
pub async fn token_session(&self, login: &LoginRequest) -> anyhow::Result<String> {
// dbg!("{}",login);
if let Some(user) = &self.auth_store.users
.read()
.await
.iter()
.find(|(_, v)| v.name == *login.name)
// .filter(|(_, v)| *v.name == name)
// .nth(0)
{
let shdw = &self.auth_store.shadows.read().await;
if let Some(usr_shdw) = shdw.get(&String::from(user.0)) {
// println!("{}",&usr_shdw.passwd);
// println!("{}",&login.passwd);
// println!("{}",&user.0);
if usr_shdw.passwd == login.passwd {
// println!("OK ");
let token = uuid::Uuid::new_v4().to_string();
self.auth_store.sessions.write().await.insert(token.clone(), String::from(user.0));
return Ok(token);
}
}
Err(AuthError::UserNotFoundError.into())
//shdw.get(usr_key);
} else {
Err(AuthError::UserNotFoundError.into())
}
}
#[allow(clippy::missing_errors_doc)]
pub async fn user_authentication(&self) -> anyhow::Result<UserCtx> {
let token = self.token_from_header().unwrap_or_else(|e| {
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<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())
}
}
}