chore: add code
This commit is contained in:
parent
f71f0cc03b
commit
814021eeac
79
Cargo.toml
Normal file
79
Cargo.toml
Normal 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
98
src/defs.rs
Normal 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
9
src/handlers.rs
Normal 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
196
src/handlers/jwt.rs
Normal 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
365
src/handlers/kratos.rs
Normal 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,®_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,®_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,®_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#"
|
||||
<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,®_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#"
|
||||
<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
68
src/handlers/router.rs
Normal 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
133
src/handlers/sessions.rs
Normal 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
633
src/kratos.rs
Normal 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
413
src/main.rs
Normal 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
66
src/state.rs
Normal 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
7
src/utils.rs
Normal 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
141
src/utils/reqenv.rs
Normal 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
453
src/utils/reqtasks.rs
Normal 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())
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user