chore: add src slq and Cargo
This commit is contained in:
parent
59478a3581
commit
0231efcd7a
78
Cargo.toml
Normal file
78
Cargo.toml
Normal file
@ -0,0 +1,78 @@
|
||||
[package]
|
||||
name = "docserver"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
#publish = false
|
||||
publish = true
|
||||
|
||||
[registry]
|
||||
default = "inrepo"
|
||||
|
||||
#[package.metadata.docs.rs]
|
||||
#all-features = true
|
||||
#rustdoc-args = ["--html-in-header", "rusdoc/header.html"]
|
||||
|
||||
[dependencies]
|
||||
log = { version = "0.4.19", features = ["max_level_trace","release_max_level_trace"], package = "log" }
|
||||
axum = { git = "https://github.com/tokio-rs/axum.git", branch = "main" }
|
||||
axum-server = { version = "0.5.1", features = ["tls-rustls"] }
|
||||
tokio = { version = "1.29.1", features = ["full"] }
|
||||
tower = { version = "0.4.13", features = ["util", "filter"] }
|
||||
tower-http = { version = "0.4.1", features = ["fs", "cors", "trace", "add-extension", "auth", "compression-full"] }
|
||||
tower-cookies = "0.9"
|
||||
tracing = "0.1.37"
|
||||
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
|
||||
|
||||
casbin = { version = "2.0.9", features = ["cached","explain","logging"], optional = true}
|
||||
pasetoken-lib= {path = "./libs/pasetoken", package = "pasetoken-lib" }
|
||||
pasetors = { version = "0.6.7" }
|
||||
|
||||
serde = { version = "1.0.171", features = ["derive"] }
|
||||
serde_derive = "1.0.171"
|
||||
serde_json = "1.0.103"
|
||||
toml = "0.7.6"
|
||||
|
||||
clap = { version = "4.3.16", features = ["derive"] }
|
||||
git-version = "0.3.5"
|
||||
once_cell = "1.18.0"
|
||||
|
||||
hyper = { version = "0.14.27", features = ["full"] }
|
||||
tera = "1.19.0"
|
||||
html-minifier = "4.0.0"
|
||||
urlencoding = "2.1.2"
|
||||
password-hash = { version = "0.5", features = ["alloc", "rand_core"] }
|
||||
rand_core = { version = "0.6", features = ["getrandom"] }
|
||||
argon2 = { version = "0.5", default-features = false, features = ["alloc", "simple"]}
|
||||
rand_chacha = "0.3.1"
|
||||
async-trait = "0.1.71"
|
||||
async-session = "3.0.0"
|
||||
async-sqlx-session = { version = "0.4.0", features = ["sqlite"] }
|
||||
|
||||
forwarded-header-value = "0.1.1"
|
||||
lettre = { version = "0.10.4", features = ["smtp-transport", "tokio1", "tokio1-native-tls", "builder"] }
|
||||
|
||||
chrono = "0.4.26"
|
||||
uuid = { version = "1.4.1", features = ["v4", "serde"] }
|
||||
rand = "0.8.5"
|
||||
walkdir = "2.3.3"
|
||||
binascii = "0.1.4"
|
||||
anyhow = "1.0.72"
|
||||
|
||||
totp-rs = { version = "5.0.2", features = ["qr","otpauth"] }
|
||||
base32 = "0.4.0"
|
||||
zxcvbn = "2.2.2"
|
||||
|
||||
futures= "0.3.28"
|
||||
sqlx = { version = "0.6.3", features = ["all-types", "all-databases","runtime-tokio-native-tls"] }
|
||||
#sqlx = { version = "0.7.1", features = ["all-databases","runtime-tokio-native-tls"] }
|
||||
|
||||
[dev-dependencies]
|
||||
async-std = { version = "1.12.0", features = ["default","attributes"] }
|
||||
|
||||
# [target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
# getrandom = { version = "0.2", features = ["js"] }
|
||||
|
||||
[features]
|
||||
default = ["casbin"]
|
||||
#default = ["authstore"]
|
||||
authstore = []
|
9
sql/check.sql
Normal file
9
sql/check.sql
Normal file
@ -0,0 +1,9 @@
|
||||
SELECT EXISTS (
|
||||
SELECT
|
||||
name
|
||||
FROM
|
||||
sqlite_schema
|
||||
WHERE
|
||||
type='table' AND
|
||||
name='users'
|
||||
);
|
27
sql/users.sql
Normal file
27
sql/users.sql
Normal file
@ -0,0 +1,27 @@
|
||||
DROP TABLE users;
|
||||
CREATE TABLE IF NOT EXISTS users
|
||||
(
|
||||
id INTEGER PRIMARY KEY NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
fullname TEXT NOT NULL,
|
||||
email TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
password TEXT NOT NULL,
|
||||
otp_enabled BOOLEAN FALSE,
|
||||
otp_verified BOOLEAN FALSE,
|
||||
otp_base32 TEXT NOT NULL,
|
||||
otp_auth_url TEXT NOT NULL,
|
||||
otp_defs TEXT NOT NULL,
|
||||
roles TEXT NOT NULL,
|
||||
created TEXT NOT NULL,
|
||||
lastaccess TEXT NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
items TEXT NOT NULL,
|
||||
isadmin BOOLEAN FALSE
|
||||
);
|
||||
CREATE UNIQUE INDEX idx_users_email
|
||||
ON users (email);
|
||||
|
||||
CREATE UNIQUE INDEX idx_users_fullname
|
||||
ON users (name);
|
||||
|
3
sql/view.sql
Normal file
3
sql/view.sql
Normal file
@ -0,0 +1,3 @@
|
||||
.tables
|
||||
PRAGMA table_info(users);
|
||||
|
62
src/defs.rs
Normal file
62
src/defs.rs
Normal file
@ -0,0 +1,62 @@
|
||||
mod config;
|
||||
mod cli;
|
||||
mod from_file;
|
||||
mod filestore;
|
||||
mod session;
|
||||
#[cfg(feature = "authstore")]
|
||||
mod authz;
|
||||
mod appdbs;
|
||||
mod req_headermap;
|
||||
mod req_handler;
|
||||
mod tracedata;
|
||||
mod local;
|
||||
mod mailer;
|
||||
mod totp_mode;
|
||||
mod totp_algorithm;
|
||||
mod app_connect_info;
|
||||
|
||||
pub(crate) use appdbs::AppDBs;
|
||||
pub(crate) use session::{
|
||||
SessionStoreDB,
|
||||
AuthState,
|
||||
Random,
|
||||
};
|
||||
pub(crate) use from_file::{
|
||||
FromFile,
|
||||
load_from_file,
|
||||
DictFromFile,
|
||||
load_dict_from_file,
|
||||
};
|
||||
pub(crate) use filestore::FileStore;
|
||||
#[cfg(feature = "authstore")]
|
||||
pub(crate) use authz::AuthStore;
|
||||
pub(crate) use config::{ServPath,Config};
|
||||
pub(crate) use cli::parse_args;
|
||||
pub(crate) use req_handler::ReqHandler;
|
||||
pub(crate) use req_headermap::ReqHeaderMap;
|
||||
pub(crate) use tracedata::{TraceData,TraceContent, TraceLogContent};
|
||||
pub(crate) use local::Local;
|
||||
pub(crate) use mailer::MailMessage;
|
||||
|
||||
pub(crate) use totp_algorithm::{TotpAlgorithm,deserialize_totp_algorithm};
|
||||
pub(crate) use totp_mode::{TotpMode, deserialize_totp_mode};
|
||||
|
||||
pub(crate) use app_connect_info::AppConnectInfo;
|
||||
|
||||
pub const TOKEN_KEY_VALUE: &str = "tii-cl";
|
||||
pub const TOKEN_AUTH_VALUE: &str = "tii-cl-token";
|
||||
pub const CLAIM_UID: &str = "uid";
|
||||
pub const CLAIM_AUTH: &str = "auth";
|
||||
pub const CLAIM_APP_KEY: &str = "app_key";
|
||||
pub const USER_DATA: &str = "user_data";
|
||||
pub const BEARER: &str = "Bearer";
|
||||
pub const USER_AGENT: &str = "user-agent";
|
||||
pub const FILE_SCHEME: &str = "file:///";
|
||||
pub const SESSION_ID: &str = "sid";
|
||||
pub const ACCEPT_LANGUAGE: &str = "accept-language";
|
||||
pub const X_INTERNAL: &str = "x-internal";
|
||||
pub const X_REAL_IP: &str = "x-real-ip";
|
||||
pub const X_FORWARDED_FOR: &str = "x-forwarded-for";
|
||||
pub const AUTHORIZATION: &str = "authorization";
|
||||
pub const REFERER: &str = "referer";
|
||||
pub const SID_TRACE_FILE: &str = "sid_trace.json";
|
415
src/defs/_config.rs
Normal file
415
src/defs/_config.rs
Normal file
@ -0,0 +1,415 @@
|
||||
use std::collections::HashMap;
|
||||
use serde::{Serialize, Deserialize};
|
||||
//use serde_json::value::{to_value, Value};
|
||||
use log::error;
|
||||
use std::io::{Error, ErrorKind, Result};
|
||||
|
||||
use pasetoken_lib::ConfigPaSeToken;
|
||||
use pasetors::footer::Footer;
|
||||
|
||||
use crate::{
|
||||
CFG_FILE_EXTENSION,
|
||||
FILE_SCHEME,
|
||||
defs::{
|
||||
Local, TotpAlgorithm, TotpMode,
|
||||
deserialize_totp_algorithm,
|
||||
deserialize_totp_mode,
|
||||
FromFile,
|
||||
load_dict_from_file,
|
||||
VecFromFile,
|
||||
load_vec_from_file,
|
||||
},
|
||||
};
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
pub type WebSubMenuItems = Vec<SubMenuItem>;
|
||||
|
||||
// use crate::tools::generate_uuid;
|
||||
|
||||
// fn default_config_empty() -> String {
|
||||
// "".to_string()
|
||||
// }
|
||||
// fn default_config_items() -> HashMap<String,String> {
|
||||
// HashMap::new()
|
||||
// }
|
||||
fn default_config_resource() -> String {
|
||||
"/".to_string()
|
||||
}
|
||||
fn default_config_array_resource() -> Vec<String> {
|
||||
Vec::new()
|
||||
}
|
||||
fn default_config_serv_paths()-> Vec<ServPath> {
|
||||
Vec::new()
|
||||
}
|
||||
fn default_config_dflt_lang() -> String {
|
||||
"en".to_string()
|
||||
}
|
||||
// fn default_server_uid() -> String {
|
||||
// generate_uuid(String::from("abcdef0123456789"))
|
||||
// }
|
||||
fn default_config_org() -> String {
|
||||
"".to_string()
|
||||
}
|
||||
fn default_config_empty() -> String {
|
||||
"".to_string()
|
||||
}
|
||||
fn default_config_locales() -> HashMap<String,Local> {
|
||||
HashMap::new()
|
||||
}
|
||||
fn default_config_tpls() -> HashMap<String,String> {
|
||||
HashMap::new()
|
||||
}
|
||||
fn default_is_restricted() -> bool {
|
||||
false
|
||||
}
|
||||
fn default_config_use_mail() -> bool {
|
||||
false
|
||||
}
|
||||
fn default_config_use_token() -> bool {
|
||||
false
|
||||
}
|
||||
fn default_config_invite_expire() -> u64 {
|
||||
300
|
||||
}
|
||||
fn default_config_web_menu_items() -> Vec<WebMenuItem> {
|
||||
Vec::new()
|
||||
}
|
||||
fn default_web_menu_item_roles() -> Vec<String> {
|
||||
Vec::new()
|
||||
}
|
||||
fn default_auth_roles() -> Vec<String> {
|
||||
Vec::new()
|
||||
}
|
||||
fn default_sub_menu_items() -> Vec<SubMenuItem> {
|
||||
Vec::new()
|
||||
}
|
||||
fn default_config_totp_digits() -> usize {
|
||||
6
|
||||
}
|
||||
fn default_config_totp_algorithm() -> TotpAlgorithm {
|
||||
TotpAlgorithm::default()
|
||||
}
|
||||
fn default_config_totp_mode() -> TotpMode {
|
||||
TotpMode::default()
|
||||
}
|
||||
fn default_config_password_score() -> u8 {
|
||||
0
|
||||
}
|
||||
fn default_config_trace_level() -> u8 {
|
||||
1
|
||||
}
|
||||
#[derive(Debug, Clone, Serialize, Deserialize,Default)]
|
||||
pub struct SubMenuItem {
|
||||
#[serde(default = "default_config_empty")]
|
||||
pub typ: String,
|
||||
#[serde(default = "default_config_empty")]
|
||||
pub srctyp: String,
|
||||
#[serde(default = "default_config_empty")]
|
||||
pub text: String,
|
||||
#[serde(default = "default_config_empty")]
|
||||
pub url: String,
|
||||
#[serde(default = "default_web_menu_item_roles")]
|
||||
pub roles: Vec<String>,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize,Default)]
|
||||
pub struct WebMenuItem {
|
||||
#[serde(default = "default_config_resource")]
|
||||
pub root_path: String,
|
||||
#[serde(default = "default_config_empty")]
|
||||
pub typ: String,
|
||||
#[serde(default = "default_config_empty")]
|
||||
pub srctyp: String,
|
||||
#[serde(default = "default_config_empty")]
|
||||
pub text: String,
|
||||
#[serde(default = "default_config_empty")]
|
||||
pub url: String,
|
||||
#[serde(default = "default_web_menu_item_roles")]
|
||||
pub roles: Vec<String>,
|
||||
#[serde(default = "default_sub_menu_items")]
|
||||
pub items: Vec<SubMenuItem>,
|
||||
}
|
||||
impl VecFromFile for WebMenuItem {
|
||||
fn fix_root_path<WebMenuItem>(&mut self, _root_path: String ) {
|
||||
}
|
||||
}
|
||||
// impl WebMenuItem {
|
||||
// pub fn load_items(path_items: String) -> Vec<WebMenuItem> {
|
||||
// if !path_items.is_empty() {
|
||||
// load_vec_from_file(&path_items.to_owned(), "web_menu.items").unwrap_or_else(|e|{
|
||||
// print!("Error loading sid_settings from {}: {}",&path_items,e);
|
||||
// error!("Error loading sid_settings from {}: {}",&path_items,e);
|
||||
// Vec::new()
|
||||
// })
|
||||
// } else {
|
||||
// Vec::new()
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize,Default)]
|
||||
pub struct UiConfig {
|
||||
#[serde(default = "default_config_empty")]
|
||||
pub css_link: String,
|
||||
#[serde(default = "default_config_empty")]
|
||||
pub js_link: String,
|
||||
#[serde(default = "default_config_empty")]
|
||||
pub other_css_link: String,
|
||||
#[serde(default = "default_config_empty")]
|
||||
pub other_js_link: String,
|
||||
#[serde(default = "default_config_empty")]
|
||||
pub main_js_link: String,
|
||||
#[serde(default = "default_config_empty")]
|
||||
pub utils_js_link: String,
|
||||
#[serde(default = "default_config_web_menu_items")]
|
||||
pub web_menu_items: Vec<WebMenuItem>,
|
||||
}
|
||||
|
||||
/// ServPath collects dir path to server as static content
|
||||
#[derive(Debug, Clone, Serialize, Deserialize,Default)]
|
||||
pub struct ServPath {
|
||||
#[serde(default = "default_config_empty")]
|
||||
pub src_path: String,
|
||||
#[serde(default = "default_config_empty")]
|
||||
pub url_path: String,
|
||||
#[serde(default = "default_config_empty")]
|
||||
pub not_found: String,
|
||||
#[serde(default = "default_config_empty")]
|
||||
pub not_auth: String,
|
||||
#[serde(default = "default_is_restricted")]
|
||||
pub is_restricted: bool,
|
||||
#[serde(default = "default_config_empty")]
|
||||
pub redirect_to: String,
|
||||
}
|
||||
/// Config collects config values.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize,Default)]
|
||||
pub struct Config {
|
||||
pub hostport: String,
|
||||
pub bind: String,
|
||||
pub port: u16,
|
||||
pub protocol: String,
|
||||
#[serde(default = "default_config_org")]
|
||||
pub org: String,
|
||||
pub name: String,
|
||||
pub verbose: u8,
|
||||
pub prefix: String,
|
||||
pub resources_path: String,
|
||||
pub cert_file: String,
|
||||
pub key_file: String,
|
||||
//pub certs_store_path: String,
|
||||
//pub cert_file_sufix: String,
|
||||
#[serde(default = "default_config_array_resource")]
|
||||
pub allow_origin: Vec<String>,
|
||||
#[serde(default = "default_config_array_resource")]
|
||||
pub langs: Vec<String>,
|
||||
#[serde(default = "default_config_dflt_lang")]
|
||||
pub dflt_lang: String,
|
||||
#[serde(default = "default_config_empty")]
|
||||
pub path_locales_config: String,
|
||||
#[serde(default = "default_config_locales")]
|
||||
pub locales: HashMap<String,Local>,
|
||||
|
||||
// Some paths
|
||||
#[serde(default = "default_config_resource")]
|
||||
pub root_path: String,
|
||||
#[serde(default = "default_config_resource")]
|
||||
pub defaults_path: String,
|
||||
#[serde(default = "default_config_serv_paths")]
|
||||
pub serv_paths: Vec<ServPath>,
|
||||
#[serde(default = "default_config_resource")]
|
||||
pub docs_index: String,
|
||||
|
||||
#[serde(default = "default_config_resource")]
|
||||
pub templates_path: String,
|
||||
#[serde(default = "default_config_resource")]
|
||||
pub html_url: String,
|
||||
#[serde(default = "default_config_resource")]
|
||||
pub assets_url: String,
|
||||
|
||||
#[serde(default = "default_config_resource")]
|
||||
pub users_store_uri: String,
|
||||
#[serde(default = "default_config_empty")]
|
||||
pub user_store_access: String,
|
||||
#[serde(default = "default_auth_roles")]
|
||||
pub auth_roles: Vec<String>,
|
||||
#[serde(default = "default_config_empty")]
|
||||
pub trace_store_uri: String,
|
||||
#[serde(default = "default_config_trace_level")]
|
||||
pub trace_level: u8,
|
||||
|
||||
#[serde(default = "default_config_empty")]
|
||||
pub signup_mode: String,
|
||||
#[serde(default = "default_config_invite_expire")]
|
||||
pub invite_expire: u64,
|
||||
#[serde(default = "default_config_use_token")]
|
||||
pub use_token: bool,
|
||||
#[serde(default = "default_config_totp_digits")]
|
||||
pub totp_digits: usize,
|
||||
#[serde(default = "default_config_totp_mode", deserialize_with = "deserialize_totp_mode")]
|
||||
pub totp_mode: TotpMode,
|
||||
#[serde(default = "default_config_totp_algorithm", deserialize_with = "deserialize_totp_algorithm")]
|
||||
pub totp_algorithm: TotpAlgorithm,
|
||||
|
||||
#[serde(default = "default_config_password_score")]
|
||||
pub password_score: u8,
|
||||
#[serde(default = "default_config_empty")]
|
||||
pub admin_fields: String,
|
||||
#[serde(default = "default_config_empty")]
|
||||
pub mail_from: String,
|
||||
#[serde(default = "default_config_empty")]
|
||||
pub mail_reply_to: String,
|
||||
|
||||
#[serde(default = "default_config_use_mail")]
|
||||
pub use_mail: bool,
|
||||
#[serde(default = "default_config_empty")]
|
||||
pub smtp: String,
|
||||
#[serde(default = "default_config_empty")]
|
||||
pub smtp_auth: String,
|
||||
|
||||
// #[cfg(feature = "authstore")]
|
||||
#[serde(default = "default_config_resource")]
|
||||
pub authz_store_uri: String,
|
||||
// #[cfg(feature = "casbin")]
|
||||
#[serde(default = "default_config_empty")]
|
||||
pub authz_model_path: String,
|
||||
// #[cfg(feature = "casbin")]
|
||||
#[serde(default = "default_config_empty")]
|
||||
pub authz_policy_path: String,
|
||||
#[serde(default = "default_config_empty")]
|
||||
|
||||
pub session_store_uri: String,
|
||||
#[serde(default = "default_config_empty")]
|
||||
pub session_store_file: String,
|
||||
pub session_expire: u64,
|
||||
|
||||
pub paseto: ConfigPaSeToken,
|
||||
|
||||
pub ui: UiConfig,
|
||||
#[serde(default = "default_config_tpls")]
|
||||
pub tpls: HashMap<String,String>,
|
||||
|
||||
#[serde(default = "default_config_resource")]
|
||||
pub path_menu_items: String,
|
||||
#[serde(default = "default_config_resource")]
|
||||
pub path_serv_paths: String,
|
||||
}
|
||||
|
||||
// impl FromFile for Config {
|
||||
// fn fix_root_path<Config>(&mut self, root_path: String ) {
|
||||
// if root_path != self.root_path {
|
||||
// self.root_path = root_path.to_owned();
|
||||
// }
|
||||
// if self.root_path.is_empty() || ! Path::new(&self.root_path).exists() {
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
impl Config {
|
||||
fn fix_item_path(&mut self, item: String ) -> String {
|
||||
if !item.is_empty() && ! Path::new(&item).exists() {
|
||||
format!("{}/{}",&self.root_path,&item)
|
||||
} else {
|
||||
item
|
||||
}
|
||||
}
|
||||
pub fn load_items(&mut self) {
|
||||
// self.fix_root_path::<Config>(self.root_path.to_owned());
|
||||
if !self.path_menu_items.is_empty() {
|
||||
self.path_menu_items = self.fix_item_path(self.path_menu_items.to_owned());
|
||||
self.ui.web_menu_items = load_vec_from_file(&self.path_menu_items.to_owned(), "menu_items").unwrap_or_else(|e|{
|
||||
error!("Error loading menu_items from {}: {}",&self.path_menu_items,e);
|
||||
Vec::new()
|
||||
});
|
||||
}
|
||||
if !self.path_locales_config.is_empty() {
|
||||
self.path_locales_config = self.fix_item_path(self.path_locales_config.to_owned());
|
||||
self.locales = load_dict_from_file(&self.path_locales_config.to_owned(), "locales").unwrap_or_else(|e|{
|
||||
error!("Error loading locales from {}: {}",&self.path_locales_config,e);
|
||||
HashMap::new()
|
||||
});
|
||||
}
|
||||
if self.users_store_uri.starts_with(FILE_SCHEME) {
|
||||
self.users_store_uri = format!("{}{}",
|
||||
FILE_SCHEME, self.fix_item_path(self.users_store_uri.replace(FILE_SCHEME,"")));
|
||||
}
|
||||
if self.session_store_uri.starts_with(FILE_SCHEME) {
|
||||
self.session_store_uri = format!("{}{}",
|
||||
FILE_SCHEME, self.fix_item_path(self.session_store_uri.replace(FILE_SCHEME,"")));
|
||||
}
|
||||
if self.trace_store_uri.starts_with(FILE_SCHEME) {
|
||||
self.trace_store_uri = format!("{}{}",
|
||||
FILE_SCHEME, self.fix_item_path(self.trace_store_uri.replace(FILE_SCHEME,"")));
|
||||
}
|
||||
self.cert_file = self.fix_item_path(self.cert_file.to_owned());
|
||||
self.key_file = self.fix_item_path(self.key_file.to_owned());
|
||||
self.templates_path = self.fix_item_path(self.templates_path.to_owned());
|
||||
self.defaults_path = self.fix_item_path(self.defaults_path.to_owned());
|
||||
|
||||
#[cfg(feature = "authstore")]
|
||||
if self.authz_store_uri.starts_with(FILE_SCHEME) {
|
||||
self.authz_store_uri = format!("{}{}",
|
||||
FILE_SCHEME, self.fix_item_path(self.authz_store_uri.replace(FILE_SCHEME,"")));
|
||||
}
|
||||
#[cfg(feature = "casbin")]
|
||||
{
|
||||
self.authz_model_path = self.fix_item_path(self.authz_model_path.to_owned());
|
||||
self.authz_policy_path = self.fix_item_path(self.authz_policy_path.to_owned());
|
||||
}
|
||||
self.paseto.public_path = self.fix_item_path(self.paseto.public_path.to_owned());
|
||||
self.paseto.secret_path = self.fix_item_path(self.paseto.secret_path.to_owned());
|
||||
// Loading paseto
|
||||
(self.paseto.public_data, self.paseto.secret_data) = self.paseto.load_data();
|
||||
self.paseto.footer = ConfigPaSeToken::make_footer(
|
||||
self.paseto.map_footer.to_owned()
|
||||
).unwrap_or(Footer::new());
|
||||
}
|
||||
#[allow(dead_code)]
|
||||
pub fn to_json(&self) -> String {
|
||||
serde_json::to_string(self).unwrap_or_else(|e|{
|
||||
println!("Error to convert Config to json: {}",e);
|
||||
String::from("")
|
||||
})
|
||||
}
|
||||
// #[allow(dead_code)]
|
||||
// pub fn st_html_path(&self) -> &'static str {
|
||||
// Box::leak(self.html_path.to_owned().into_boxed_str())
|
||||
// }
|
||||
#[allow(dead_code)]
|
||||
pub fn full_html_url(&self, url: &str) -> String {
|
||||
format!("{}://{}:{}/{}",
|
||||
&self.protocol,&self.bind,&self.port,
|
||||
url
|
||||
)
|
||||
}
|
||||
pub fn load_from_file<'a>(file_cfg: &str, name: &str) -> Result<Self> {
|
||||
let item_cfg: Self;
|
||||
let file_path: String;
|
||||
if file_cfg.contains(CFG_FILE_EXTENSION) {
|
||||
file_path = file_cfg.to_string();
|
||||
} else {
|
||||
file_path = format!("{}{}",file_cfg.to_string(),CFG_FILE_EXTENSION);
|
||||
}
|
||||
let config_content: &'a str;
|
||||
match std::fs::read_to_string(&file_path) {
|
||||
Ok(cfgcontent) => config_content = Box::leak(cfgcontent.into_boxed_str()),
|
||||
Err(e) =>
|
||||
return Err(Error::new(
|
||||
ErrorKind::InvalidInput,
|
||||
format!("Error read {}: {}",&file_path,e)
|
||||
)),
|
||||
};
|
||||
// match toml::from_str::<T>(&config_content) {
|
||||
match toml::from_str::<Self>(&config_content) {
|
||||
Ok(cfg) => item_cfg = cfg,
|
||||
Err(e) => return Err(Error::new(
|
||||
ErrorKind::InvalidInput,
|
||||
format!("Error loading config {}: {}",&file_path,e)
|
||||
)),
|
||||
};
|
||||
log::info!("Loaded {} config from: {}", &name, &file_path);
|
||||
Ok(item_cfg)
|
||||
}
|
||||
|
||||
}
|
18
src/defs/app_connect_info.rs
Normal file
18
src/defs/app_connect_info.rs
Normal file
@ -0,0 +1,18 @@
|
||||
use std::net::SocketAddr;
|
||||
use hyper::server::conn::AddrStream;
|
||||
use axum::extract::connect_info::Connected;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AppConnectInfo {
|
||||
pub remote_addr: SocketAddr,
|
||||
pub local_addr: SocketAddr,
|
||||
}
|
||||
|
||||
impl Connected<&AddrStream> for AppConnectInfo {
|
||||
fn connect_info(target: &AddrStream) -> Self {
|
||||
AppConnectInfo {
|
||||
remote_addr: target.remote_addr(),
|
||||
local_addr: target.local_addr(),
|
||||
}
|
||||
}
|
||||
}
|
82
src/defs/appdbs.rs
Normal file
82
src/defs/appdbs.rs
Normal file
@ -0,0 +1,82 @@
|
||||
|
||||
use tera::{Tera,Context};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
#[cfg(feature = "casbin")]
|
||||
use casbin::{CoreApi,Enforcer};
|
||||
#[cfg(feature = "casbin")]
|
||||
use std::{
|
||||
path::Path,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use crate::defs::{
|
||||
Config,
|
||||
SessionStoreDB,
|
||||
};
|
||||
|
||||
#[cfg(feature = "authstore")]
|
||||
use crate::defs::AuthStore;
|
||||
|
||||
use crate::users::UserStore;
|
||||
|
||||
#[cfg(feature = "authstore")]
|
||||
#[derive(Clone)]
|
||||
pub struct AppDBs {
|
||||
pub config: Config,
|
||||
pub auth_store: AuthStore,
|
||||
pub sessions_store_db: SessionStoreDB,
|
||||
pub user_store: UserStore,
|
||||
pub tera: Tera,
|
||||
pub context: Context,
|
||||
}
|
||||
#[cfg(feature = "authstore")]
|
||||
impl AppDBs {
|
||||
pub fn new(config: &Config, store: SessionStoreDB, user_store: UserStore, tera: Tera, context: Context) -> Self {
|
||||
Self {
|
||||
config: config.to_owned(),
|
||||
auth_store: AuthStore::new(&config.authz_store_uri),
|
||||
sessions_store_db: store,
|
||||
user_store,
|
||||
tera,
|
||||
context,
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "casbin")]
|
||||
#[derive(Clone)]
|
||||
pub struct AppDBs {
|
||||
pub config: Config,
|
||||
pub enforcer: Arc<RwLock<Enforcer>>,
|
||||
pub sessions_store_db: SessionStoreDB,
|
||||
pub user_store: UserStore,
|
||||
pub tera: Tera,
|
||||
pub context: Context,
|
||||
}
|
||||
#[cfg(feature = "casbin")]
|
||||
impl AppDBs {
|
||||
pub fn new(config: &Config, store: SessionStoreDB, user_store: UserStore, enforcer: Arc<RwLock<Enforcer>>, tera: Tera, context: Context) -> Self {
|
||||
Self {
|
||||
config: config.to_owned(),
|
||||
enforcer,
|
||||
sessions_store_db: store,
|
||||
user_store,
|
||||
tera,
|
||||
context,
|
||||
}
|
||||
}
|
||||
pub async fn create_enforcer(model_path: &'static str, policy_path: &'static str) -> Arc<RwLock<Enforcer>> {
|
||||
if ! Path::new(&model_path).exists() {
|
||||
panic!("model path: {} not exists",&model_path);
|
||||
}
|
||||
if ! Path::new(&policy_path).exists() {
|
||||
panic!("policy path: {} not exists",&policy_path);
|
||||
}
|
||||
let e = Enforcer::new(model_path, policy_path)
|
||||
.await
|
||||
.expect("can read casbin model and policy files");
|
||||
Arc::new(RwLock::new(e))
|
||||
}
|
||||
}
|
||||
|
||||
|
67
src/defs/authz.rs
Normal file
67
src/defs/authz.rs
Normal file
@ -0,0 +1,67 @@
|
||||
use std::{
|
||||
fs,
|
||||
collections::HashMap,
|
||||
};
|
||||
use serde::{Deserialize,Serialize};
|
||||
|
||||
use crate::defs::{
|
||||
UserRole,
|
||||
user_role::deserialize_user_role,
|
||||
};
|
||||
|
||||
use log::{info,error};
|
||||
|
||||
use crate::FILE_SCHEME;
|
||||
|
||||
#[derive(Deserialize,Serialize,Clone,Debug,Default)]
|
||||
pub struct Authz {
|
||||
pub user_id: String,
|
||||
pub name: String,
|
||||
pub passwd: String,
|
||||
pub init: String,
|
||||
pub last: String,
|
||||
pub change: bool,
|
||||
#[serde(deserialize_with = "deserialize_user_role")]
|
||||
pub role: UserRole,
|
||||
}
|
||||
|
||||
// pub type AuthzMap = Arc<RwLock<HashMap<String,Authz>>>;
|
||||
#[derive(Clone,Debug)]
|
||||
pub struct AuthStore {
|
||||
pub authz: HashMap<String,Authz>,
|
||||
// pub authz: AuthzMap,
|
||||
}
|
||||
impl AuthStore {
|
||||
pub fn new(authz_store_uri: &str) -> Self {
|
||||
Self {
|
||||
authz: AuthStore::create_authz_map(authz_store_uri),
|
||||
// authz: Arc::new(RwLock::new(AuthStore::create_authz_map(config))),
|
||||
}
|
||||
}
|
||||
pub fn load_authz_from_fs(target: &str) -> HashMap<String, Authz> {
|
||||
let data_content = fs::read_to_string(target).unwrap_or_else(|_|String::from(""));
|
||||
if ! data_content.contains("role") {
|
||||
println!("Error no 'role' in authz from store: {}", &target);
|
||||
return HashMap::new()
|
||||
}
|
||||
let authz: HashMap<String, Authz> = toml::from_str(&data_content).unwrap_or_else(|e| {
|
||||
println!("Error loading authz from store: {} error: {}", &target,e);
|
||||
HashMap::new()
|
||||
});
|
||||
authz
|
||||
}
|
||||
|
||||
pub fn create_authz_map(authz_store_uri: &str) -> HashMap<String, Authz> {
|
||||
let mut authz = HashMap::new();
|
||||
if authz_store_uri.starts_with(FILE_SCHEME) {
|
||||
let authz_store = authz_store_uri.replace(FILE_SCHEME, "");
|
||||
authz = AuthStore::load_authz_from_fs(&authz_store);
|
||||
if !authz.is_empty() {
|
||||
info!("Authz loaded successfully ({})", &authz.len());
|
||||
}
|
||||
} else {
|
||||
error!("Store not set for authz store: {}", authz_store_uri);
|
||||
}
|
||||
authz
|
||||
}
|
||||
}
|
95
src/defs/cli.rs
Normal file
95
src/defs/cli.rs
Normal file
@ -0,0 +1,95 @@
|
||||
|
||||
use clap::Parser;
|
||||
|
||||
use pasetoken_lib::pasetoken::ConfigPaSeToken;
|
||||
//use pasetors::footer::Footer;
|
||||
|
||||
use crate::{PKG_NAME,PKG_VERSION};
|
||||
|
||||
use crate::tools::generate_uuid;
|
||||
|
||||
// Use `clap` to parse command line options with `derive` mode
|
||||
#[derive(Parser, Debug)]
|
||||
pub struct Cli {
|
||||
|
||||
/// * Configuration TOML file-path to run `WebServer` REQUIRED
|
||||
#[clap(short = 'c', long = "config", value_parser, display_order=1)]
|
||||
pub config_path: Option<String>,
|
||||
|
||||
/// Generate id key for identification
|
||||
#[clap(short = 'i', long = "id", action, display_order=2)]
|
||||
pub create_id: bool,
|
||||
|
||||
/// Path to generate PaSeTo public and secret keys to use for tokens
|
||||
#[clap(short = 'p', long = "pasetoken", value_parser, display_order=3)]
|
||||
pub pasetokeys_path: Option<String>,
|
||||
|
||||
/// Settings-toml-file to Generate PaSeTo Token (-o to filepath)
|
||||
#[clap(short = 't', long = "token", value_parser, display_order=4)]
|
||||
pub make_token: Option<String>,
|
||||
|
||||
/// Output path to save generated token with `make_token` (-t)
|
||||
#[clap(short = 'o', long = "output", value_parser, display_order=5)]
|
||||
pub output_path: Option<String>,
|
||||
|
||||
/// Show version
|
||||
#[clap(short = 'v', long = "version", action, display_order=6)]
|
||||
pub version: bool,
|
||||
}
|
||||
|
||||
|
||||
/// Runs some options from command line like: genererate id, PaSeTo keys, Token.
|
||||
/// Set TOML config-path to load web-server settings
|
||||
pub fn parse_args() -> String {
|
||||
let args = Cli::parse();
|
||||
if let Some(pasetokeys_path) = args.pasetokeys_path {
|
||||
match pasetoken_lib::generate_keys(&pasetokeys_path,false) {
|
||||
Ok(_) => println!("PaSeTo keys generated to: {}", &pasetokeys_path),
|
||||
Err(e) => eprint!("Error generating keys: {}",e),
|
||||
};
|
||||
std::process::exit(0);
|
||||
}
|
||||
let output_path = if let Some(out_path) = args.output_path {
|
||||
out_path
|
||||
} else {
|
||||
String::from("")
|
||||
};
|
||||
if let Some(def_path) = args.make_token {
|
||||
match ConfigPaSeToken::token_from_file_defs(&def_path, &output_path) {
|
||||
Ok(token) => {
|
||||
if output_path.is_empty() {
|
||||
println!("{}",&token)
|
||||
} else {
|
||||
println!("Token from defs {} generated to {}",&def_path,&output_path)
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
if output_path.is_empty() {
|
||||
eprintln!("Error generating token from file defs {} error: {}",
|
||||
&def_path, e
|
||||
)
|
||||
} else {
|
||||
eprintln!("Error generating token from file defs {} to {} error: {}",
|
||||
&def_path, &output_path, e
|
||||
)
|
||||
}
|
||||
},
|
||||
};
|
||||
std::process::exit(0);
|
||||
}
|
||||
if args.create_id {
|
||||
println!("{}",generate_uuid(String::from("abcdef0123456789")));
|
||||
std::process::exit(0);
|
||||
}
|
||||
if args.version {
|
||||
println!("{} version: {}",PKG_NAME,PKG_VERSION);
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
let config_path = args.config_path.unwrap_or(String::from(""));
|
||||
if config_path.is_empty() {
|
||||
eprintln!("No config-file found");
|
||||
std::process::exit(2);
|
||||
}
|
||||
config_path
|
||||
}
|
383
src/defs/config.rs
Normal file
383
src/defs/config.rs
Normal file
@ -0,0 +1,383 @@
|
||||
use std::collections::HashMap;
|
||||
use serde::{Serialize, Deserialize};
|
||||
//use serde_json::value::{to_value, Value};
|
||||
use log::error;
|
||||
use pasetoken_lib::ConfigPaSeToken;
|
||||
use pasetors::footer::Footer;
|
||||
|
||||
use crate::{
|
||||
FILE_SCHEME,
|
||||
defs::{
|
||||
Local, TotpAlgorithm, TotpMode,
|
||||
deserialize_totp_algorithm,
|
||||
deserialize_totp_mode,
|
||||
FromFile,
|
||||
load_from_file,
|
||||
load_dict_from_file,
|
||||
},
|
||||
};
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
// use crate::tools::generate_uuid;
|
||||
// fn default_server_uid() -> String {
|
||||
// generate_uuid(String::from("abcdef0123456789"))
|
||||
// }
|
||||
|
||||
fn default_config_resource() -> String {
|
||||
"/".to_string()
|
||||
}
|
||||
fn default_config_array_resource() -> Vec<String> {
|
||||
Vec::new()
|
||||
}
|
||||
fn default_config_serv_paths()-> Vec<ServPath> {
|
||||
Vec::new()
|
||||
}
|
||||
fn default_config_dflt_lang() -> String {
|
||||
"en".to_string()
|
||||
}
|
||||
fn default_config_org() -> String {
|
||||
"".to_string()
|
||||
}
|
||||
fn default_config_empty() -> String {
|
||||
"".to_string()
|
||||
}
|
||||
fn default_config_locales() -> HashMap<String,Local> {
|
||||
HashMap::new()
|
||||
}
|
||||
fn default_config_tpls() -> HashMap<String,String> {
|
||||
HashMap::new()
|
||||
}
|
||||
fn default_is_restricted() -> bool {
|
||||
false
|
||||
}
|
||||
fn default_config_use_mail() -> bool {
|
||||
false
|
||||
}
|
||||
fn default_config_use_token() -> bool {
|
||||
false
|
||||
}
|
||||
fn default_config_invite_expire() -> u64 {
|
||||
300
|
||||
}
|
||||
fn default_config_web_menu_items() -> Vec<WebMenuItem> {
|
||||
Vec::new()
|
||||
}
|
||||
fn default_web_menu_item_roles() -> Vec<String> {
|
||||
Vec::new()
|
||||
}
|
||||
fn default_auth_roles() -> Vec<String> {
|
||||
Vec::new()
|
||||
}
|
||||
fn default_sub_menu_items() -> Vec<SubMenuItem> {
|
||||
Vec::new()
|
||||
}
|
||||
fn default_config_totp_digits() -> usize {
|
||||
6
|
||||
}
|
||||
fn default_config_totp_algorithm() -> TotpAlgorithm {
|
||||
TotpAlgorithm::default()
|
||||
}
|
||||
fn default_config_totp_mode() -> TotpMode {
|
||||
TotpMode::default()
|
||||
}
|
||||
fn default_config_password_score() -> u8 {
|
||||
0
|
||||
}
|
||||
fn default_config_trace_level() -> u8 {
|
||||
1
|
||||
}
|
||||
#[derive(Debug, Clone, Serialize, Deserialize,Default)]
|
||||
pub struct SubMenuItem {
|
||||
#[serde(default = "default_config_empty")]
|
||||
pub typ: String,
|
||||
#[serde(default = "default_config_empty")]
|
||||
pub srctyp: String,
|
||||
#[serde(default = "default_config_empty")]
|
||||
pub text: String,
|
||||
#[serde(default = "default_config_empty")]
|
||||
pub url: String,
|
||||
#[serde(default = "default_web_menu_item_roles")]
|
||||
pub roles: Vec<String>,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize,Default)]
|
||||
pub struct WebMenuItem {
|
||||
#[serde(default = "default_config_resource")]
|
||||
pub root_path: String,
|
||||
#[serde(default = "default_config_empty")]
|
||||
pub typ: String,
|
||||
#[serde(default = "default_config_empty")]
|
||||
pub srctyp: String,
|
||||
#[serde(default = "default_config_empty")]
|
||||
pub text: String,
|
||||
#[serde(default = "default_config_empty")]
|
||||
pub url: String,
|
||||
#[serde(default = "default_web_menu_item_roles")]
|
||||
pub roles: Vec<String>,
|
||||
#[serde(default = "default_sub_menu_items")]
|
||||
pub items: Vec<SubMenuItem>,
|
||||
}
|
||||
#[derive(Debug, Clone, Serialize, Deserialize,Default)]
|
||||
pub struct DataMenuItems {
|
||||
#[serde(default = "default_config_web_menu_items")]
|
||||
pub web_menu_items: Vec<WebMenuItem>,
|
||||
}
|
||||
impl FromFile for DataMenuItems {
|
||||
fn fix_root_path<DataMenuItems>(&mut self, _root_path: String ) {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize,Default)]
|
||||
pub struct UiConfig {
|
||||
#[serde(default = "default_config_empty")]
|
||||
pub css_link: String,
|
||||
#[serde(default = "default_config_empty")]
|
||||
pub js_link: String,
|
||||
#[serde(default = "default_config_empty")]
|
||||
pub other_css_link: String,
|
||||
#[serde(default = "default_config_empty")]
|
||||
pub other_js_link: String,
|
||||
#[serde(default = "default_config_empty")]
|
||||
pub main_js_link: String,
|
||||
#[serde(default = "default_config_empty")]
|
||||
pub utils_js_link: String,
|
||||
#[serde(default = "default_config_web_menu_items")]
|
||||
pub web_menu_items: Vec<WebMenuItem>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize,Default)]
|
||||
pub struct DataServPath {
|
||||
#[serde(default = "default_config_serv_paths")]
|
||||
pub serv_paths: Vec<ServPath>,
|
||||
}
|
||||
impl FromFile for DataServPath {
|
||||
fn fix_root_path<DataServPath>(&mut self, _root_path: String ) {
|
||||
}
|
||||
}
|
||||
/// ServPath collects dir path to server as static content
|
||||
#[derive(Debug, Clone, Serialize, Deserialize,Default)]
|
||||
pub struct ServPath {
|
||||
#[serde(default = "default_config_empty")]
|
||||
pub src_path: String,
|
||||
#[serde(default = "default_config_empty")]
|
||||
pub url_path: String,
|
||||
#[serde(default = "default_config_empty")]
|
||||
pub not_found: String,
|
||||
#[serde(default = "default_config_empty")]
|
||||
pub not_auth: String,
|
||||
#[serde(default = "default_is_restricted")]
|
||||
pub is_restricted: bool,
|
||||
#[serde(default = "default_config_empty")]
|
||||
pub redirect_to: String,
|
||||
}
|
||||
/// Config collects config values.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize,Default)]
|
||||
pub struct Config {
|
||||
pub hostport: String,
|
||||
pub bind: String,
|
||||
pub port: u16,
|
||||
pub protocol: String,
|
||||
#[serde(default = "default_config_org")]
|
||||
pub org: String,
|
||||
pub name: String,
|
||||
pub verbose: u8,
|
||||
pub prefix: String,
|
||||
pub resources_path: String,
|
||||
pub cert_file: String,
|
||||
pub key_file: String,
|
||||
//pub certs_store_path: String,
|
||||
//pub cert_file_sufix: String,
|
||||
#[serde(default = "default_config_array_resource")]
|
||||
pub allow_origin: Vec<String>,
|
||||
#[serde(default = "default_config_array_resource")]
|
||||
pub langs: Vec<String>,
|
||||
#[serde(default = "default_config_dflt_lang")]
|
||||
pub dflt_lang: String,
|
||||
#[serde(default = "default_config_empty")]
|
||||
pub path_locales_config: String,
|
||||
#[serde(default = "default_config_locales")]
|
||||
pub locales: HashMap<String,Local>,
|
||||
|
||||
// Some paths
|
||||
#[serde(default = "default_config_resource")]
|
||||
pub root_path: String,
|
||||
#[serde(default = "default_config_resource")]
|
||||
pub defaults_path: String,
|
||||
#[serde(default = "default_config_serv_paths")]
|
||||
pub serv_paths: Vec<ServPath>,
|
||||
#[serde(default = "default_config_resource")]
|
||||
pub docs_index: String,
|
||||
|
||||
#[serde(default = "default_config_resource")]
|
||||
pub templates_path: String,
|
||||
#[serde(default = "default_config_resource")]
|
||||
pub html_url: String,
|
||||
#[serde(default = "default_config_resource")]
|
||||
pub assets_url: String,
|
||||
|
||||
#[serde(default = "default_config_resource")]
|
||||
pub users_store_uri: String,
|
||||
#[serde(default = "default_config_empty")]
|
||||
pub user_store_access: String,
|
||||
#[serde(default = "default_auth_roles")]
|
||||
pub auth_roles: Vec<String>,
|
||||
#[serde(default = "default_config_empty")]
|
||||
pub trace_store_uri: String,
|
||||
#[serde(default = "default_config_trace_level")]
|
||||
pub trace_level: u8,
|
||||
|
||||
#[serde(default = "default_config_empty")]
|
||||
pub signup_mode: String,
|
||||
#[serde(default = "default_config_invite_expire")]
|
||||
pub invite_expire: u64,
|
||||
#[serde(default = "default_config_use_token")]
|
||||
pub use_token: bool,
|
||||
#[serde(default = "default_config_totp_digits")]
|
||||
pub totp_digits: usize,
|
||||
#[serde(default = "default_config_totp_mode", deserialize_with = "deserialize_totp_mode")]
|
||||
pub totp_mode: TotpMode,
|
||||
#[serde(default = "default_config_totp_algorithm", deserialize_with = "deserialize_totp_algorithm")]
|
||||
pub totp_algorithm: TotpAlgorithm,
|
||||
|
||||
#[serde(default = "default_config_password_score")]
|
||||
pub password_score: u8,
|
||||
#[serde(default = "default_config_empty")]
|
||||
pub admin_fields: String,
|
||||
#[serde(default = "default_config_empty")]
|
||||
pub mail_from: String,
|
||||
#[serde(default = "default_config_empty")]
|
||||
pub mail_reply_to: String,
|
||||
|
||||
#[serde(default = "default_config_use_mail")]
|
||||
pub use_mail: bool,
|
||||
#[serde(default = "default_config_empty")]
|
||||
pub smtp: String,
|
||||
#[serde(default = "default_config_empty")]
|
||||
pub smtp_auth: String,
|
||||
|
||||
// #[cfg(feature = "authstore")]
|
||||
#[serde(default = "default_config_resource")]
|
||||
pub authz_store_uri: String,
|
||||
// #[cfg(feature = "casbin")]
|
||||
#[serde(default = "default_config_empty")]
|
||||
pub authz_model_path: String,
|
||||
// #[cfg(feature = "casbin")]
|
||||
#[serde(default = "default_config_empty")]
|
||||
pub authz_policy_path: String,
|
||||
#[serde(default = "default_config_empty")]
|
||||
|
||||
pub session_store_uri: String,
|
||||
#[serde(default = "default_config_empty")]
|
||||
pub session_store_file: String,
|
||||
pub session_expire: u64,
|
||||
|
||||
pub paseto: ConfigPaSeToken,
|
||||
|
||||
pub ui: UiConfig,
|
||||
#[serde(default = "default_config_tpls")]
|
||||
pub tpls: HashMap<String,String>,
|
||||
|
||||
#[serde(default = "default_config_resource")]
|
||||
pub path_menu_items: String,
|
||||
#[serde(default = "default_config_resource")]
|
||||
pub path_serv_paths: String,
|
||||
}
|
||||
|
||||
impl FromFile for Config {
|
||||
fn fix_root_path<Config>(&mut self, root_path: String ) {
|
||||
if root_path != self.root_path {
|
||||
self.root_path = root_path.to_owned();
|
||||
}
|
||||
if self.root_path.is_empty() || ! Path::new(&self.root_path).exists() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Config {
|
||||
fn fix_item_path(&mut self, item: String ) -> String {
|
||||
if !item.is_empty() && ! Path::new(&item).exists() {
|
||||
format!("{}/{}",&self.root_path,&item)
|
||||
} else {
|
||||
item
|
||||
}
|
||||
}
|
||||
pub fn load_items(&mut self) {
|
||||
self.fix_root_path::<Config>(self.root_path.to_owned());
|
||||
if !self.path_menu_items.is_empty() {
|
||||
self.path_menu_items = self.fix_item_path(self.path_menu_items.to_owned());
|
||||
let data_menu_items = load_from_file(&self.path_menu_items.to_owned(), "menu_items").unwrap_or_else(|e|{
|
||||
error!("Error loading menu_items from {}: {}",&self.path_menu_items,e);
|
||||
DataMenuItems::default()
|
||||
});
|
||||
self.ui.web_menu_items = data_menu_items.web_menu_items;
|
||||
}
|
||||
if !self.path_serv_paths.is_empty() {
|
||||
self.path_serv_paths = self.fix_item_path(self.path_serv_paths.to_owned());
|
||||
let data_serv_paths = load_from_file(&self.path_serv_paths.to_owned(), "serv_paths").unwrap_or_else(|e|{
|
||||
error!("Error loading serv_paths from {}: {}",&self.path_serv_paths,e);
|
||||
DataServPath::default()
|
||||
});
|
||||
self.serv_paths = data_serv_paths.serv_paths;
|
||||
}
|
||||
if !self.path_locales_config.is_empty() {
|
||||
self.path_locales_config = self.fix_item_path(self.path_locales_config.to_owned());
|
||||
self.locales = load_dict_from_file(&self.path_locales_config.to_owned(), "locales").unwrap_or_else(|e|{
|
||||
error!("Error loading locales from {}: {}",&self.path_locales_config,e);
|
||||
HashMap::new()
|
||||
});
|
||||
}
|
||||
if self.users_store_uri.starts_with(FILE_SCHEME) {
|
||||
self.users_store_uri = format!("{}{}",
|
||||
FILE_SCHEME, self.fix_item_path(self.users_store_uri.replace(FILE_SCHEME,"")));
|
||||
}
|
||||
if self.session_store_uri.starts_with(FILE_SCHEME) {
|
||||
self.session_store_uri = format!("{}{}",
|
||||
FILE_SCHEME, self.fix_item_path(self.session_store_uri.replace(FILE_SCHEME,"")));
|
||||
}
|
||||
if self.trace_store_uri.starts_with(FILE_SCHEME) {
|
||||
self.trace_store_uri = format!("{}{}",
|
||||
FILE_SCHEME, self.fix_item_path(self.trace_store_uri.replace(FILE_SCHEME,"")));
|
||||
}
|
||||
self.cert_file = self.fix_item_path(self.cert_file.to_owned());
|
||||
self.key_file = self.fix_item_path(self.key_file.to_owned());
|
||||
self.templates_path = self.fix_item_path(self.templates_path.to_owned());
|
||||
self.defaults_path = self.fix_item_path(self.defaults_path.to_owned());
|
||||
|
||||
#[cfg(feature = "authstore")]
|
||||
if self.authz_store_uri.starts_with(FILE_SCHEME) {
|
||||
self.authz_store_uri = format!("{}{}",
|
||||
FILE_SCHEME, self.fix_item_path(self.authz_store_uri.replace(FILE_SCHEME,"")));
|
||||
}
|
||||
#[cfg(feature = "casbin")]
|
||||
{
|
||||
self.authz_model_path = self.fix_item_path(self.authz_model_path.to_owned());
|
||||
self.authz_policy_path = self.fix_item_path(self.authz_policy_path.to_owned());
|
||||
}
|
||||
self.paseto.public_path = self.fix_item_path(self.paseto.public_path.to_owned());
|
||||
self.paseto.secret_path = self.fix_item_path(self.paseto.secret_path.to_owned());
|
||||
(self.paseto.public_data, self.paseto.secret_data) = self.paseto.load_data();
|
||||
self.paseto.footer = ConfigPaSeToken::make_footer(
|
||||
self.paseto.map_footer.to_owned()
|
||||
).unwrap_or(Footer::new());
|
||||
}
|
||||
#[allow(dead_code)]
|
||||
pub fn to_json(&self) -> String {
|
||||
serde_json::to_string(self).unwrap_or_else(|e|{
|
||||
println!("Error to convert Config to json: {}",e);
|
||||
String::from("")
|
||||
})
|
||||
}
|
||||
// #[allow(dead_code)]
|
||||
// pub fn st_html_path(&self) -> &'static str {
|
||||
// Box::leak(self.html_path.to_owned().into_boxed_str())
|
||||
// }
|
||||
#[allow(dead_code)]
|
||||
pub fn full_html_url(&self, url: &str) -> String {
|
||||
format!("{}://{}:{}/{}",
|
||||
&self.protocol,&self.bind,&self.port,
|
||||
url
|
||||
)
|
||||
}
|
||||
}
|
368
src/defs/filestore.rs
Normal file
368
src/defs/filestore.rs
Normal file
@ -0,0 +1,368 @@
|
||||
|
||||
use async_session::{Result, Session, SessionStore};
|
||||
use anyhow::anyhow;
|
||||
use async_trait::async_trait;
|
||||
use std::{
|
||||
fs,
|
||||
path::Path,
|
||||
};
|
||||
use walkdir::{DirEntry, WalkDir};
|
||||
use binascii::bin2hex;
|
||||
|
||||
#[allow(unused)]
|
||||
fn is_hidden(entry: &DirEntry) -> bool {
|
||||
entry.file_name()
|
||||
.to_str()
|
||||
.map(|s| s.starts_with("."))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FileStore {
|
||||
pub sess_path: String,
|
||||
pub ses_file: String,
|
||||
}
|
||||
#[async_trait]
|
||||
impl SessionStore for FileStore {
|
||||
async fn load_session(&self, cookie_value: String) -> Result<Option<Session>> {
|
||||
let id = Session::id_from_cookie_value(&cookie_value)?;
|
||||
log::trace!("loading session by id `{}`", &id);
|
||||
//dbg!("loading session by id `{}`", &id);
|
||||
self.load_session_file(&id).await
|
||||
}
|
||||
async fn store_session(&self, session: Session) -> Result<Option<String>> {
|
||||
log::trace!("storing session by id `{}`", session.id());
|
||||
let id_filename = match self.get_path(session.id()) {
|
||||
Ok(res) => res,
|
||||
Err(e) => {
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
// let mut out_buffer = [0u8; 100];
|
||||
// let id_filename = if let Ok(res) = bin2hex(session.id().as_bytes(),&mut out_buffer) {
|
||||
// std::str::from_utf8(res)?.to_owned()
|
||||
// } else {
|
||||
// return Ok(None);
|
||||
// };
|
||||
let sess_id_path = format!("{}/{}", self.sess_path, &id_filename);
|
||||
if ! Path::new(&sess_id_path).exists() {
|
||||
fs::create_dir(&sess_id_path)?;
|
||||
}
|
||||
let content_session = serde_json::to_string(&session)?;
|
||||
fs::write(&format!("{}/{}",&sess_id_path, self.ses_file), content_session)?;
|
||||
//session.reset_data_changed(); // do not need is it is serialized in file write
|
||||
Ok(session.into_cookie_value())
|
||||
}
|
||||
async fn destroy_session(&self, session: Session) -> Result {
|
||||
log::trace!("destroying session by id `{}`", session.id());
|
||||
match self.get_path(session.id()) {
|
||||
Ok(res) => match self.get_session_id_path(&res) {
|
||||
Ok(session_id_path) => Ok(fs::remove_file(&session_id_path)?),
|
||||
Err(e) => Err(e),
|
||||
},
|
||||
Err(e) => Err(e)
|
||||
}
|
||||
// let mut out_buffer = [0u8; 100];
|
||||
// if let Ok(res) = bin2hex(session.id().as_bytes(),&mut out_buffer) {
|
||||
// let id_filename = std::str::from_utf8(res)?.to_owned();
|
||||
// Ok(fs::remove_file(
|
||||
// &format!("{}/{}/{}",self.sess_path, &id_filename, self.ses_file)
|
||||
// )?)
|
||||
// } else {
|
||||
// Ok(())
|
||||
// }
|
||||
}
|
||||
async fn clear_store(&self) -> Result {
|
||||
log::trace!("clearing memory store");
|
||||
let sess_path = format!("{}", self.sess_path);
|
||||
fs::remove_dir_all(&sess_path)?;
|
||||
fs::create_dir(&sess_path)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl FileStore {
|
||||
/// Create a new instance of FilesStore
|
||||
pub fn check_paths(&self) -> Result {
|
||||
if ! Path::new(&self.sess_path).exists() {
|
||||
fs::create_dir(&self.sess_path)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
pub fn get_path(&self,id: &str) -> Result<String> {
|
||||
let mut out_buffer = [0u8; 100];
|
||||
match bin2hex(&id.as_bytes(),&mut out_buffer) {
|
||||
Ok(res) => Ok(std::str::from_utf8(res)?.to_owned()),
|
||||
Err(e) => {
|
||||
Err(anyhow!("Filename path {} not generated: {:?}", &id, &e))
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn get_session_id_path(&self,id_filename: &str) -> Result<String> {
|
||||
let session_id_path = format!("{}/{}/{}",self.sess_path, id_filename, &self.ses_file);
|
||||
if ! Path::new(&session_id_path).exists() {
|
||||
Err(anyhow!("Filename path {} not found: {}", id_filename, &session_id_path ))
|
||||
} else {
|
||||
Ok(session_id_path)
|
||||
}
|
||||
}
|
||||
/// As session Id from `async_session` comes in base64 it will be not valid for OS filename
|
||||
/// `bin2hex` pass id to hex as bytes and from there to string or viceversa
|
||||
pub async fn load_session_file(&self, id: &str) -> Result<Option<Session>> {
|
||||
let session_id_path = match self.get_path(id) {
|
||||
Ok(res) => match self.get_session_id_path(&res) {
|
||||
Ok(path) => path,
|
||||
Err(e) => return Err(e),
|
||||
},
|
||||
Err(e) => {
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
// let mut out_buffer = [0u8; 100];
|
||||
// let id_filename = if let Ok(res) = bin2hex(&id.as_bytes(),&mut out_buffer) {
|
||||
// std::str::from_utf8(res)?.to_owned()
|
||||
// } else {
|
||||
// return Ok(None);
|
||||
// };
|
||||
// let session_id_path = format!("{}/{}/{}",self.sess_path, &id_filename, &self.ses_file);
|
||||
// dbg!(&session_id_path);
|
||||
if ! Path::new(&session_id_path).exists() {
|
||||
dbg!("No path: {}", &session_id_path);
|
||||
// let sess_id_path = format!("{}/{}", self.sess_path, &id_filename);
|
||||
// if ! Path::new(&sess_id_path).exists() {
|
||||
// fs::create_dir(&sess_id_path)?;
|
||||
// }
|
||||
// create
|
||||
}
|
||||
if let Ok(session_content) = fs::read_to_string(&session_id_path) {
|
||||
// match serde_json::from_str::<serde_json::Value>(&session_content) {
|
||||
match serde_json::from_str::<Session>(&session_content) {
|
||||
Ok(session) => {
|
||||
Ok(session.validate())
|
||||
},
|
||||
Err(e) => {
|
||||
dbg!("Error loading session content from {}: {}",&session_id_path, e);
|
||||
//log::error!("Error loading session content from {}: {}",&session_id_path, e);
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
#[allow(dead_code)]
|
||||
pub async fn cleanup(&self) -> Result {
|
||||
log::trace!("cleaning up file store...");
|
||||
let mut count: usize = 0;
|
||||
let sess_path = format!("{}", self.sess_path);
|
||||
let walker = WalkDir::new(&sess_path).into_iter();
|
||||
for entry in walker.filter_entry(|e| !is_hidden(e)) {
|
||||
match entry {
|
||||
Ok(dir_entry) => {
|
||||
// println!("{}", &dir_entry.path().display());
|
||||
if ! Path::new(&dir_entry.path()).is_dir() {
|
||||
continue;
|
||||
}
|
||||
let session_file = format!("{}/{}",&dir_entry.path().display(), &self.ses_file);
|
||||
let id_path = format!("{}",&dir_entry.path().display());
|
||||
let id = id_path.replace(&sess_path,"");
|
||||
if let Some(session) = self.load_session_file(&id).await.unwrap_or_default() {
|
||||
if session.is_expired() {
|
||||
let _ = fs::remove_file(&session_file);
|
||||
log::trace!("found {} expired session",&id_path);
|
||||
count +=1;
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => println!("Error on {}: {}", &sess_path, e)
|
||||
}
|
||||
}
|
||||
log::trace!("found {} expired session {} cleaned",&sess_path, count);
|
||||
Ok(())
|
||||
}
|
||||
#[allow(dead_code)]
|
||||
pub async fn count(&self) -> usize {
|
||||
let mut count: usize = 0;
|
||||
let sess_path = format!("{}", self.sess_path);
|
||||
let walker = WalkDir::new(&sess_path).into_iter();
|
||||
for entry in walker.filter_entry(|e| !is_hidden(e)) {
|
||||
match entry {
|
||||
Ok(dir_entry) => {
|
||||
// println!("{}", &dir_entry.path().display());
|
||||
if ! Path::new(&dir_entry.path()).is_dir() {
|
||||
continue;
|
||||
}
|
||||
let session_file = format!("{}/{}",&dir_entry.path().display(), &self.ses_file);
|
||||
if Path::new(&session_file).exists() {
|
||||
count +=1;
|
||||
}
|
||||
},
|
||||
Err(e) => println!("Error on {}: {}", &sess_path, e)
|
||||
}
|
||||
}
|
||||
count
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use async_std::task;
|
||||
use std::time::Duration;
|
||||
const TEST_SESS_FILESTORE: &str = "/tmp/test_sess_filestore";
|
||||
const TEST_IDS_FILESTORE: &str = "/tmp/test_ids_filestore";
|
||||
|
||||
#[async_std::test]
|
||||
async fn creating_a_new_session_with_no_expiry() -> Result {
|
||||
let sess_path_store = format!("{}_0", TEST_SESS_FILESTORE);
|
||||
let ids_path_store = format!("{}_0", TEST_IDS_FILESTORE);
|
||||
let _ = fs::remove_dir_all(&sess_path_store);
|
||||
let _ = fs::remove_dir_all(&ids_path_store);
|
||||
let store = FileStore {
|
||||
sess_path: sess_path_store.to_owned(),
|
||||
ses_file: String::from("session"),
|
||||
};
|
||||
store.check_paths()?;
|
||||
let mut session = Session::new();
|
||||
session.insert("key", "Hello")?;
|
||||
let cloned = session.clone();
|
||||
let cookie_value = store.store_session(session).await?.unwrap();
|
||||
assert!(true);
|
||||
let loaded_session = store.load_session(cookie_value).await?.unwrap();
|
||||
assert_eq!(cloned.id(), loaded_session.id());
|
||||
assert_eq!("Hello", &loaded_session.get::<String>("key").unwrap());
|
||||
assert!(!loaded_session.is_expired());
|
||||
assert!(loaded_session.validate().is_some());
|
||||
let _ = fs::remove_dir_all(&sess_path_store);
|
||||
let _ = fs::remove_dir_all(&ids_path_store);
|
||||
Ok(())
|
||||
}
|
||||
#[async_std::test]
|
||||
async fn updating_a_session() -> Result {
|
||||
let sess_path_store = format!("{}_1", TEST_SESS_FILESTORE);
|
||||
let _ = fs::remove_dir_all(&sess_path_store);
|
||||
let store = FileStore {
|
||||
sess_path: sess_path_store.to_owned(),
|
||||
ses_file: String::from("session"),
|
||||
};
|
||||
store.check_paths()?;
|
||||
|
||||
let mut session = Session::new();
|
||||
|
||||
session.insert("key", "value")?;
|
||||
let cookie_value = store.store_session(session).await?.unwrap();
|
||||
|
||||
let mut session = store.load_session(cookie_value.clone()).await?.unwrap();
|
||||
session.insert("key", "other value")?;
|
||||
|
||||
assert_eq!(store.store_session(session).await?, None);
|
||||
let session = store.load_session(cookie_value).await?.unwrap();
|
||||
assert_eq!(&session.get::<String>("key").unwrap(), "other value");
|
||||
fs::remove_dir_all(&sess_path_store)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn updating_a_session_extending_expiry() -> Result {
|
||||
let sess_path_store = format!("{}_2", TEST_SESS_FILESTORE);
|
||||
let _ = fs::remove_dir_all(&sess_path_store);
|
||||
let store = FileStore {
|
||||
sess_path: sess_path_store.to_owned(),
|
||||
ses_file: String::from("session"),
|
||||
};
|
||||
store.check_paths()?;
|
||||
|
||||
let mut session = Session::new();
|
||||
session.expire_in(Duration::from_secs(1));
|
||||
let original_expires = session.expiry().unwrap().clone();
|
||||
let cookie_value = store.store_session(session).await?.unwrap();
|
||||
|
||||
let mut session = store.load_session(cookie_value.clone()).await?.unwrap();
|
||||
|
||||
assert_eq!(session.expiry().unwrap(), &original_expires);
|
||||
session.expire_in(Duration::from_secs(3));
|
||||
let new_expires = session.expiry().unwrap().clone();
|
||||
assert_eq!(None, store.store_session(session).await?);
|
||||
|
||||
let session = store.load_session(cookie_value.clone()).await?.unwrap();
|
||||
assert_eq!(session.expiry().unwrap(), &new_expires);
|
||||
|
||||
task::sleep(Duration::from_secs(3)).await;
|
||||
assert_eq!(None, store.load_session(cookie_value).await?);
|
||||
fs::remove_dir_all(&sess_path_store)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn creating_a_new_session_with_expiry() -> Result {
|
||||
let sess_path_store = format!("{}_3", TEST_SESS_FILESTORE);
|
||||
let _ = fs::remove_dir_all(&sess_path_store);
|
||||
let store = FileStore {
|
||||
sess_path: sess_path_store.to_owned(),
|
||||
ses_file: String::from("session"),
|
||||
};
|
||||
store.check_paths()?;
|
||||
|
||||
let mut session = Session::new();
|
||||
session.expire_in(Duration::from_secs(3));
|
||||
session.insert("key", "value")?;
|
||||
let cloned = session.clone();
|
||||
|
||||
let cookie_value = store.store_session(session).await?.unwrap();
|
||||
|
||||
let loaded_session = store.load_session(cookie_value.clone()).await?.unwrap();
|
||||
assert_eq!(cloned.id(), loaded_session.id());
|
||||
assert_eq!("value", &*loaded_session.get::<String>("key").unwrap());
|
||||
|
||||
assert!(!loaded_session.is_expired());
|
||||
|
||||
task::sleep(Duration::from_secs(3)).await;
|
||||
assert_eq!(None, store.load_session(cookie_value).await?);
|
||||
fs::remove_dir_all(&sess_path_store)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn destroying_a_single_session() -> Result {
|
||||
let sess_path_store = format!("{}_4", TEST_SESS_FILESTORE);
|
||||
let _ = fs::remove_dir_all(&sess_path_store);
|
||||
let store = FileStore {
|
||||
sess_path: sess_path_store.to_owned(),
|
||||
ses_file: String::from("session"),
|
||||
};
|
||||
store.check_paths()?;
|
||||
|
||||
for _ in 0..3i8 {
|
||||
store.store_session(Session::new()).await?;
|
||||
}
|
||||
let cookie = store.store_session(Session::new()).await?.unwrap();
|
||||
assert_eq!(4, store.count().await);
|
||||
let session = store.load_session(cookie.clone()).await?.unwrap();
|
||||
store.destroy_session(session.clone()).await?;
|
||||
assert!(store.load_session(cookie).await.is_err());
|
||||
assert_eq!(3, store.count().await);
|
||||
|
||||
// attempting to destroy the session again IS an ERROR, file should be deleted before
|
||||
assert!(store.destroy_session(session).await.is_err());
|
||||
fs::remove_dir_all(&sess_path_store)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn clearing_the_whole_store() -> Result {
|
||||
let sess_path_store = format!("{}_5", TEST_SESS_FILESTORE);
|
||||
let _ = fs::remove_dir_all(&sess_path_store);
|
||||
let store = FileStore {
|
||||
sess_path: sess_path_store.to_owned(),
|
||||
ses_file: String::from("session"),
|
||||
};
|
||||
store.check_paths()?;
|
||||
|
||||
for _ in 0..3i8 {
|
||||
store.store_session(Session::new()).await?;
|
||||
}
|
||||
assert_eq!(3, store.count().await);
|
||||
store.clear_store().await.unwrap();
|
||||
assert_eq!(0, store.count().await);
|
||||
fs::remove_dir_all(&sess_path_store)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
76
src/defs/from_file.rs
Normal file
76
src/defs/from_file.rs
Normal file
@ -0,0 +1,76 @@
|
||||
use std::io::{Error, ErrorKind, Result};
|
||||
use std::collections::HashMap;
|
||||
use serde::de::DeserializeOwned;
|
||||
|
||||
use log::info;
|
||||
|
||||
pub const CFG_FILE_EXTENSION: &str = ".toml";
|
||||
|
||||
pub trait AppDef {
|
||||
fn app_def<T>(&mut self, root_path: String ); // -> T;
|
||||
}
|
||||
pub trait FromFile {
|
||||
fn fix_root_path<T>(&mut self, root_path: String ); // -> T;
|
||||
}
|
||||
pub fn load_from_file<'a, T: FromFile + DeserializeOwned>(file_cfg: &str, name: &str) -> Result<T> {
|
||||
let item_cfg: T;
|
||||
let file_path: String;
|
||||
if file_cfg.contains(CFG_FILE_EXTENSION) {
|
||||
file_path = file_cfg.to_string();
|
||||
} else {
|
||||
file_path = format!("{}{}",file_cfg.to_string(),CFG_FILE_EXTENSION);
|
||||
}
|
||||
let config_content: &'a str;
|
||||
match std::fs::read_to_string(&file_path) {
|
||||
Ok(cfgcontent) => config_content = Box::leak(cfgcontent.into_boxed_str()),
|
||||
Err(e) =>
|
||||
return Err(Error::new(
|
||||
ErrorKind::InvalidInput,
|
||||
format!("Error read {}: {}",&file_path,e)
|
||||
)),
|
||||
};
|
||||
match toml::from_str::<T>(&config_content) {
|
||||
Ok(cfg) => item_cfg = cfg,
|
||||
Err(e) => return Err(Error::new(
|
||||
ErrorKind::InvalidInput,
|
||||
format!("Error loading config {}: {}",&file_path,e)
|
||||
)),
|
||||
};
|
||||
info!("Loaded {} config from: {}", &name, &file_path);
|
||||
Ok(item_cfg)
|
||||
}
|
||||
|
||||
pub trait DictFromFile {
|
||||
fn fix_root_path<T>(&mut self, root_path: String ); // -> T;
|
||||
}
|
||||
pub fn load_dict_from_file<'a, T: DictFromFile + DeserializeOwned>(file_cfg: &str, name: &str) -> Result<HashMap<String,T>> {
|
||||
let item_cfg: HashMap<String,T>;
|
||||
let file_path: String;
|
||||
if file_cfg.contains(CFG_FILE_EXTENSION) {
|
||||
file_path = file_cfg.to_string();
|
||||
} else {
|
||||
file_path = format!("{}{}",file_cfg.to_string(),CFG_FILE_EXTENSION);
|
||||
}
|
||||
let config_content: &'a str;
|
||||
match std::fs::read_to_string(&file_path) {
|
||||
Ok(cfgcontent) => config_content = Box::leak(cfgcontent.into_boxed_str()),
|
||||
Err(e) =>
|
||||
return Err(Error::new(
|
||||
ErrorKind::InvalidInput,
|
||||
format!("Error read {}: {}",&file_path,e)
|
||||
)),
|
||||
};
|
||||
match toml::from_str::<HashMap<String,T>>(&config_content) {
|
||||
Ok(cfg) => {
|
||||
item_cfg = cfg
|
||||
},
|
||||
Err(e) => {
|
||||
return Err(Error::new(
|
||||
ErrorKind::InvalidInput,
|
||||
format!("Error loading {} {}: {}",name,&file_path,e)
|
||||
))
|
||||
},
|
||||
};
|
||||
info!("Loaded {} config from: {}", name, &file_path);
|
||||
Ok(item_cfg)
|
||||
}
|
43
src/defs/local.rs
Normal file
43
src/defs/local.rs
Normal file
@ -0,0 +1,43 @@
|
||||
use std::collections::HashMap;
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
use crate::defs::DictFromFile;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize ,Default)]
|
||||
pub struct Local {
|
||||
pub id: String,
|
||||
pub itms: HashMap<String,String>
|
||||
}
|
||||
impl DictFromFile for Local {
|
||||
fn fix_root_path<Local>(&mut self, _root_path: String ) {
|
||||
}
|
||||
}
|
||||
|
||||
impl Local {
|
||||
pub fn new(id: String, itms: HashMap<String,String>) -> Self {
|
||||
Self {
|
||||
id,
|
||||
itms
|
||||
}
|
||||
}
|
||||
#[allow(dead_code)]
|
||||
pub fn itm(&self, key: &str, dflt: &str) -> String {
|
||||
match self.itms.get(key) {
|
||||
Some(val) => format!("{}",val),
|
||||
None => if dflt.is_empty() {
|
||||
format!("{}",key)
|
||||
} else {
|
||||
format!("{}",dflt)
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn get_lang(locales: &HashMap<String, Local>, key: &str, dflt: &str) -> Local {
|
||||
match locales.get(key) {
|
||||
Some(val) => val.to_owned(),
|
||||
None => match locales.get(dflt) {
|
||||
Some(val) => val.to_owned(),
|
||||
None => Local::new(String::from(""),HashMap::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
152
src/defs/mailer.rs
Normal file
152
src/defs/mailer.rs
Normal file
@ -0,0 +1,152 @@
|
||||
use lettre::{
|
||||
transport::smtp::authentication::Credentials,
|
||||
AsyncSmtpTransport,
|
||||
AsyncTransport,
|
||||
Tokio1Executor,
|
||||
// Address,
|
||||
message::{
|
||||
Mailbox,
|
||||
header::ContentType,
|
||||
MultiPart,
|
||||
SinglePart,
|
||||
},
|
||||
Message,
|
||||
};
|
||||
|
||||
use crate::defs::AppDBs;
|
||||
use pasetoken_lib::ConfigPaSeToken;
|
||||
use serde_json::json;
|
||||
|
||||
#[derive(Clone,Debug)]
|
||||
pub struct MailMessage {
|
||||
pub from: Mailbox,
|
||||
pub to: Mailbox,
|
||||
pub reply_to: Mailbox,
|
||||
}
|
||||
|
||||
impl MailMessage {
|
||||
pub fn new(from: &str, to: &str, reply: &str) -> anyhow::Result<Self> {
|
||||
let reply_to = if reply.is_empty() {
|
||||
to
|
||||
} else {
|
||||
reply
|
||||
};
|
||||
Ok(
|
||||
Self {
|
||||
from: from.parse()?,
|
||||
to: to.parse()?,
|
||||
reply_to: reply_to.parse()?,
|
||||
}
|
||||
)
|
||||
}
|
||||
pub fn check(app_dbs: &AppDBs) -> String {
|
||||
if app_dbs.config.smtp.is_empty() {
|
||||
String::from("Error: no mail server")
|
||||
} else if app_dbs.config.mail_from.is_empty() {
|
||||
String::from("Error: no mail from address")
|
||||
} else {
|
||||
String::from("")
|
||||
}
|
||||
}
|
||||
#[allow(dead_code)]
|
||||
pub async fn send_message(&self, subject: &str, body: &str, app_dbs: &AppDBs) -> std::io::Result<()> {
|
||||
match Message::builder()
|
||||
.from(self.from.to_owned())
|
||||
.reply_to(self.reply_to.to_owned())
|
||||
.to(self.to.to_owned())
|
||||
.subject(subject)
|
||||
.header(ContentType::TEXT_PLAIN)
|
||||
.body(body.to_owned())
|
||||
{
|
||||
Ok(message) => self.mail_message(message, app_dbs).await,
|
||||
Err(e) =>
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::NotFound,
|
||||
format!("ERROR: Invalid mail: {}",e)
|
||||
))
|
||||
}
|
||||
}
|
||||
pub async fn send_html_message(&self, subject: &str, body: &str, html_body: &str, app_dbs: &AppDBs) -> std::io::Result<()> {
|
||||
match Message::builder()
|
||||
.from(self.from.to_owned())
|
||||
.reply_to(self.reply_to.to_owned())
|
||||
.to(self.to.to_owned())
|
||||
.subject(subject)
|
||||
.multipart(
|
||||
MultiPart::alternative() // This is composed of two parts.
|
||||
.singlepart(
|
||||
SinglePart::builder()
|
||||
.header(ContentType::TEXT_PLAIN)
|
||||
.body(body.to_owned()),
|
||||
)
|
||||
.singlepart(
|
||||
SinglePart::builder()
|
||||
.header(ContentType::TEXT_HTML)
|
||||
.body(html_body.to_owned()),
|
||||
),
|
||||
)
|
||||
{
|
||||
Ok(message) => self.mail_message(message, app_dbs).await,
|
||||
Err(e) =>
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::NotFound,
|
||||
format!("ERROR: Invalid mail: {}",e)
|
||||
))
|
||||
}
|
||||
}
|
||||
pub async fn mail_message(&self, message: Message, app_dbs: &AppDBs) -> std::io::Result<()> {
|
||||
let mail_cred = crate::defs::MailMessage::get_credentials(&app_dbs.config.smtp_auth, &app_dbs.config.paseto);
|
||||
if ! mail_cred.contains("|") {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::NotFound,
|
||||
format!("ERROR: Invalid mail credentials")
|
||||
));
|
||||
}
|
||||
let auth_data: Vec<String> = mail_cred.split("|").map(|s| s.to_string()).collect();
|
||||
if auth_data.len() < 2 {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::NotFound,
|
||||
format!("ERROR: Invalid mail credentials")
|
||||
));
|
||||
}
|
||||
let creds = Credentials::new(auth_data[0].to_owned(), auth_data[1].to_owned());
|
||||
// Open a remote connection to gmail
|
||||
let mailer: AsyncSmtpTransport<Tokio1Executor> =
|
||||
AsyncSmtpTransport::<Tokio1Executor>::relay(&app_dbs.config.smtp)
|
||||
.unwrap()
|
||||
.credentials(creds)
|
||||
.build();
|
||||
|
||||
// Send the email
|
||||
match mailer.send(message).await {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => Err(std::io::Error::new(
|
||||
std::io::ErrorKind::NotConnected,
|
||||
format!("ERROR: Could not send email: {e:?}")
|
||||
))
|
||||
}
|
||||
}
|
||||
pub fn get_credentials(token: &str, paseto_config: &ConfigPaSeToken) -> String {
|
||||
match paseto_config.pasetoken() {
|
||||
Ok(paseto) => {
|
||||
match paseto.trusted(token, false) {
|
||||
Ok(trusted_token) => {
|
||||
if let Some(claims) = trusted_token.payload_claims() {
|
||||
claims.get_claim("smtp_auth").unwrap_or(&json!("")).to_string().replace("\"","")
|
||||
} else {
|
||||
String::from("")
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
println!("Token not trusted: {}",e);
|
||||
String::from("")
|
||||
},
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
println!("Error collecting notify data: {}",e);
|
||||
String::from("")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
294
src/defs/req_handler.rs
Normal file
294
src/defs/req_handler.rs
Normal file
@ -0,0 +1,294 @@
|
||||
use axum::http::{header::{HeaderValue},uri::Uri};
|
||||
use html_minifier::HTMLMinifier;
|
||||
use std::{
|
||||
error::Error,
|
||||
collections::HashMap,
|
||||
str,
|
||||
io::{ErrorKind, Result},
|
||||
};
|
||||
use tera::Context;
|
||||
use totp_rs::{Algorithm, TOTP, Secret};
|
||||
use rand::Rng;
|
||||
|
||||
use crate::{
|
||||
defs::{
|
||||
AppDBs,
|
||||
ReqHeaderMap,
|
||||
Local,
|
||||
TraceData,
|
||||
TraceContent,
|
||||
TraceLogContent,
|
||||
AuthState,
|
||||
Random,
|
||||
TOKEN_AUTH_VALUE,
|
||||
TOKEN_KEY_VALUE,
|
||||
},
|
||||
users::{
|
||||
UserRole,
|
||||
User,
|
||||
},
|
||||
};
|
||||
|
||||
use super::{CLAIM_UID, CLAIM_AUTH, CLAIM_APP_KEY};
|
||||
|
||||
pub struct ReqHandler<'a> {
|
||||
pub req_header: ReqHeaderMap,
|
||||
pub app_dbs: &'a AppDBs,
|
||||
pub lang: Local,
|
||||
pub uri: &'a Uri,
|
||||
pub auth_state: AuthState,
|
||||
pub random: &'a Random,
|
||||
pub req_name: String,
|
||||
pub context: Context,
|
||||
}
|
||||
|
||||
impl<'a> ReqHandler<'a> {
|
||||
pub fn new(req_header: ReqHeaderMap, app_dbs: &'a AppDBs, uri: &'a Uri, auth_state: &'a AuthState, random: &'a Random, req_name: &str) -> Self {
|
||||
let curr_lang = req_header.lang(&app_dbs.config);
|
||||
let lang = Local::get_lang(&app_dbs.config.locales,&curr_lang, &app_dbs.config.dflt_lang);
|
||||
let mut context = app_dbs.context.to_owned();
|
||||
context.insert("site_name",&app_dbs.config.name);
|
||||
context.insert("site_org", &app_dbs.config.org);
|
||||
context.insert("lang", &lang.itms);
|
||||
context.insert("req_name", &req_name);
|
||||
context.insert("main_url", &format!("{}://{}",&app_dbs.config.protocol, &app_dbs.config.hostport));
|
||||
context.insert("html_url", &app_dbs.config.html_url);
|
||||
context.insert("assets_url", &app_dbs.config.assets_url);
|
||||
context.insert("req_path", uri.path());
|
||||
context.insert("css_link", &app_dbs.config.ui.css_link);
|
||||
context.insert("js_link", &app_dbs.config.ui.js_link);
|
||||
context.insert("other_css_link", &app_dbs.config.ui.other_css_link);
|
||||
context.insert("other_js_link", &app_dbs.config.ui.other_js_link);
|
||||
context.insert("main_js_link", &app_dbs.config.ui.main_js_link);
|
||||
context.insert("utils_js_link", &app_dbs.config.ui.utils_js_link);
|
||||
context.insert("web_menu_items", &app_dbs.config.ui.web_menu_items);
|
||||
context.insert("usr_roles", &auth_state.user_roles());
|
||||
let user_items = User::hash_items(&auth_state.user_items());
|
||||
context.insert("usr_items", &user_items);
|
||||
context.insert("usr_name", &auth_state.user_name());
|
||||
if auth_state.is_admin() {
|
||||
context.insert("isadmin", "TRUE");
|
||||
}
|
||||
context.insert("usr_email", &auth_state.user_email());
|
||||
context.insert("session_expire", &app_dbs.config.session_expire);
|
||||
context.insert("update_session_item", &format!("{}://{}/update_item",
|
||||
&app_dbs.config.protocol, &app_dbs.config.hostport));
|
||||
let user_id = auth_state.id();
|
||||
let sid = if user_id.is_empty() || user_id == "No ID" {
|
||||
String::from("")
|
||||
} else {
|
||||
user_id.replace("-", "").to_owned()
|
||||
};
|
||||
context.insert("session_id",&sid);
|
||||
context.insert("signup_mode",&app_dbs.config.signup_mode);
|
||||
ReqHandler {
|
||||
req_header,
|
||||
app_dbs,
|
||||
lang,
|
||||
uri,
|
||||
auth_state: auth_state.to_owned(),
|
||||
random,
|
||||
req_name: req_name.to_owned(),
|
||||
context,
|
||||
}
|
||||
}
|
||||
pub fn prepare_response(&mut self) {
|
||||
// self.req_header.header.contains_key(axum::http::header::CONTENT_TYPE)
|
||||
if self.req_header.is_browser() || self.req_header.is_wget() {
|
||||
self.req_header.header.append(axum::http::header::CONTENT_TYPE,
|
||||
HeaderValue::try_from("text/html; charset=utf-8")
|
||||
.expect("URI isn't a valid header value")
|
||||
);
|
||||
}
|
||||
}
|
||||
pub fn render_template(&mut self,template_file: &str, dflt_content: &str) -> String {
|
||||
self.context.insert("page", &template_file.replace(".j2","").replace("html/", "").as_str());
|
||||
if template_file.contains("md") || template_file.contains("sample") || template_file.contains("code") {
|
||||
self.context.insert("with_code", "true");
|
||||
}
|
||||
match self.app_dbs.tera.render(&template_file, &self.context) {
|
||||
Ok(s) => {
|
||||
if self.req_header.is_browser() {
|
||||
let mut html_minifier = HTMLMinifier::new();
|
||||
match html_minifier.digest(s.as_str()) {
|
||||
Ok(_) => {
|
||||
let Ok(result) = str::from_utf8(html_minifier.get_html()) else { return s };
|
||||
// let result = str::from_utf8(html_minifier.get_html()).unwrap_or(s);
|
||||
result.to_owned()
|
||||
},
|
||||
Err(e) => {
|
||||
println!("Error minifier: {}",e);
|
||||
s
|
||||
},
|
||||
}
|
||||
} else {
|
||||
s
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
println!("Error: {}", &e);
|
||||
let mut cause = e.source();
|
||||
while let Some(e) = cause {
|
||||
println!("Reason: {}", e);
|
||||
cause = e.source();
|
||||
}
|
||||
dflt_content.to_owned()
|
||||
}
|
||||
}
|
||||
}
|
||||
#[allow(dead_code)]
|
||||
pub fn bad_request(&mut self,msg: &str) -> String {
|
||||
if let Some(tpl) = self.app_dbs.config.tpls.get("notfound") {
|
||||
self.render_template(tpl, msg)
|
||||
} else {
|
||||
msg.to_owned()
|
||||
}
|
||||
}
|
||||
#[allow(dead_code)]
|
||||
pub fn auth_role(&self) -> UserRole {
|
||||
#[cfg(feature = "authstore")]
|
||||
if let Some(authz) = self.app_dbs.auth_store.authz.get(&self.notify_data.auth) {
|
||||
println!("aut: {}, name: {}, role: {}",&self.notify_data.auth, &authz.name,&authz.role);
|
||||
authz.role.to_owned()
|
||||
} else {
|
||||
UserRole::Anonymous
|
||||
}
|
||||
#[cfg(feature = "casbin")]
|
||||
UserRole::Anonymous
|
||||
}
|
||||
#[allow(dead_code)]
|
||||
pub fn get_lang(&self, key: &str) -> Local {
|
||||
Local::get_lang(&self.app_dbs.config.locales, key, &self.app_dbs.config.dflt_lang)
|
||||
}
|
||||
pub fn new_token(&self) -> String {
|
||||
if self.app_dbs.config.use_token {
|
||||
let data_claim = HashMap::from([
|
||||
(String::from(CLAIM_UID), self.auth_state.user_id()),
|
||||
(String::from(CLAIM_AUTH), TOKEN_AUTH_VALUE.to_owned()),
|
||||
(String::from(CLAIM_APP_KEY), TOKEN_KEY_VALUE.to_owned()),
|
||||
]);
|
||||
let expire = false;
|
||||
self.app_dbs.config.paseto.generate_token("", &data_claim, expire).unwrap_or_else(|e|{
|
||||
eprintln!("Error generating token: {}", e);
|
||||
String::from("")
|
||||
})
|
||||
} else {
|
||||
let mut u128_pool = [0u8; 16];
|
||||
match self.random.lock() {
|
||||
Ok(mut r) => r.fill(&mut u128_pool),
|
||||
Err(e) => println!("Error random: {}",e),
|
||||
}
|
||||
u128::from_le_bytes(u128_pool).to_string()
|
||||
}
|
||||
}
|
||||
pub fn otp_make(&self, code: &str, defs: &str) -> Result<TOTP> {
|
||||
let secret = match Secret::Encoded(code.to_owned()).to_bytes() {
|
||||
Ok(scrt) => scrt,
|
||||
Err(e) =>
|
||||
return Err(std::io::Error::new(
|
||||
ErrorKind::NotFound,
|
||||
format!("ERROR: Invalid Secret {}",&e)
|
||||
))
|
||||
};
|
||||
let (digits, str_algorithm) = if defs.is_empty() {
|
||||
( self.app_dbs.config.totp_digits,
|
||||
format!("{}",self.app_dbs.config.totp_algorithm)
|
||||
)
|
||||
} else {
|
||||
let arr_defs = defs.split(",").collect::<Vec<&str>>();
|
||||
if arr_defs.len() > 1 {
|
||||
( arr_defs[0].parse().unwrap_or(self.app_dbs.config.totp_digits),
|
||||
arr_defs[1].to_owned()
|
||||
)
|
||||
} else {
|
||||
( self.app_dbs.config.totp_digits,
|
||||
format!("{}",self.app_dbs.config.totp_algorithm)
|
||||
)
|
||||
}
|
||||
};
|
||||
let algorithm = match str_algorithm.to_uppercase().as_str() {
|
||||
"SHA1" => Algorithm::SHA1,
|
||||
"SHA256" => Algorithm::SHA256,
|
||||
"SHA512" => Algorithm::SHA512,
|
||||
_ => Algorithm::SHA1,
|
||||
};
|
||||
let totp = match TOTP::new(
|
||||
algorithm,
|
||||
digits,
|
||||
1,
|
||||
30,
|
||||
secret,
|
||||
Some(format!("{}",self.app_dbs.config.name)),
|
||||
format!("{}",self.app_dbs.config.org)
|
||||
) {
|
||||
Ok(totp) => totp,
|
||||
Err(e) =>
|
||||
return Err(std::io::Error::new(
|
||||
ErrorKind::NotFound,
|
||||
format!("ERROR: Invalid Topt {}",&e)
|
||||
))
|
||||
};
|
||||
Ok(totp)
|
||||
}
|
||||
pub fn otp_generate(&self) -> Result<TOTP> {
|
||||
let mut rng = rand::thread_rng();
|
||||
let data_byte: [u8; 21] = rng.gen();
|
||||
let base32_string = base32::encode(base32::Alphabet::RFC4648 { padding: false }, &data_byte);
|
||||
match self.otp_make(&base32_string, "") {
|
||||
Ok(totp) => Ok(totp),
|
||||
Err(e) =>
|
||||
Err(std::io::Error::new(
|
||||
ErrorKind::NotFound,
|
||||
format!("ERROR: Invalid TOTP {}",&e)
|
||||
))
|
||||
}
|
||||
}
|
||||
pub fn otp_check(&self, code: &str, token: &str, defs: &str) -> Result<bool> {
|
||||
let totp = match self.otp_make(code, defs) {
|
||||
Ok(totp) => totp,
|
||||
Err(e) =>
|
||||
return Err(std::io::Error::new(
|
||||
ErrorKind::NotFound,
|
||||
format!("ERROR: Invalid TOTP {}",&e)
|
||||
))
|
||||
};
|
||||
match totp.check_current(&token) {
|
||||
Ok(result) => Ok(result),
|
||||
Err(e) =>
|
||||
return Err(std::io::Error::new(
|
||||
ErrorKind::NotFound,
|
||||
format!("ERROR: check {}",&e)
|
||||
))
|
||||
}
|
||||
}
|
||||
pub fn trace_req(&self, info: String) -> Result<()> {
|
||||
let timestamp = chrono::Utc::now().timestamp().to_string();
|
||||
let user_id = self.auth_state.user_id();
|
||||
let trace_content = TraceContent{
|
||||
when: timestamp.to_owned(),
|
||||
sid: self.auth_state.id(),
|
||||
origin: self.uri.path().to_owned(),
|
||||
trigger: String::from("req_handler"),
|
||||
id: user_id.to_owned(),
|
||||
info: info.to_owned(),
|
||||
context: self.req_name.to_owned(),
|
||||
role: self.auth_state.user_roles(),
|
||||
req: self.req_header.req_info(),
|
||||
};
|
||||
let trace_data = TraceData{
|
||||
user_id,
|
||||
timestamp,
|
||||
contents: vec![trace_content]
|
||||
};
|
||||
trace_data.save(&self.app_dbs.config, false)
|
||||
}
|
||||
pub fn logs(&self, userid: &str, human: bool, reverse: bool) -> Vec<TraceLogContent> {
|
||||
let timestamp = chrono::Utc::now().timestamp().to_string();
|
||||
let trace_data = TraceData{
|
||||
user_id: format!("{}",userid),
|
||||
timestamp,
|
||||
contents: Vec::new()
|
||||
};
|
||||
trace_data.load(&self.app_dbs.config, human, reverse)
|
||||
}
|
||||
}
|
192
src/defs/req_headermap.rs
Normal file
192
src/defs/req_headermap.rs
Normal file
@ -0,0 +1,192 @@
|
||||
|
||||
use std::{
|
||||
net::IpAddr,
|
||||
collections::HashMap,
|
||||
};
|
||||
use axum::http::{header::FORWARDED, HeaderMap};
|
||||
use forwarded_header_value::{ForwardedHeaderValue, Identifier};
|
||||
|
||||
use crate::defs::{
|
||||
Config,
|
||||
AppConnectInfo,
|
||||
AUTHORIZATION,
|
||||
REFERER,
|
||||
BEARER,
|
||||
USER_AGENT,
|
||||
ACCEPT_LANGUAGE,
|
||||
X_FORWARDED_FOR,
|
||||
X_REAL_IP,
|
||||
X_INTERNAL,
|
||||
};
|
||||
|
||||
|
||||
pub struct ReqHeaderMap {
|
||||
pub header: HeaderMap,
|
||||
pub req_path: Vec<String>,
|
||||
pub app_connect_info: AppConnectInfo,
|
||||
}
|
||||
|
||||
impl ReqHeaderMap {
|
||||
pub fn new(header: HeaderMap, request_path: &str, app_connect_info: &AppConnectInfo) -> Self {
|
||||
let req_path =
|
||||
Self::req_end_path(request_path)
|
||||
.split(",").map(|s| s.to_string())
|
||||
.collect();
|
||||
ReqHeaderMap {
|
||||
header,
|
||||
req_path,
|
||||
app_connect_info: app_connect_info.to_owned(),
|
||||
}
|
||||
}
|
||||
pub fn req_end_path(req_path: &str) -> String {
|
||||
let arr_req_path: Vec<String> = req_path.split("/").map(|s| s.to_string()).collect();
|
||||
format!("{}",arr_req_path[arr_req_path.len()-1])
|
||||
}
|
||||
|
||||
pub fn auth(&self) -> String {
|
||||
if let Some(auth) = self.header.get(AUTHORIZATION) {
|
||||
format!("{}",auth.to_str().unwrap_or("").replace(&format!("{} ", BEARER),""))
|
||||
} else {
|
||||
String::from("")
|
||||
}
|
||||
}
|
||||
pub fn referer(&self) -> String {
|
||||
if let Some(referer) = self.header.get(REFERER) {
|
||||
format!("{}",referer.to_str().unwrap_or(""))
|
||||
} else {
|
||||
String::from("")
|
||||
}
|
||||
}
|
||||
pub fn internal(&self) -> String {
|
||||
if let Some(internal) = self.header.get(X_INTERNAL) {
|
||||
format!("{}",internal.to_str().unwrap_or(""))
|
||||
} else {
|
||||
String::from("")
|
||||
}
|
||||
}
|
||||
pub fn is_browser(&self) -> bool {
|
||||
if let Some(user_agent) = self.header.get(USER_AGENT) {
|
||||
let agent = user_agent.to_str().unwrap_or("");
|
||||
if agent.contains("Mozilla") {
|
||||
true
|
||||
} else if agent.contains("WebKit") {
|
||||
true
|
||||
} else if agent.contains("Chrome") {
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
#[allow(dead_code)]
|
||||
pub fn is_curl(&self) -> bool {
|
||||
if let Some(user_agent) = self.header.get(USER_AGENT) {
|
||||
let agent = user_agent.to_str().unwrap_or("");
|
||||
if agent.contains("curl") {
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
pub fn is_wget(&self) -> bool {
|
||||
if let Some(user_agent) = self.header.get(USER_AGENT) {
|
||||
let agent = user_agent.to_str().unwrap_or("").to_lowercase();
|
||||
if agent.contains("wget") {
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
#[allow(dead_code)]
|
||||
pub fn response_user_agent_html(&self) -> bool {
|
||||
if let Some(user_agent) = self.header.get(USER_AGENT) {
|
||||
let agent = user_agent.to_str().unwrap_or("").to_lowercase();
|
||||
agent.contains("curl") || agent.contains("wget")
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
pub fn agent(&self) -> String {
|
||||
if let Some(user_agent) = self.header.get(USER_AGENT) {
|
||||
user_agent.to_str().unwrap_or("").to_owned()
|
||||
} else {
|
||||
String::from("")
|
||||
}
|
||||
}
|
||||
pub fn lang(&self, config: &Config) -> String {
|
||||
if let Some(langs) = self.header.get(ACCEPT_LANGUAGE) {
|
||||
let langs_data = langs.to_str().unwrap_or("");
|
||||
if langs_data.is_empty() {
|
||||
format!("{}",config.dflt_lang)
|
||||
} else {
|
||||
let arr_langs: Vec<String> = langs_data.split(",").map(|s| s.to_string()).collect();
|
||||
let arr_lang: Vec<String> = arr_langs[0].split("-").map(|s| s.to_string()).collect();
|
||||
format!("{}",arr_lang[0])
|
||||
}
|
||||
} else {
|
||||
format!("{}",config.dflt_lang)
|
||||
}
|
||||
}
|
||||
/// Tries to parse the `x-real-ip` header
|
||||
fn maybe_x_forwarded_for(&self) -> Option<IpAddr> {
|
||||
self.header
|
||||
.get(X_FORWARDED_FOR)
|
||||
.and_then(|hv| hv.to_str().ok())
|
||||
.and_then(|s| s.split(',').find_map(|s| s.trim().parse::<IpAddr>().ok()))
|
||||
}
|
||||
|
||||
/// Tries to parse the `x-real-ip` header
|
||||
fn maybe_x_real_ip(&self) -> Option<IpAddr> {
|
||||
self.header
|
||||
.get(X_REAL_IP)
|
||||
.and_then(|hv| hv.to_str().ok())
|
||||
.and_then(|s| s.parse::<IpAddr>().ok())
|
||||
}
|
||||
|
||||
/// Tries to parse `forwarded` headers
|
||||
fn maybe_forwarded(&self) -> Option<IpAddr> {
|
||||
self.header
|
||||
.get_all(FORWARDED).iter().find_map(|hv| {
|
||||
hv.to_str()
|
||||
.ok()
|
||||
.and_then(|s| ForwardedHeaderValue::from_forwarded(s).ok())
|
||||
.and_then(|f| {
|
||||
f.iter()
|
||||
.filter_map(|fs| fs.forwarded_for.as_ref())
|
||||
.find_map(|ff| match ff {
|
||||
Identifier::SocketAddr(a) => Some(a.ip()),
|
||||
Identifier::IpAddr(ip) => Some(*ip),
|
||||
_ => None,
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
pub fn ip(&self) -> String {
|
||||
if let Some(ip) = self.maybe_x_real_ip()
|
||||
.or_else(||self.maybe_x_forwarded_for())
|
||||
.or_else(|| self.maybe_forwarded())
|
||||
.or_else(|| None )
|
||||
{
|
||||
format!("{}",ip)
|
||||
} else {
|
||||
format!("{}", self.app_connect_info.remote_addr)
|
||||
}
|
||||
}
|
||||
pub fn req_info(&self) -> HashMap<String,String> {
|
||||
let mut req_data: HashMap<String,String> = HashMap::new();
|
||||
req_data.insert(String::from("agent"), self.agent());
|
||||
req_data.insert(String::from("ip"), self.ip());
|
||||
req_data.insert(String::from("auth"), self.auth());
|
||||
req_data.insert(String::from("ref"), self.referer());
|
||||
req_data.insert(String::from("int"), self.internal());
|
||||
req_data
|
||||
}
|
||||
}
|
35
src/defs/req_settings.rs
Normal file
35
src/defs/req_settings.rs
Normal file
@ -0,0 +1,35 @@
|
||||
use serde::{Serialize, Deserialize, Deserializer};
|
||||
use std::collections::HashMap;
|
||||
|
||||
fn default_config_resource() -> String {
|
||||
String::from("")
|
||||
}
|
||||
fn default_sitewith() -> Vec<String> {
|
||||
Vec::new()
|
||||
}
|
||||
fn default_config_tpls() -> HashMap<String,String> {
|
||||
HashMap::new()
|
||||
}
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct ReqSettings {
|
||||
pub name: String,
|
||||
pub author: String,
|
||||
pub fullname: String,
|
||||
pub desc: String,
|
||||
#[serde(default = "default_config_resource")]
|
||||
pub url: String,
|
||||
#[serde(default = "default_config_resource")]
|
||||
pub trace_url: String,
|
||||
#[serde(default = "default_config_resource")]
|
||||
pub title: String,
|
||||
#[serde(default = "default_config_resource")]
|
||||
pub subtitle: String,
|
||||
#[serde(default = "default_config_resource")]
|
||||
pub pagetitle: String,
|
||||
#[serde(default = "default_config_tpls")]
|
||||
pub tpls: HashMap<String,String>,
|
||||
#[serde(default = "default_config_resource")]
|
||||
pub sid: String,
|
||||
#[serde(default = "default_sitewith")]
|
||||
pub sitewith: Vec<String>,
|
||||
}
|
403
src/defs/session.rs
Normal file
403
src/defs/session.rs
Normal file
@ -0,0 +1,403 @@
|
||||
//use async_trait::async_trait;
|
||||
use async_session::{Session, SessionStore,MemoryStore};
|
||||
use async_sqlx_session::SqliteSessionStore;
|
||||
|
||||
// use axum::{
|
||||
// http::{
|
||||
// StatusCode,
|
||||
// request::Parts,
|
||||
// },
|
||||
// extract::FromRequestParts,
|
||||
// };
|
||||
// use tower_cookies::Cookies;
|
||||
use std::sync::{Arc,Mutex};
|
||||
use rand_chacha::ChaCha8Rng;
|
||||
pub type Random = Arc<Mutex<ChaCha8Rng>>;
|
||||
|
||||
use crate::{
|
||||
// SESSION_COOKIE_NAME,
|
||||
defs::{
|
||||
FileStore,
|
||||
AppDBs,
|
||||
SESSION_ID,
|
||||
USER_DATA,
|
||||
},
|
||||
users::{
|
||||
User,
|
||||
UserStatus,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum SessionStoreDB {
|
||||
Files(FileStore),
|
||||
SqlLite(SqliteSessionStore),
|
||||
Memory(MemoryStore),
|
||||
None,
|
||||
}
|
||||
/// In `SessionStoreDB` creation match the corresponding store to return it as item argument
|
||||
impl SessionStoreDB {
|
||||
pub fn connect_file_store(store: FileStore) -> Self{
|
||||
SessionStoreDB::Files(store)
|
||||
}
|
||||
pub fn connect_memory_store() -> Self{
|
||||
SessionStoreDB::Memory(MemoryStore::new())
|
||||
}
|
||||
pub fn connect_sqlite_store(store: SqliteSessionStore) -> Self{
|
||||
SessionStoreDB::SqlLite(store)
|
||||
}
|
||||
pub async fn store_session_data(id: &str, user_data: &str, expire: u64, app_dbs: &AppDBs) -> String {
|
||||
// dbg!("user_data: {}",&user_data);
|
||||
let mut session = Session::new();
|
||||
if ! id.is_empty() {
|
||||
session.insert(SESSION_ID, id).unwrap_or_else(|e|{
|
||||
println!("Error insert session {}",e);
|
||||
});
|
||||
}
|
||||
if ! user_data.is_empty() {
|
||||
session.insert(USER_DATA, user_data).unwrap_or_else(|e|{
|
||||
println!("Error insert session {}",e);
|
||||
});
|
||||
}
|
||||
if expire > 0 {
|
||||
session.expire_in(std::time::Duration::from_secs(expire));
|
||||
} else if app_dbs.config.session_expire > 0 {
|
||||
session.expire_in(std::time::Duration::from_secs(app_dbs.config.session_expire));
|
||||
}
|
||||
//session.into_cookie_value().unwrap_or_default()
|
||||
match app_dbs.sessions_store_db.to_owned() {
|
||||
SessionStoreDB::Files(store) => {
|
||||
store.store_session(session).await.unwrap_or_else(|e|{
|
||||
println!("Error store session {}",e);
|
||||
None
|
||||
}).unwrap_or_default()
|
||||
},
|
||||
SessionStoreDB::SqlLite(store) => {
|
||||
store.store_session(session).await.unwrap_or_else(|e|{
|
||||
println!("Error store session {}",e);
|
||||
None
|
||||
}).unwrap_or_default()
|
||||
},
|
||||
SessionStoreDB::Memory(store) => {
|
||||
store.store_session(session).await.unwrap_or_else(|e|{
|
||||
println!("Error store session {}",e);
|
||||
None
|
||||
}).unwrap_or_default()
|
||||
},
|
||||
SessionStoreDB::None => {
|
||||
String::from("")
|
||||
},
|
||||
}
|
||||
}
|
||||
pub async fn update_session_data(session: Session, app_dbs: &AppDBs) -> String {
|
||||
//session.into_cookie_value().unwrap_or_default()
|
||||
let cur_session = session.clone();
|
||||
match app_dbs.sessions_store_db.to_owned() {
|
||||
SessionStoreDB::Files(store) => {
|
||||
store.store_session(session).await.unwrap_or_else(|e|{
|
||||
println!("Error store session {}",e);
|
||||
None
|
||||
}).unwrap_or_default()
|
||||
},
|
||||
SessionStoreDB::SqlLite(store) => {
|
||||
let _ = store.destroy_session(session).await;
|
||||
store.store_session(cur_session).await.unwrap_or_else(|e|{
|
||||
println!("Error store session {}",e);
|
||||
None
|
||||
}).unwrap_or_default()
|
||||
},
|
||||
SessionStoreDB::Memory(store) => {
|
||||
store.store_session(session).await.unwrap_or_else(|e|{
|
||||
println!("Error store session {}",e);
|
||||
None
|
||||
}).unwrap_or_default()
|
||||
},
|
||||
SessionStoreDB::None => {
|
||||
String::from("")
|
||||
},
|
||||
}
|
||||
}
|
||||
pub async fn cleanup_data(app_dbs: &AppDBs) {
|
||||
//session.into_cookie_value().unwrap_or_default()
|
||||
match app_dbs.sessions_store_db.to_owned() {
|
||||
SessionStoreDB::Files(store) => {
|
||||
let _ = store.cleanup().await;
|
||||
},
|
||||
SessionStoreDB::SqlLite(store) => {
|
||||
let _ = store.cleanup().await;
|
||||
},
|
||||
SessionStoreDB::Memory(store) => {
|
||||
let _ = store.cleanup().await;
|
||||
},
|
||||
SessionStoreDB::None => {
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Debug)]
|
||||
pub struct AuthState{
|
||||
pub session: Option<Session>,
|
||||
pub user: Option<User>,
|
||||
}
|
||||
impl AuthState {
|
||||
// pub fn new(session: Session, user: User) -> Self {
|
||||
// let sid = session.get("sid").unwrap_or_default();
|
||||
// let user_store = &app_dbs.user_store;
|
||||
// let user = User::select("id", sid, user_store).await.unwrap_or_default();
|
||||
// //let user = User::from_id(sid, app_dbs);
|
||||
// Self {
|
||||
// session,
|
||||
// user
|
||||
// }
|
||||
// }
|
||||
// pub fn sid(&self) -> String {
|
||||
// if let Some(session) = &self.session {
|
||||
// session.get("sid").unwrap_or_default()
|
||||
// } else {
|
||||
// String::from("")
|
||||
// }
|
||||
// }
|
||||
pub fn id(&self) -> String {
|
||||
if let Some(session) = &self.session {
|
||||
session.id().to_owned()
|
||||
} else {
|
||||
String::from("")
|
||||
}
|
||||
}
|
||||
#[allow(dead_code)]
|
||||
pub fn ses_expired(&self) -> bool {
|
||||
if let Some(session) = &self.session {
|
||||
session.is_expired()
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
pub fn ses_destroyed(&self) -> bool {
|
||||
if let Some(session) = &self.session {
|
||||
session.is_destroyed()
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
pub fn destroy(&self) -> bool {
|
||||
if let Some(mut session) = self.session.to_owned() {
|
||||
session.destroy();
|
||||
self.ses_destroyed()
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
#[allow(dead_code)]
|
||||
pub fn ses_validate(&self) -> bool {
|
||||
if let Some(session) = &self.session {
|
||||
session.clone().validate().is_some()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
pub fn user_data(&self) -> Vec<String> {
|
||||
if let Some(session) = &self.session {
|
||||
let user_data: String = session.get(USER_DATA).unwrap_or_default();
|
||||
user_data.split("|").map(|s| s.to_string()).collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
pub fn user_id(&self) -> String {
|
||||
let user_data = self.user_data();
|
||||
if user_data.len() > 0 && user_data[0] != "0" {
|
||||
user_data[0].to_owned()
|
||||
} else {
|
||||
String::from("")
|
||||
}
|
||||
}
|
||||
pub fn user_name(&self) -> String {
|
||||
let user_data = self.user_data();
|
||||
if user_data.len() > 1 && user_data[1] != "" {
|
||||
user_data[1].to_owned()
|
||||
} else {
|
||||
String::from("")
|
||||
}
|
||||
}
|
||||
pub fn user_email(&self) -> String {
|
||||
let user_data = self.user_data();
|
||||
if user_data.len() > 2 && user_data[2] != "" {
|
||||
user_data[2].to_owned()
|
||||
} else {
|
||||
String::from("")
|
||||
}
|
||||
}
|
||||
pub fn user_roles(&self) -> String {
|
||||
let user_data = self.user_data();
|
||||
if user_data.len() > 3 && user_data[3] != "" {
|
||||
user_data[3].to_owned()
|
||||
} else {
|
||||
String::from("")
|
||||
}
|
||||
}
|
||||
pub fn user_items(&self) -> String {
|
||||
let user_data = self.user_data();
|
||||
if user_data.len() > 4 && user_data[4] != "" {
|
||||
user_data[4].to_owned()
|
||||
} else {
|
||||
String::from("")
|
||||
}
|
||||
}
|
||||
pub fn is_admin(&self) -> bool {
|
||||
let user_data = self.user_data();
|
||||
if user_data.len() > 5 && user_data[5] != "" {
|
||||
user_data[5] == "true" || user_data[5] == "1"
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
#[allow(dead_code)]
|
||||
pub fn user_status(&self) -> UserStatus {
|
||||
let user_data = self.user_data();
|
||||
if user_data.len() > 6 && user_data[6] != "" {
|
||||
UserStatus::from_str(&user_data[6])
|
||||
} else {
|
||||
UserStatus::Unknown
|
||||
}
|
||||
}
|
||||
pub fn has_auth_role(&self, auth_roles: &Vec<String>) -> bool {
|
||||
let roles = self.user_roles();
|
||||
for role in auth_roles.to_owned() {
|
||||
if roles.contains(&role) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
pub async fn expire_in(&mut self, expire_secs: u64, app_dbs: &AppDBs ) -> Self {
|
||||
if let Some(mut session) = self.session.to_owned() {
|
||||
session.expire_in(std::time::Duration::from_secs(expire_secs));
|
||||
let _ = SessionStoreDB::update_session_data(session.to_owned(),&app_dbs).await;
|
||||
Self {
|
||||
session: Some(session),
|
||||
user: self.user.to_owned(),
|
||||
}
|
||||
} else {
|
||||
self.to_owned()
|
||||
}
|
||||
}
|
||||
#[allow(dead_code)]
|
||||
pub async fn from_data(&self, app_dbs: &AppDBs) -> Self {
|
||||
let user_id = self.user_id();
|
||||
let user = if !user_id.is_empty() {
|
||||
// dbg!(&user_id);
|
||||
match User::select("id", &user_id, false, &app_dbs.user_store).await {
|
||||
Ok(user) => Some(user),
|
||||
Err(e) => {
|
||||
println!("Error user {}: {:#}",&user_id,e);
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
// dbg!(&user);
|
||||
Self {
|
||||
session: self.session.to_owned(),
|
||||
user,
|
||||
}
|
||||
}
|
||||
pub async fn from_cookie(session_cookie: String, app_dbs: &AppDBs) -> Self {
|
||||
let result_session = match app_dbs.sessions_store_db.to_owned() {
|
||||
SessionStoreDB::Files(store) =>
|
||||
store
|
||||
.load_session(session_cookie)
|
||||
.await
|
||||
,
|
||||
SessionStoreDB::SqlLite(store) =>
|
||||
store
|
||||
.load_session(session_cookie)
|
||||
.await
|
||||
,
|
||||
SessionStoreDB::Memory(store) =>
|
||||
store
|
||||
.load_session(session_cookie.to_string())
|
||||
.await
|
||||
,
|
||||
SessionStoreDB::None => {
|
||||
dbg!("No store");
|
||||
return AuthState::default();
|
||||
},
|
||||
};
|
||||
// dbg!(&result_session);
|
||||
match result_session {
|
||||
Ok(op_session) => if let Some(session) = op_session {
|
||||
if let Some(sid) = session.get::<String>("sid") {
|
||||
// dbg!(
|
||||
// "UserDataFromSession: session decoded success, user_data={}",
|
||||
// &session
|
||||
// );
|
||||
let user_store = &app_dbs.user_store;
|
||||
let user = User::select("id", &sid, false, user_store).await.unwrap_or_default();
|
||||
return AuthState {
|
||||
session: Some(session),
|
||||
user: Some(user)
|
||||
};
|
||||
} else {
|
||||
println!("No `sid` found in session");
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
println!("Error session {}", e);
|
||||
}
|
||||
}
|
||||
AuthState::default()
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
#[async_trait]
|
||||
impl<S> FromRequestParts<S> for AuthState
|
||||
where
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = (StatusCode, &'static str);
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
|
||||
// let h = parts.headers.to_owned();
|
||||
// dbg!(&h);
|
||||
/*
|
||||
if let Some(app_dbs) = parts.extensions.get::<Arc<AppDBs>>() {
|
||||
if let Some(cookies) = parts.extensions.get::<Cookies>() {
|
||||
if let Some(s_cookie) = cookies.get(SESSION_COOKIE_NAME) {
|
||||
let session_cookie = s_cookie.to_string().replace(&format!("{}=",SESSION_COOKIE_NAME),"");
|
||||
println!("From request parts cookie: {}",session_cookie);
|
||||
// continue to decode the session cookie
|
||||
// return Ok(
|
||||
// AuthState::from_cookie(session_cookie, app_dbs).await
|
||||
// );
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
Ok(AuthState::default())
|
||||
// Err((
|
||||
// StatusCode::INTERNAL_SERVER_ERROR,
|
||||
// "No `user_data` found in session",
|
||||
// ))
|
||||
// let cookie = Option::<Cookie>::g(parts, state);
|
||||
// let cookie = Cookies::get(parts, state);
|
||||
// if let Some(_user_agent) = parts.headers.get(USER_AGENT) {
|
||||
// Ok(AuthState(None))
|
||||
// } else {
|
||||
// Err((StatusCode::BAD_REQUEST, "`User-Agent` header is missing"))
|
||||
// }
|
||||
// let cookie = Option::<TypedHeader<Cookie>>::from_request_parts(req, state)
|
||||
//let cookies = Cookies::from(pa, state).await?;
|
||||
|
||||
// let info: String = cookies
|
||||
// .get(SESSION_COOKIE_NAME)
|
||||
// // .and_then(|c| c.value().parse().ok())
|
||||
// .unwrap_or_default()
|
||||
// ;
|
||||
// dbg!(&info);
|
||||
// //cookies.add(Cookie::new(COOKIE_NAME, visited.to_string()));
|
||||
}
|
||||
}
|
||||
*/
|
37
src/defs/totp_algorithm.rs
Normal file
37
src/defs/totp_algorithm.rs
Normal file
@ -0,0 +1,37 @@
|
||||
use serde::{Deserialize,Serialize,Deserializer};
|
||||
|
||||
#[derive(Eq, PartialEq, Clone, Serialize, Debug, Deserialize)]
|
||||
pub enum TotpAlgorithm {
|
||||
Sha1,
|
||||
Sha256,
|
||||
Sha512,
|
||||
}
|
||||
impl Default for TotpAlgorithm {
|
||||
fn default() -> Self {
|
||||
TotpAlgorithm::Sha1
|
||||
}
|
||||
}
|
||||
impl std::fmt::Display for TotpAlgorithm {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
TotpAlgorithm::Sha1 => write!(f,"sha1"),
|
||||
TotpAlgorithm::Sha256 => write!(f,"sha256"),
|
||||
TotpAlgorithm::Sha512 => write!(f,"sha512"),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl TotpAlgorithm {
|
||||
pub fn from_str(value: &str) -> TotpAlgorithm {
|
||||
match value {
|
||||
"sha1" | "SHA1" => TotpAlgorithm::Sha1,
|
||||
"sha512" | "SHA512" => TotpAlgorithm::Sha512,
|
||||
"sha256" | "SHA256" => TotpAlgorithm::Sha256,
|
||||
_ => TotpAlgorithm::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn deserialize_totp_algorithm<'de, D>(deserializer: D) -> Result<TotpAlgorithm, D::Error>
|
||||
where D: Deserializer<'de> {
|
||||
let buf = String::deserialize(deserializer)?;
|
||||
Ok(TotpAlgorithm::from_str(&buf))
|
||||
}
|
37
src/defs/totp_mode.rs
Normal file
37
src/defs/totp_mode.rs
Normal file
@ -0,0 +1,37 @@
|
||||
use serde::{Deserialize,Serialize,Deserializer};
|
||||
|
||||
#[derive(Eq, PartialEq, Clone, Serialize, Debug, Deserialize)]
|
||||
pub enum TotpMode {
|
||||
Mandatory,
|
||||
Optional,
|
||||
No,
|
||||
}
|
||||
impl Default for TotpMode {
|
||||
fn default() -> Self {
|
||||
TotpMode::No
|
||||
}
|
||||
}
|
||||
impl std::fmt::Display for TotpMode {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
TotpMode::Mandatory => write!(f,"mandatory"),
|
||||
TotpMode::Optional => write!(f,"optional"),
|
||||
TotpMode::No => write!(f,"no"),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl TotpMode {
|
||||
pub fn from_str(value: &str) -> TotpMode {
|
||||
match value {
|
||||
"mandatory" | "Mandaltory" | "required" | "Required" => TotpMode::Mandatory,
|
||||
"optional" | "Optional" => TotpMode::Optional,
|
||||
"no" | "No" => TotpMode::No,
|
||||
_ => TotpMode::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn deserialize_totp_mode<'de, D>(deserializer: D) -> Result<TotpMode, D::Error>
|
||||
where D: Deserializer<'de> {
|
||||
let buf = String::deserialize(deserializer)?;
|
||||
Ok(TotpMode::from_str(&buf))
|
||||
}
|
290
src/defs/tracedata.rs
Normal file
290
src/defs/tracedata.rs
Normal file
@ -0,0 +1,290 @@
|
||||
use serde::{Deserialize,Serialize};
|
||||
use std::{
|
||||
io::Write,
|
||||
// sync::Arc,
|
||||
fmt::Debug,
|
||||
fs::{self,File},
|
||||
path::{Path, PathBuf},
|
||||
io::{Error, ErrorKind, BufRead, BufReader},
|
||||
collections::HashMap,
|
||||
};
|
||||
use log::error;
|
||||
use crate::{
|
||||
defs::{
|
||||
FILE_SCHEME,
|
||||
SID_TRACE_FILE,
|
||||
Config,
|
||||
},
|
||||
tools::str_date_from_timestamp,
|
||||
};
|
||||
|
||||
fn default_empty() -> String {
|
||||
"/".to_string()
|
||||
}
|
||||
fn default_tracedata_items() -> Vec<TraceContent> {
|
||||
Vec::new()
|
||||
}
|
||||
fn default_tracecontent() -> TraceContent {
|
||||
TraceContent::default()
|
||||
}
|
||||
fn default_tracecontent_req() -> HashMap<String,String> {
|
||||
HashMap::new()
|
||||
}
|
||||
|
||||
#[derive(Default,Deserialize,Serialize,Debug,Clone)]
|
||||
pub struct RequestData {
|
||||
#[serde(default = "default_empty")]
|
||||
pub agent: String,
|
||||
}
|
||||
#[derive(Default,Deserialize,Serialize,Debug,Clone)]
|
||||
pub struct TraceContent {
|
||||
#[serde(default = "default_empty")]
|
||||
pub when: String,
|
||||
#[serde(default = "default_empty")]
|
||||
pub sid: String,
|
||||
#[serde(default = "default_empty")]
|
||||
pub origin: String,
|
||||
#[serde(default = "default_empty")]
|
||||
pub trigger: String,
|
||||
#[serde(default = "default_empty")]
|
||||
pub id: String,
|
||||
#[serde(default = "default_empty")]
|
||||
pub info: String,
|
||||
#[serde(default = "default_empty")]
|
||||
pub context: String,
|
||||
#[serde(default = "default_empty")]
|
||||
pub role: String,
|
||||
#[serde(default = "default_tracecontent_req")]
|
||||
pub req: HashMap<String,String>,
|
||||
}
|
||||
impl TraceContent {
|
||||
pub fn to_json(&self) -> String {
|
||||
serde_json::to_string(self).unwrap_or_else(|e|{
|
||||
println!("Error to convert TraceContent to json: {}",e);
|
||||
String::from("")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default,Deserialize,Serialize,Debug,Clone)]
|
||||
pub struct TraceLogContent {
|
||||
#[serde(default = "default_empty")]
|
||||
pub line_id: String,
|
||||
#[serde(default = "default_tracecontent")]
|
||||
pub content: TraceContent,
|
||||
}
|
||||
|
||||
#[derive(Default,Deserialize,Serialize,Debug,Clone)]
|
||||
pub struct TraceData {
|
||||
#[serde(default = "default_empty")]
|
||||
pub user_id: String,
|
||||
#[serde(default = "default_empty")]
|
||||
pub timestamp: String,
|
||||
#[serde(default = "default_tracedata_items")]
|
||||
pub contents: Vec<TraceContent>,
|
||||
}
|
||||
impl TraceData {
|
||||
fn id_path(&self, file: &str, config: &Config) -> String {
|
||||
if self.user_id.is_empty() {
|
||||
format!("{}/{}",
|
||||
config.trace_store_uri.replace(FILE_SCHEME, ""),
|
||||
file)
|
||||
} else {
|
||||
format!("{}/{}/{}",
|
||||
config.trace_store_uri.replace(FILE_SCHEME, ""),
|
||||
self.user_id,file)
|
||||
}
|
||||
}
|
||||
fn write_data(&self, file_path: &str, data: &str, overwrite: bool, verbose: u8 ) -> std::io::Result<()> {
|
||||
let check_path = |path: &Path| -> std::io::Result<()> {
|
||||
if ! Path::new(&path).exists() {
|
||||
if let Err(e) = std::fs::create_dir(&path) {
|
||||
return Err(Error::new( ErrorKind::InvalidInput,
|
||||
format!("Error create path {}: {}",&path.display(), e)
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
if file_path.is_empty() || data.is_empty() {
|
||||
return Err(Error::new(
|
||||
ErrorKind::InvalidInput,
|
||||
format!("Error save {}",&file_path)
|
||||
));
|
||||
}
|
||||
if ! Path::new(&file_path).exists() {
|
||||
let path = PathBuf::from(&file_path);
|
||||
if let Some(dir_path) = path.parent() {
|
||||
if ! Path::new(&dir_path).exists() {
|
||||
if let Some(parent_dir_path) = dir_path.parent() {
|
||||
if ! Path::new(&parent_dir_path).exists() {
|
||||
let res = check_path(&parent_dir_path);
|
||||
if res.is_err() { return res; }
|
||||
}
|
||||
}
|
||||
let res = check_path(&dir_path);
|
||||
if res.is_err() { return res; }
|
||||
}
|
||||
}
|
||||
}
|
||||
if overwrite || ! Path::new(&file_path).exists() {
|
||||
fs::write(&file_path, data)?;
|
||||
if verbose > 2 { println!("Overwrite: {}",&file_path); }
|
||||
} else {
|
||||
let sid_settings_file = fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.append(true) // This is needed to append to file
|
||||
.open(&file_path);
|
||||
if let Ok(mut file) = sid_settings_file {
|
||||
file.write_all(data.as_bytes())?;
|
||||
}
|
||||
if verbose > 2 { println!("write: {}",&file_path); }
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
pub fn contents_to_json(&self) -> Vec<String> {
|
||||
self.contents.clone().into_iter().map(|item| item.to_json()).collect()
|
||||
}
|
||||
pub fn save(&self, config: &Config, overwrite: bool) -> std::io::Result<()> {
|
||||
let file_trace_path = self.id_path(&format!("{}",SID_TRACE_FILE), config);
|
||||
let contents = self.contents_to_json();
|
||||
let mut result = Ok(());
|
||||
let cnt_lines = contents.len();
|
||||
let lines = if cnt_lines > 0 {
|
||||
cnt_lines - 1
|
||||
} else {
|
||||
cnt_lines
|
||||
};
|
||||
let mut write_overwrite = overwrite;
|
||||
for (idx, line) in contents.iter().enumerate() {
|
||||
let prfx = if idx == 0 && Path::new(&file_trace_path).exists() {
|
||||
",\n"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let sfx = if idx == lines {
|
||||
""
|
||||
} else {
|
||||
",\n"
|
||||
};
|
||||
result = self.write_data(
|
||||
&file_trace_path,
|
||||
format!("{}{}{}",&prfx,&line,&sfx).as_str(),
|
||||
write_overwrite,
|
||||
config.verbose
|
||||
);
|
||||
let _ = result.as_ref().unwrap_or_else(|e|{
|
||||
println!("Error save trace contets: {} line {}",&e, &idx);
|
||||
&()
|
||||
});
|
||||
if write_overwrite { write_overwrite = false}
|
||||
if result.is_err() { break; }
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
pub fn get_reader(self, config: &Config) -> std::io::Result<BufReader<File>> {
|
||||
let file_path = self.id_path(&format!("{}",SID_TRACE_FILE), config);
|
||||
if ! Path::new(&file_path).exists() {
|
||||
error!("Error file path not exist: {}",&file_path);
|
||||
return Err(Error::new(
|
||||
ErrorKind::InvalidInput,
|
||||
format!("Error file path not exist: {}",file_path)
|
||||
));
|
||||
}
|
||||
let file = File::open(&file_path)?;
|
||||
let reader = BufReader::new(file);
|
||||
Ok(reader)
|
||||
}
|
||||
pub fn load(&self, config: &Config, human: bool, reverse: bool) -> Vec<TraceLogContent> {
|
||||
let mut log: Vec<TraceLogContent> = Vec::new();
|
||||
match self.clone().get_reader(config) {
|
||||
Ok(reader) => {
|
||||
let mut line_pos = 0;
|
||||
for line in reader.lines() {
|
||||
line_pos +=1;
|
||||
let line = match line {
|
||||
Ok(res) => if res.ends_with(",") {
|
||||
res[0..res.len() - 1].to_owned()
|
||||
} else {
|
||||
res
|
||||
},
|
||||
Err(_) => continue,
|
||||
};
|
||||
if ! line.is_empty() {
|
||||
let mut log_line: TraceContent = serde_json::from_str(&line).unwrap_or_else(|e| {
|
||||
println!("Error parse load line {}: {}", &line_pos,e);
|
||||
TraceContent::default()
|
||||
});
|
||||
if ! log_line.when.is_empty() {
|
||||
let line_id = log_line.when.to_owned();
|
||||
if human {
|
||||
log_line.when = str_date_from_timestamp(&log_line.when);
|
||||
}
|
||||
log.push(TraceLogContent { line_id, content: log_line })
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Error on load log: {}",e);
|
||||
}
|
||||
}
|
||||
if reverse { log.reverse(); }
|
||||
log
|
||||
}
|
||||
pub fn clean(&self, config: &Config, line_id: &str) -> std::io::Result<()> {
|
||||
let file_path = self.id_path(&format!("{}",SID_TRACE_FILE), config);
|
||||
if ! Path::new(&file_path).exists() {
|
||||
error!("Error file path not exist: {}",&file_path);
|
||||
return Err(Error::new(
|
||||
ErrorKind::InvalidInput,
|
||||
format!("Error file path not exist: {}",file_path)
|
||||
));
|
||||
}
|
||||
if line_id == "ALL" {
|
||||
std::fs::remove_file(&file_path)
|
||||
} else {
|
||||
let mut log: Vec<TraceContent> = Vec::new();
|
||||
match self.clone().get_reader(config) {
|
||||
Ok(reader) => {
|
||||
let mut line_pos = 0;
|
||||
for line in reader.lines() {
|
||||
line_pos +=1;
|
||||
let line = match line {
|
||||
Ok(res) => if res.ends_with(",") {
|
||||
res[0..res.len() - 1].to_owned()
|
||||
} else {
|
||||
res
|
||||
},
|
||||
Err(_) => continue,
|
||||
};
|
||||
if ! line.is_empty() {
|
||||
let log_line: TraceContent = serde_json::from_str(&line).unwrap_or_else(|e| {
|
||||
println!("Error parse load line {}: {}", &line_pos,e);
|
||||
TraceContent::default()
|
||||
});
|
||||
if ! log_line.when.is_empty() && log_line.when != line_id {
|
||||
log.push(log_line)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Error on load log: {}",e);
|
||||
}
|
||||
}
|
||||
if log.len() == 0 { return std::fs::remove_file(&file_path) }
|
||||
let trace_data = TraceData {
|
||||
user_id: self.user_id.to_owned(),
|
||||
timestamp: String::from(""),
|
||||
contents: log
|
||||
};
|
||||
trace_data.save(config, true).unwrap_or_else(|e|{
|
||||
println!("Error save filter {} to path {}: {}",line_id, &file_path,&e);
|
||||
error!("Error save filter {} to path {}: {}",&line_id, &file_path,e);
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
15
src/handlers.rs
Normal file
15
src/handlers.rs
Normal file
@ -0,0 +1,15 @@
|
||||
mod users_handlers;
|
||||
mod other_handlers;
|
||||
mod pages_handlers;
|
||||
mod admin_handlers;
|
||||
|
||||
pub(crate) use users_handlers::users_router_handlers;
|
||||
pub(crate) use admin_handlers::admin_router_handlers;
|
||||
pub(crate) use pages_handlers::pages_router_handlers;
|
||||
|
||||
pub(crate) use other_handlers::{
|
||||
rewrite_request_uri,
|
||||
add_session_cookie,
|
||||
get_auth_state,
|
||||
handle_404,
|
||||
};
|
742
src/handlers/admin_handlers.rs
Normal file
742
src/handlers/admin_handlers.rs
Normal file
@ -0,0 +1,742 @@
|
||||
use std::sync::Arc;
|
||||
use axum::{
|
||||
http::{
|
||||
StatusCode,
|
||||
Uri,
|
||||
header::HeaderMap,
|
||||
},
|
||||
Json,
|
||||
routing::{get,post},
|
||||
Extension,
|
||||
extract::ConnectInfo,
|
||||
response::{IntoResponse,Response,Redirect},
|
||||
Router,
|
||||
};
|
||||
use tower_cookies::Cookies;
|
||||
|
||||
use crate::{
|
||||
route,
|
||||
defs::{
|
||||
AppDBs,
|
||||
Random,
|
||||
ReqHandler,
|
||||
ReqHeaderMap,
|
||||
AppConnectInfo, TraceData,
|
||||
},
|
||||
users::{
|
||||
User,
|
||||
UserItem,
|
||||
UserStatus,
|
||||
},
|
||||
handlers::{
|
||||
add_session_cookie,
|
||||
get_auth_state,
|
||||
},
|
||||
};
|
||||
pub fn admin_router_handlers() -> Router {
|
||||
async fn users_handler(
|
||||
header: HeaderMap,
|
||||
uri: Uri,
|
||||
Extension(app_dbs): Extension<Arc<AppDBs>>,
|
||||
Extension(cookies): Extension<Cookies>,
|
||||
Extension(random): Extension<Random>,
|
||||
ConnectInfo(app_connect_info): ConnectInfo<AppConnectInfo>,
|
||||
) -> Response {
|
||||
let auth_state = get_auth_state(true, &cookies, &app_dbs).await;
|
||||
let mut req_handler = ReqHandler::new(
|
||||
ReqHeaderMap::new(header, &format!("{}",&uri.path().to_string()), &app_connect_info),
|
||||
&app_dbs,
|
||||
&uri,
|
||||
&auth_state,
|
||||
&random,
|
||||
"users_handler"
|
||||
);
|
||||
if !auth_state.is_admin() {
|
||||
let _ = req_handler.trace_req(format!("User: {} is not admin",auth_state.user_id()));
|
||||
return Redirect::temporary( &format!("/login?o={}",uri.path().to_string())).into_response();
|
||||
}
|
||||
let usrs = match User::list(&app_dbs.user_store, true, false, "").await {
|
||||
Ok(data) => data,
|
||||
Err(e) => {
|
||||
let _ = req_handler.trace_req(format!("Error list users: {}",e));
|
||||
println!("Error list users: {}",e);
|
||||
Vec::new()
|
||||
},
|
||||
};
|
||||
req_handler.context.insert("usrs", &usrs);
|
||||
req_handler.context.insert("total_usrs", &usrs.len());
|
||||
req_handler.context.insert("with_menu", "1");
|
||||
let mut res_headers = HeaderMap::new();
|
||||
if req_handler.req_header.is_browser() {
|
||||
res_headers.append(axum::http::header::CONTENT_TYPE,"text/html; charset=utf-8".parse().unwrap());
|
||||
}
|
||||
let result = if let Some(tpl) = app_dbs.config.tpls.get("users") {
|
||||
req_handler.render_template(&tpl,"Users")
|
||||
} else {
|
||||
String::from("Users")
|
||||
};
|
||||
let _ = req_handler.trace_req(format!("Render users list"));
|
||||
(
|
||||
res_headers,
|
||||
result.to_owned()
|
||||
).into_response()
|
||||
}
|
||||
async fn user_get_handler(
|
||||
header: HeaderMap,
|
||||
uri: Uri,
|
||||
Extension(app_dbs): Extension<Arc<AppDBs>>,
|
||||
Extension(cookies): Extension<Cookies>,
|
||||
Extension(random): Extension<Random>,
|
||||
ConnectInfo(app_connect_info): ConnectInfo<AppConnectInfo>,
|
||||
Json(user_item): Json<UserItem>,
|
||||
) -> Response {
|
||||
let auth_state = get_auth_state(true, &cookies, &app_dbs).await;
|
||||
let req_handler = ReqHandler::new(
|
||||
ReqHeaderMap::new(header, &format!("{}",&uri.path().to_string()), &app_connect_info),
|
||||
&app_dbs,
|
||||
&uri,
|
||||
&auth_state,
|
||||
&random,
|
||||
"user_get_handler"
|
||||
);
|
||||
if auth_state.session.is_none() {
|
||||
let _ = req_handler.trace_req(format!("No session found"));
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
req_handler.req_header.header,
|
||||
"Error"
|
||||
).into_response();
|
||||
}
|
||||
let mut res_headers = HeaderMap::new();
|
||||
res_headers.append(axum::http::header::CONTENT_TYPE,"application/json; charset=utf-8".parse().unwrap());
|
||||
let user_id = auth_state.user_id();
|
||||
if user_id.is_empty() {
|
||||
let _ = req_handler.trace_req(format!("No user found"));
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
res_headers,
|
||||
"Error"
|
||||
).into_response();
|
||||
}
|
||||
if !auth_state.is_admin() {
|
||||
let _ = req_handler.trace_req(format!("User: {} is not admin",auth_state.user_id()));
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
res_headers,
|
||||
"Error"
|
||||
).into_response();
|
||||
}
|
||||
let mut user_sel = User::select(&user_item.name, &user_item.value, true,&app_dbs.user_store).await.unwrap_or_default();
|
||||
if user_sel.name.is_empty() {
|
||||
// User not exists
|
||||
let _ = req_handler.trace_req(format!("User '{}' = {}' not found",&user_item.name, &user_item.value));
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
res_headers,
|
||||
"Error"
|
||||
).into_response();
|
||||
}
|
||||
user_sel.password = String::from("");
|
||||
user_sel.otp_base32 = String::from("");
|
||||
let result = serde_json::to_string(&user_sel).unwrap_or_else(|e|{
|
||||
let msg = format!("Error to convert user items to json: {}",e);
|
||||
println!("{}", &msg);
|
||||
let _ = req_handler.trace_req(msg);
|
||||
String::from("")
|
||||
});
|
||||
let _ = req_handler.trace_req(format!("User: {} get data", &user_sel.id));
|
||||
(
|
||||
res_headers,
|
||||
result.to_owned()
|
||||
).into_response()
|
||||
}
|
||||
async fn user_save_handler(
|
||||
header: HeaderMap,
|
||||
uri: Uri,
|
||||
Extension(app_dbs): Extension<Arc<AppDBs>>,
|
||||
Extension(cookies): Extension<Cookies>,
|
||||
Extension(random): Extension<Random>,
|
||||
ConnectInfo(app_connect_info): ConnectInfo<AppConnectInfo>,
|
||||
Json(new_user): Json<User>,
|
||||
) -> Response {
|
||||
let auth_state = get_auth_state(true, &cookies, &app_dbs).await;
|
||||
let req_handler = ReqHandler::new(
|
||||
ReqHeaderMap::new(header, &format!("{}",&uri.path().to_string()), &app_connect_info),
|
||||
&app_dbs,
|
||||
&uri,
|
||||
&auth_state,
|
||||
&random,
|
||||
"user_save_handler"
|
||||
);
|
||||
if auth_state.session.is_none() {
|
||||
let _ = req_handler.trace_req(format!("No session found"));
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
req_handler.req_header.header,
|
||||
"Error"
|
||||
).into_response();
|
||||
}
|
||||
let mut res_headers = HeaderMap::new();
|
||||
res_headers.append(axum::http::header::CONTENT_TYPE,"text/plain; charset=utf-8".parse().unwrap());
|
||||
let user_id = auth_state.user_id();
|
||||
if user_id.is_empty() {
|
||||
let _ = req_handler.trace_req(format!("No user found"));
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
res_headers,
|
||||
"Error"
|
||||
).into_response();
|
||||
}
|
||||
if !auth_state.is_admin() {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
res_headers,
|
||||
"Error"
|
||||
).into_response();
|
||||
}
|
||||
let mut user_sel = User::select("id", &format!("{}",&new_user.id), false,&app_dbs.user_store).await.unwrap_or_default();
|
||||
if user_sel.name.is_empty() {
|
||||
// User not exists
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
res_headers,
|
||||
"Error"
|
||||
).into_response();
|
||||
}
|
||||
user_sel.from_user(new_user);
|
||||
let user_data = user_sel.session_data();
|
||||
if user_sel.update(&app_dbs.user_store).await.is_err() {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
req_handler.req_header.header,
|
||||
"Error"
|
||||
).into_response();
|
||||
}
|
||||
let session_token = req_handler.new_token();
|
||||
let session_cookie = add_session_cookie(true,&cookies, &session_token, &user_data, 0, &app_dbs, "/").await;
|
||||
if app_dbs.config.verbose > 1 { println!("session cookie: {}", &session_cookie) };
|
||||
let result = String::from("Ok");
|
||||
(
|
||||
res_headers,
|
||||
result
|
||||
).into_response()
|
||||
}
|
||||
async fn user_delete_handler(
|
||||
header: HeaderMap,
|
||||
uri: Uri,
|
||||
Extension(app_dbs): Extension<Arc<AppDBs>>,
|
||||
Extension(cookies): Extension<Cookies>,
|
||||
Extension(random): Extension<Random>,
|
||||
ConnectInfo(app_connect_info): ConnectInfo<AppConnectInfo>,
|
||||
Json(user_item): Json<UserItem>,
|
||||
) -> Response {
|
||||
let auth_state = get_auth_state(true, &cookies, &app_dbs).await;
|
||||
let req_handler = ReqHandler::new(
|
||||
ReqHeaderMap::new(header, &format!("{}",&uri.path().to_string()), &app_connect_info),
|
||||
&app_dbs,
|
||||
&uri,
|
||||
&auth_state,
|
||||
&random,
|
||||
"user_delete_handler"
|
||||
);
|
||||
if auth_state.session.is_none() {
|
||||
let _ = req_handler.trace_req(format!("No session found"));
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
req_handler.req_header.header,
|
||||
"Error"
|
||||
).into_response();
|
||||
}
|
||||
// let req_handler = ReqHandler::new(
|
||||
let mut res_headers = HeaderMap::new();
|
||||
res_headers.append(axum::http::header::CONTENT_TYPE,"application/json; charset=utf-8".parse().unwrap());
|
||||
let user_id = auth_state.user_id();
|
||||
if user_id.is_empty() {
|
||||
let _ = req_handler.trace_req(format!("No user found"));
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
res_headers,
|
||||
"Error"
|
||||
).into_response();
|
||||
}
|
||||
if !auth_state.is_admin() {
|
||||
let _ = req_handler.trace_req(format!("User: {} is not admin",auth_state.user_id()));
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
res_headers,
|
||||
"Error"
|
||||
).into_response();
|
||||
}
|
||||
let user_sel = User::select(&user_item.name, &user_item.value, true,&app_dbs.user_store).await.unwrap_or_default();
|
||||
if user_sel.name.is_empty() {
|
||||
let _ = req_handler.trace_req(format!("User '{}' not found",&user_id));
|
||||
// User not exists
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
res_headers,
|
||||
"Error"
|
||||
).into_response();
|
||||
}
|
||||
let result = match User::delete(user_sel.id,&app_dbs.user_store).await {
|
||||
Ok(val) => if val {
|
||||
let _ = req_handler.trace_req(format!("User '{}' delete",&user_id));
|
||||
format!("Ok")
|
||||
} else {
|
||||
let _ = req_handler.trace_req(format!("Error delete user '{}'" ,&user_id));
|
||||
format!("Error user not deleted")
|
||||
},
|
||||
Err(e) => {
|
||||
let _ = req_handler.trace_req(format!("Delte user '{}' Error: {}",&user_id,e));
|
||||
println!("Error delete user: {}", e);
|
||||
format!("Error on delete user")
|
||||
}
|
||||
};
|
||||
(
|
||||
res_headers,
|
||||
result.to_owned()
|
||||
).into_response()
|
||||
}
|
||||
async fn user_as_admin_handler(
|
||||
header: HeaderMap,
|
||||
uri: Uri,
|
||||
Extension(app_dbs): Extension<Arc<AppDBs>>,
|
||||
Extension(cookies): Extension<Cookies>,
|
||||
Extension(random): Extension<Random>,
|
||||
ConnectInfo(app_connect_info): ConnectInfo<AppConnectInfo>,
|
||||
Json(user_item): Json<UserItem>,
|
||||
) -> Response {
|
||||
let auth_state = get_auth_state(true, &cookies, &app_dbs).await;
|
||||
let req_handler = ReqHandler::new(
|
||||
ReqHeaderMap::new(header, &format!("{}",&uri.path().to_string()), &app_connect_info),
|
||||
&app_dbs,
|
||||
&uri,
|
||||
&auth_state,
|
||||
&random,
|
||||
"user_as_admin_handler"
|
||||
);
|
||||
if auth_state.session.is_none() {
|
||||
let _ = req_handler.trace_req(format!("No session found"));
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
req_handler.req_header.header,
|
||||
"Error"
|
||||
).into_response();
|
||||
}
|
||||
let mut res_headers = HeaderMap::new();
|
||||
res_headers.append(axum::http::header::CONTENT_TYPE,"application/json; charset=utf-8".parse().unwrap());
|
||||
let user_id = auth_state.user_id();
|
||||
if user_id.is_empty() {
|
||||
let _ = req_handler.trace_req(format!("No user found"));
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
res_headers,
|
||||
"Error"
|
||||
).into_response();
|
||||
}
|
||||
if !auth_state.is_admin() {
|
||||
let _ = req_handler.trace_req(format!("User: {} is not admin",auth_state.user_id()));
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
res_headers,
|
||||
"Error"
|
||||
).into_response();
|
||||
}
|
||||
let mut user_sel = User::select(&user_item.name, &user_item.value, true,&app_dbs.user_store).await.unwrap_or_default();
|
||||
if user_sel.name.is_empty() {
|
||||
let _ = req_handler.trace_req(format!("User '{}' = '{}' not found",&user_item.name, &user_item.value));
|
||||
// User not exists
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
res_headers,
|
||||
"Error"
|
||||
).into_response();
|
||||
}
|
||||
let result = if user_sel.isadmin {
|
||||
let _ = req_handler.trace_req(format!("User '{}' already admin",&user_id));
|
||||
format!("User already admin")
|
||||
} else {
|
||||
user_sel.isadmin = true;
|
||||
let user_data = user_sel.session_data();
|
||||
match user_sel.update(&app_dbs.user_store).await {
|
||||
Ok(_) => {
|
||||
let _ = req_handler.trace_req(format!("User '{}' updated",&user_id));
|
||||
let session_token = req_handler.new_token();
|
||||
let session_cookie = add_session_cookie(true,&cookies, &session_token, &user_data, 0, &app_dbs, "/").await;
|
||||
if app_dbs.config.verbose > 1 { println!("session cookie: {}", &session_cookie) };
|
||||
String::from("Ok")
|
||||
},
|
||||
Err(e) => {
|
||||
let _ = req_handler.trace_req(format!("User '{}' update error: {}",&user_id,e));
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
req_handler.req_header.header,
|
||||
"Error"
|
||||
).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
(
|
||||
res_headers,
|
||||
result.to_owned()
|
||||
).into_response()
|
||||
}
|
||||
async fn user_disable_totp_handler(
|
||||
header: HeaderMap,
|
||||
uri: Uri,
|
||||
Extension(app_dbs): Extension<Arc<AppDBs>>,
|
||||
Extension(cookies): Extension<Cookies>,
|
||||
Extension(random): Extension<Random>,
|
||||
ConnectInfo(app_connect_info): ConnectInfo<AppConnectInfo>,
|
||||
Json(user_item): Json<UserItem>,
|
||||
) -> Response {
|
||||
let auth_state = get_auth_state(true, &cookies, &app_dbs).await;
|
||||
let req_handler = ReqHandler::new(
|
||||
ReqHeaderMap::new(header, &format!("{}",&uri.path().to_string()), &app_connect_info),
|
||||
&app_dbs,
|
||||
&uri,
|
||||
&auth_state,
|
||||
&random,
|
||||
"user_disable_totp_handler"
|
||||
);
|
||||
if auth_state.session.is_none() {
|
||||
let _ = req_handler.trace_req(format!("No session found"));
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
req_handler.req_header.header,
|
||||
"Error"
|
||||
).into_response();
|
||||
}
|
||||
let mut res_headers = HeaderMap::new();
|
||||
res_headers.append(axum::http::header::CONTENT_TYPE,"application/json; charset=utf-8".parse().unwrap());
|
||||
let user_id = auth_state.user_id();
|
||||
if user_id.is_empty() {
|
||||
let _ = req_handler.trace_req(format!("No user found"));
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
res_headers,
|
||||
"Error"
|
||||
).into_response();
|
||||
}
|
||||
if !auth_state.is_admin() {
|
||||
let _ = req_handler.trace_req(format!("User: {} is not admin",auth_state.user_id()));
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
res_headers,
|
||||
"Error"
|
||||
).into_response();
|
||||
}
|
||||
let mut user_sel = User::select(&user_item.name, &user_item.value, true,&app_dbs.user_store).await.unwrap_or_default();
|
||||
if user_sel.name.is_empty() {
|
||||
// User not exists
|
||||
let _ = req_handler.trace_req(format!("User '{}' = '{}' not found",&user_item.name, &user_item.value));
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
res_headers,
|
||||
"Error"
|
||||
).into_response();
|
||||
}
|
||||
let result = if user_sel.otp_enabled && !user_sel.otp_base32.is_empty() {
|
||||
user_sel.disable_totp();
|
||||
let user_data = user_sel.session_data();
|
||||
match user_sel.update(&app_dbs.user_store).await {
|
||||
Ok(_) => {
|
||||
let session_token = req_handler.new_token();
|
||||
let session_cookie = add_session_cookie(true,&cookies, &session_token, &user_data, 0, &app_dbs, "/").await;
|
||||
if app_dbs.config.verbose > 1 { println!("session cookie: {}", &session_cookie) };
|
||||
String::from("Ok")
|
||||
},
|
||||
Err(e) => {
|
||||
let _ = req_handler.trace_req(format!("User '{}' = '{}' TOTP update error: {}",&user_item.name,&user_item.value,e));
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
req_handler.req_header.header,
|
||||
"Error"
|
||||
).into_response();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let _ = req_handler.trace_req(format!("User '{}' TOTP no enabled",&user_sel.id));
|
||||
format!("User does not have TOTP enabled")
|
||||
};
|
||||
(
|
||||
res_headers,
|
||||
result.to_owned()
|
||||
).into_response()
|
||||
}
|
||||
async fn user_passwd_reset_handler(
|
||||
header: HeaderMap,
|
||||
uri: Uri,
|
||||
Extension(app_dbs): Extension<Arc<AppDBs>>,
|
||||
Extension(cookies): Extension<Cookies>,
|
||||
Extension(random): Extension<Random>,
|
||||
ConnectInfo(app_connect_info): ConnectInfo<AppConnectInfo>,
|
||||
Json(user_item): Json<UserItem>,
|
||||
) -> Response {
|
||||
let auth_state = get_auth_state(true, &cookies, &app_dbs).await;
|
||||
let req_handler = ReqHandler::new(
|
||||
ReqHeaderMap::new(header, &format!("{}",&uri.path().to_string()), &app_connect_info),
|
||||
&app_dbs,
|
||||
&uri,
|
||||
&auth_state,
|
||||
&random,
|
||||
"user_passwd_reset_handler"
|
||||
);
|
||||
if auth_state.session.is_none() {
|
||||
let _ = req_handler.trace_req(format!("No session found"));
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
req_handler.req_header.header,
|
||||
"Error"
|
||||
).into_response();
|
||||
}
|
||||
let mut res_headers = HeaderMap::new();
|
||||
res_headers.append(axum::http::header::CONTENT_TYPE,"application/json; charset=utf-8".parse().unwrap());
|
||||
let user_id = auth_state.user_id();
|
||||
if user_id.is_empty() {
|
||||
let _ = req_handler.trace_req(format!("No user found"));
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
res_headers,
|
||||
"Error"
|
||||
).into_response();
|
||||
}
|
||||
if !auth_state.is_admin() {
|
||||
let _ = req_handler.trace_req(format!("User: {} is not admin",auth_state.user_id()));
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
res_headers,
|
||||
"Error"
|
||||
).into_response();
|
||||
}
|
||||
let mut user_sel = User::select(&user_item.name, &user_item.value, true,&app_dbs.user_store).await.unwrap_or_default();
|
||||
if user_sel.name.is_empty() {
|
||||
// User not exists
|
||||
let _ = req_handler.trace_req(format!("User '{}' = '{}' not found",&user_item.name, &user_item.value));
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
res_headers,
|
||||
"Error"
|
||||
).into_response();
|
||||
}
|
||||
if user_sel.status != UserStatus::Active {
|
||||
let _ = req_handler.trace_req(format!("User '{}' is not '{}' state",&user_item.value, UserStatus::Active));
|
||||
(
|
||||
StatusCode::BAD_REQUEST,
|
||||
res_headers,
|
||||
"Error User not active !!"
|
||||
).into_response()
|
||||
} else {
|
||||
user_sel.status = UserStatus::Pending;
|
||||
// TODO send email notification and redirect in login to completed pending task
|
||||
let user_data = user_sel.session_data();
|
||||
match user_sel.update(&app_dbs.user_store).await {
|
||||
Ok(_) => {
|
||||
let session_token = req_handler.new_token();
|
||||
let session_cookie = add_session_cookie(true,&cookies, &session_token, &user_data, 0, &app_dbs, "/").await;
|
||||
if app_dbs.config.verbose > 1 { println!("session cookie: {}", &session_cookie) };
|
||||
let result = String::from("Ok");
|
||||
(
|
||||
res_headers,
|
||||
result.to_owned()
|
||||
).into_response()
|
||||
},
|
||||
Err(e) => {
|
||||
let _ = req_handler.trace_req(format!("User '{}' update error: {}",&user_item.value, e));
|
||||
(
|
||||
StatusCode::BAD_REQUEST,
|
||||
req_handler.req_header.header,
|
||||
"Error"
|
||||
).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
async fn logs_handler(
|
||||
header: HeaderMap,
|
||||
uri: Uri,
|
||||
Extension(app_dbs): Extension<Arc<AppDBs>>,
|
||||
Extension(cookies): Extension<Cookies>,
|
||||
Extension(random): Extension<Random>,
|
||||
ConnectInfo(app_connect_info): ConnectInfo<AppConnectInfo>,
|
||||
) -> Response {
|
||||
let auth_state = get_auth_state(true, &cookies, &app_dbs).await;
|
||||
let mut req_handler = ReqHandler::new(
|
||||
ReqHeaderMap::new(header, &format!("{}",&uri.path().to_string()), &app_connect_info),
|
||||
&app_dbs,
|
||||
&uri,
|
||||
&auth_state,
|
||||
&random,
|
||||
"log_handler"
|
||||
);
|
||||
if auth_state.session.is_none() {
|
||||
let _ = req_handler.trace_req(format!("No session found"));
|
||||
return Redirect::temporary( &format!("/login?o={}",uri.path().to_string())).into_response();
|
||||
}
|
||||
let user_id = auth_state.user_id();
|
||||
if user_id.is_empty() {
|
||||
let _ = req_handler.trace_req(format!("No user found"));
|
||||
return Redirect::temporary( &format!("/login?o={}",uri.path().to_string())).into_response();
|
||||
}
|
||||
if !auth_state.is_admin() {
|
||||
let _ = req_handler.trace_req(format!("User: {} is not admin",auth_state.user_id()));
|
||||
return Redirect::temporary( &format!("/login?o={}",uri.path().to_string())).into_response();
|
||||
}
|
||||
let mut res_headers = HeaderMap::new();
|
||||
if req_handler.req_header.is_browser() {
|
||||
res_headers.append(axum::http::header::CONTENT_TYPE,"text/html; charset=utf-8".parse().unwrap());
|
||||
}
|
||||
let logs = req_handler.logs("", true, true);
|
||||
let usr = User::default();
|
||||
req_handler.context.insert("user",&usr);
|
||||
req_handler.context.insert("logs", &logs);
|
||||
req_handler.context.insert("total_logs", &logs.len());
|
||||
req_handler.context.insert("with_menu", "1");
|
||||
let result = if let Some(tpl) = app_dbs.config.tpls.get("logs") {
|
||||
req_handler.render_template(&tpl,"Logs")
|
||||
} else {
|
||||
String::from("Logs")
|
||||
};
|
||||
// Turn off logging this here !!!
|
||||
// let _ = req_handler.trace_req(format!("Render logs"));
|
||||
(
|
||||
res_headers,
|
||||
result.to_owned()
|
||||
).into_response()
|
||||
}
|
||||
async fn log_id_handler(
|
||||
header: HeaderMap,
|
||||
uri: Uri,
|
||||
Extension(app_dbs): Extension<Arc<AppDBs>>,
|
||||
Extension(cookies): Extension<Cookies>,
|
||||
Extension(random): Extension<Random>,
|
||||
ConnectInfo(app_connect_info): ConnectInfo<AppConnectInfo>,
|
||||
axum::extract::Path(userid): axum::extract::Path<String>,
|
||||
) -> Response {
|
||||
let auth_state = get_auth_state(true, &cookies, &app_dbs).await;
|
||||
let mut req_handler = ReqHandler::new(
|
||||
ReqHeaderMap::new(header, &format!("{}",&uri.path().to_string()), &app_connect_info),
|
||||
&app_dbs,
|
||||
&uri,
|
||||
&auth_state,
|
||||
&random,
|
||||
"log_id_handler"
|
||||
);
|
||||
if auth_state.session.is_none() {
|
||||
let _ = req_handler.trace_req(format!("No session found"));
|
||||
return Redirect::temporary( &format!("/login?o={}",uri.path().to_string())).into_response();
|
||||
}
|
||||
let user_id = auth_state.user_id();
|
||||
if user_id.is_empty() {
|
||||
let _ = req_handler.trace_req(format!("No user found"));
|
||||
return Redirect::temporary( &format!("/login?o={}",uri.path().to_string())).into_response();
|
||||
}
|
||||
if !auth_state.is_admin() {
|
||||
let _ = req_handler.trace_req(format!("User: {} is not admin",auth_state.user_id()));
|
||||
return Redirect::temporary( &format!("/login?o={}",uri.path().to_string())).into_response();
|
||||
}
|
||||
let user_sel = User::select("id", &userid, true,&app_dbs.user_store).await.unwrap_or_default();
|
||||
if user_sel.name.is_empty() {
|
||||
// User not exists
|
||||
let _ = req_handler.trace_req(format!("User 'id' = '{}' not found",&userid));
|
||||
return Redirect::temporary( &format!("/login?o={}",uri.path().to_string())).into_response();
|
||||
}
|
||||
let mut res_headers = HeaderMap::new();
|
||||
if req_handler.req_header.is_browser() {
|
||||
res_headers.append(axum::http::header::CONTENT_TYPE,"text/html; charset=utf-8".parse().unwrap());
|
||||
}
|
||||
let logs = req_handler.logs(&userid, true, true);
|
||||
req_handler.context.insert("user", &user_sel);
|
||||
req_handler.context.insert("logs", &logs);
|
||||
req_handler.context.insert("total_logs", &logs.len());
|
||||
req_handler.context.insert("with_menu", "1");
|
||||
let result = if let Some(tpl) = app_dbs.config.tpls.get("logs") {
|
||||
req_handler.render_template(&tpl,"Logs")
|
||||
} else {
|
||||
String::from("Logs")
|
||||
};
|
||||
// Turn off logging this here !!!
|
||||
// let _ = req_handler.trace_req(format!("Render logs id: {}", &userid));
|
||||
(
|
||||
res_headers,
|
||||
result.to_owned()
|
||||
).into_response()
|
||||
}
|
||||
async fn log_delete_handler(
|
||||
header: HeaderMap,
|
||||
uri: Uri,
|
||||
Extension(app_dbs): Extension<Arc<AppDBs>>,
|
||||
Extension(cookies): Extension<Cookies>,
|
||||
Extension(random): Extension<Random>,
|
||||
ConnectInfo(app_connect_info): ConnectInfo<AppConnectInfo>,
|
||||
Json(user_item): Json<UserItem>,
|
||||
) -> Response {
|
||||
let auth_state = get_auth_state(true, &cookies, &app_dbs).await;
|
||||
let req_handler = ReqHandler::new(
|
||||
ReqHeaderMap::new(header, &format!("{}",&uri.path().to_string()), &app_connect_info),
|
||||
&app_dbs,
|
||||
&uri,
|
||||
&auth_state,
|
||||
&random,
|
||||
"log_delete_handler"
|
||||
);
|
||||
if auth_state.session.is_none() {
|
||||
let _ = req_handler.trace_req(format!("No session found"));
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
req_handler.req_header.header,
|
||||
"Error"
|
||||
).into_response();
|
||||
}
|
||||
let mut res_headers = HeaderMap::new();
|
||||
res_headers.append(axum::http::header::CONTENT_TYPE,"application/json; charset=utf-8".parse().unwrap());
|
||||
let user_id = auth_state.user_id();
|
||||
if user_id.is_empty() {
|
||||
let _ = req_handler.trace_req(format!("No user found"));
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
res_headers,
|
||||
"Error"
|
||||
).into_response();
|
||||
}
|
||||
if !auth_state.is_admin() {
|
||||
let _ = req_handler.trace_req(format!("User: {} is not admin",auth_state.user_id()));
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
res_headers,
|
||||
"Error"
|
||||
).into_response();
|
||||
}
|
||||
let trace_data = TraceData { user_id: user_item.name.to_owned(), timestamp: String::from(""), contents: Vec::new() };
|
||||
match trace_data.clean(&app_dbs.config, &user_item.value) {
|
||||
Ok(_) => {
|
||||
// Turn off logging this here !!!
|
||||
// let _ = req_handler.trace_req(format!("Log '{}' = '{}' deleted",&user_item.name, &user_item.value));
|
||||
(
|
||||
res_headers,
|
||||
"OK".to_owned()
|
||||
).into_response()
|
||||
},
|
||||
Err(e) => {
|
||||
let msg = format!("Log '{}' = '{}' not deleted",&user_item.name, &user_item.value);
|
||||
let _ = req_handler.trace_req(format!("{}", &msg));
|
||||
println!("Error {}: {}", &msg,e);
|
||||
(
|
||||
StatusCode::BAD_REQUEST,
|
||||
res_headers,
|
||||
"Error"
|
||||
).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
route("/users", get(users_handler))
|
||||
.route("/userget", post(user_get_handler))
|
||||
.route("/usersave", post(user_save_handler))
|
||||
.route("/userdelete", post(user_delete_handler))
|
||||
.route("/userdisabletotp", post(user_disable_totp_handler))
|
||||
.route("/userasadmin", post(user_as_admin_handler))
|
||||
.route("/passwdreset", post(user_passwd_reset_handler))
|
||||
.route("/logs", get(logs_handler))
|
||||
.route("/log/:id", get(log_id_handler))
|
||||
.route("/logdelete", post(log_delete_handler))
|
||||
}
|
224
src/handlers/other_handlers.rs
Normal file
224
src/handlers/other_handlers.rs
Normal file
@ -0,0 +1,224 @@
|
||||
use std::sync::Arc;
|
||||
use casbin::CoreApi;
|
||||
use axum::{
|
||||
extract::{Request,ConnectInfo},
|
||||
http::{
|
||||
StatusCode,
|
||||
},
|
||||
Extension,
|
||||
response::{IntoResponse,Response,Redirect},
|
||||
middleware::Next,
|
||||
};
|
||||
use tower_cookies::{Cookie, Cookies};
|
||||
|
||||
use crate::{
|
||||
USER_AGENT,
|
||||
SESSION_COOKIE_NAME,
|
||||
defs::{
|
||||
AppDBs,
|
||||
ServPath,
|
||||
AuthState,
|
||||
SessionStoreDB,
|
||||
TraceData,
|
||||
TraceContent,
|
||||
ReqHeaderMap,
|
||||
AppConnectInfo,
|
||||
},
|
||||
};
|
||||
|
||||
/* // OLD get_cookie from Request
|
||||
pub fn get_cookie(req: &Request) -> Option<u128> {
|
||||
req
|
||||
.headers()
|
||||
.get_all("Cookie")
|
||||
.iter()
|
||||
.filter_map(|cookie| {
|
||||
cookie
|
||||
.to_str()
|
||||
.ok()
|
||||
.and_then(|cookie| cookie.parse::<Cookie>().ok())
|
||||
})
|
||||
.find_map(|cookie| {
|
||||
(cookie.name() == SESSION_COOKIE_NAME).then(move || cookie.value().to_owned())
|
||||
})
|
||||
.and_then(|cookie_value| cookie_value.parse::<u128>().ok())
|
||||
}
|
||||
*/
|
||||
|
||||
pub async fn add_session_cookie(make: bool, cookies: &Cookies, session_token: &str, user_data: &str, expire: u64, app_dbs: &AppDBs, cookie_path: &str) -> String {
|
||||
if make {
|
||||
cookies.remove(Cookie::new(SESSION_COOKIE_NAME, ""));
|
||||
}
|
||||
let result_store = SessionStoreDB::store_session_data(&session_token,&user_data, expire, &app_dbs).await;
|
||||
if result_store.is_empty() {
|
||||
eprintln!("Unable to store session {}", &app_dbs.config.session_store_uri);
|
||||
} else {
|
||||
let cookie = Cookie::build(SESSION_COOKIE_NAME, result_store.to_owned())
|
||||
// .domain(domain)
|
||||
.path(format!("{}",cookie_path))
|
||||
.secure(true)
|
||||
.http_only(true)
|
||||
.finish();
|
||||
if make {
|
||||
cookies.add(cookie);
|
||||
}
|
||||
}
|
||||
result_store
|
||||
}
|
||||
pub async fn get_auth_state(update: bool, cookies: &Cookies, app_dbs: &AppDBs) -> AuthState {
|
||||
if let Some(s_cookie) = cookies.get(SESSION_COOKIE_NAME) {
|
||||
let session_cookie = s_cookie.to_string().replace(&format!("{}=",SESSION_COOKIE_NAME),"");
|
||||
let mut auth_state = AuthState::from_cookie(session_cookie.to_string(), app_dbs).await;
|
||||
if update {
|
||||
let _ = auth_state.expire_in(app_dbs.config.session_expire, &app_dbs).await;
|
||||
}
|
||||
auth_state
|
||||
} else {
|
||||
// eprintln!("get_auth_state: No SESSION COOKIE found ");
|
||||
AuthState::default()
|
||||
}
|
||||
}
|
||||
pub fn trace_req(uri_path: &str, user_id: String, sid: String, info: String, context: String, role: String, req_header: ReqHeaderMap, app_dbs: &AppDBs) -> std::io::Result<()> {
|
||||
let timestamp = chrono::Utc::now().timestamp().to_string();
|
||||
let trace_content = TraceContent{
|
||||
when: timestamp.to_owned(),
|
||||
sid,
|
||||
origin: uri_path.to_owned(),
|
||||
trigger: String::from("req_handler"),
|
||||
id: user_id.to_owned(),
|
||||
info: info.to_owned(),
|
||||
context,
|
||||
role,
|
||||
req: req_header.req_info(),
|
||||
};
|
||||
let trace_data = TraceData{
|
||||
user_id,
|
||||
timestamp,
|
||||
contents: vec![trace_content]
|
||||
};
|
||||
trace_data.save(&app_dbs.config,false)
|
||||
}
|
||||
|
||||
pub async fn rewrite_request_uri(
|
||||
Extension(app_dbs): Extension<Arc<AppDBs>>,
|
||||
Extension(cookies): Extension<Cookies>,
|
||||
ConnectInfo(app_connect_info): ConnectInfo<AppConnectInfo>,
|
||||
req: Request, next: Next,
|
||||
) -> Result<impl IntoResponse, Response> {
|
||||
// TODO Trace acccess to log or user session file !!!
|
||||
let auth_state = get_auth_state(true, &cookies, &app_dbs).await;
|
||||
let uri_path = req.uri().path().to_owned();
|
||||
if uri_path == "/" {
|
||||
return Ok(next.run(req).await);
|
||||
}
|
||||
// For long path is better than:
|
||||
// let arr_root_path: Vec<String> = uri_path.split("/").map(|s| s.to_string()).collect();
|
||||
// let root_path = arr_root_path[1].to_owned();
|
||||
let mut root_path = String::from("/");
|
||||
for it in uri_path.split("/") {
|
||||
if ! it.is_empty() {
|
||||
root_path = format!("/{}",it.to_owned());
|
||||
break;
|
||||
}
|
||||
}
|
||||
let serv_paths: Vec<ServPath> = app_dbs.config.serv_paths.clone().into_iter().filter(
|
||||
|it| it.is_restricted && it.url_path == root_path
|
||||
).collect();
|
||||
// Only on First one
|
||||
if serv_paths.len() > 0 {
|
||||
let serv_path = serv_paths[0].to_owned();
|
||||
let name = auth_state.user_name();
|
||||
if name.is_empty() {
|
||||
let uri_path = req.uri().path().to_string();
|
||||
if uri_path.ends_with(".html") {
|
||||
eprintln!("rewrite_request_uri: No user found in session for {}", &uri_path);
|
||||
let new_uri = format!("{}?o={}",&serv_path.not_auth.as_str(),req.uri().path().to_string());
|
||||
let _ = trace_req(&uri_path, auth_state.user_id(),auth_state.id(),
|
||||
format!("user no name found in session"), String::from("rewrite_request_uri"),
|
||||
auth_state.user_roles(),
|
||||
ReqHeaderMap::new(req.headers().to_owned(), &uri_path, &app_connect_info),
|
||||
&app_dbs);
|
||||
return Err(
|
||||
Redirect::temporary( &new_uri).into_response()
|
||||
);
|
||||
} else {
|
||||
return Ok(next.run(req).await);
|
||||
}
|
||||
}
|
||||
let arr_roles: Vec<String> = auth_state.user_roles().split(",").map(|s| s.replace(" ", "").to_string()).collect();
|
||||
let req_method = req.method().to_string();
|
||||
let target_path = serv_path.url_path.to_owned();
|
||||
let enforcer = app_dbs.enforcer.clone();
|
||||
for role in arr_roles {
|
||||
let mut lock = enforcer.write().await;
|
||||
let result = lock.enforce_mut(
|
||||
vec![role.to_owned(),target_path.to_owned(), req_method.to_owned()]
|
||||
).unwrap_or_else(|e|{
|
||||
println!("Error enforce: {}",e);
|
||||
false
|
||||
});
|
||||
drop(lock);
|
||||
if result {
|
||||
if uri_path.ends_with(".html") || app_dbs.config.trace_level > 1 {
|
||||
let _ = trace_req(&uri_path,auth_state.user_id(),auth_state.id(),
|
||||
format!("user in session with role {}",role),
|
||||
String::from("rewrite_request_uri"),
|
||||
auth_state.user_roles(),
|
||||
ReqHeaderMap::new(req.headers().to_owned(), &uri_path, &app_connect_info),
|
||||
&app_dbs);
|
||||
}
|
||||
return Ok(next.run(req).await);
|
||||
}
|
||||
}
|
||||
// try with email
|
||||
let mut lock = enforcer.write().await;
|
||||
let result = lock.enforce_mut(
|
||||
vec![
|
||||
name,
|
||||
target_path.to_owned(),
|
||||
req_method.to_owned()
|
||||
]
|
||||
).unwrap_or_else(|e|{
|
||||
println!("Error enforce: {}",e);
|
||||
false
|
||||
});
|
||||
drop(lock);
|
||||
if result { return Ok(next.run(req).await); }
|
||||
let new_uri = format!("{}",serv_path.not_auth);
|
||||
let agent = if let Some(user_agent) = req.headers().get(USER_AGENT) {
|
||||
user_agent.to_str().unwrap_or("").to_owned()
|
||||
} else {
|
||||
String::from("")
|
||||
};
|
||||
if uri_path.ends_with(".html") || app_dbs.config.trace_level > 1 {
|
||||
let _ = trace_req(&uri_path,auth_state.user_id(),auth_state.id(),
|
||||
format!("user found in session"),
|
||||
String::from("rewrite_request_uri"),
|
||||
auth_state.user_roles(),
|
||||
ReqHeaderMap::new(req.headers().to_owned(), &uri_path, &app_connect_info),
|
||||
&app_dbs);
|
||||
}
|
||||
if agent.contains("curl") {
|
||||
return Ok(
|
||||
format!("Got to {}",&new_uri).into_response()
|
||||
);
|
||||
} else {
|
||||
return Err(
|
||||
Redirect::temporary(&new_uri).into_response()
|
||||
);
|
||||
}
|
||||
}
|
||||
if uri_path.ends_with(".html") || app_dbs.config.trace_level > 1 {
|
||||
let _ = trace_req(&uri_path,auth_state.user_id(),auth_state.id(),
|
||||
format!("user in session"),
|
||||
String::from("rewrite_request_uri"),
|
||||
auth_state.user_roles(),
|
||||
ReqHeaderMap::new(req.headers().to_owned(), &uri_path, &app_connect_info),
|
||||
&app_dbs);
|
||||
}
|
||||
Ok(next.run(req).await)
|
||||
}
|
||||
pub async fn handle_404(_req: Request) -> (StatusCode, &'static str) {
|
||||
(StatusCode::NOT_FOUND, "Not found")
|
||||
}
|
||||
|
109
src/handlers/pages_handlers.rs
Normal file
109
src/handlers/pages_handlers.rs
Normal file
@ -0,0 +1,109 @@
|
||||
use std::sync::Arc;
|
||||
use axum::{
|
||||
//extract::{self,Request,Query},
|
||||
http::{
|
||||
// StatusCode,
|
||||
Uri,
|
||||
header::HeaderMap,// HeaderValue},
|
||||
},
|
||||
// Json,
|
||||
routing::get,
|
||||
Extension,
|
||||
extract::ConnectInfo,
|
||||
response::{IntoResponse,Response},
|
||||
// http::Request, handler::HandlerWithoutStateExt, http::StatusCode, routing::get, Router,
|
||||
Router,
|
||||
};
|
||||
use tower_cookies::Cookies;
|
||||
|
||||
use crate::{
|
||||
route,
|
||||
defs::{
|
||||
AppDBs,
|
||||
Random,
|
||||
ReqHandler,
|
||||
ReqHeaderMap,
|
||||
AppConnectInfo,
|
||||
},
|
||||
handlers::get_auth_state,
|
||||
};
|
||||
|
||||
pub fn pages_router_handlers() -> Router {
|
||||
async fn page_handler(
|
||||
//req: Request,
|
||||
header: HeaderMap,
|
||||
uri: Uri,
|
||||
Extension(app_dbs): Extension<Arc<AppDBs>>,
|
||||
Extension(cookies): Extension<Cookies>,
|
||||
Extension(random): Extension<Random>,
|
||||
ConnectInfo(app_connect_info): ConnectInfo<AppConnectInfo>,
|
||||
axum::extract::Path(name): axum::extract::Path<String>,
|
||||
//auth_state: AuthState,
|
||||
) -> Response {
|
||||
// if let Some(cookie_value) = get_cookie(&req) {
|
||||
// println!("cookie_value: {}",&cookie_value.to_string());
|
||||
// // TODO check value
|
||||
// }
|
||||
//let has_cookie: bool;
|
||||
// dbg!(&auth_state.session);
|
||||
// if auth_state.user.is_none() {
|
||||
// eprintln!("No user found in session");
|
||||
// }
|
||||
let auth_state = get_auth_state(true, &cookies, &app_dbs).await;
|
||||
// has_cookie=true;
|
||||
// TODO check value
|
||||
// let sid: String = format!("{}",&auth_state.sid());
|
||||
// println!("auth_sid: {}",&sid);
|
||||
// println!("id: {}",&auth_state.id());
|
||||
// println!("is_expired: {}",&auth_state.ses_expired());
|
||||
// println!("is_destroyed: {}",&auth_state.ses_destroyed());
|
||||
// println!("validate: {}",&auth_state.ses_validate());
|
||||
// let _u = &auth_state.user;
|
||||
let mut req_handler = ReqHandler::new(
|
||||
ReqHeaderMap::new(header, &format!("{}",&uri.path().to_string()), &app_connect_info),
|
||||
&app_dbs,
|
||||
&uri,
|
||||
&auth_state,
|
||||
&random,
|
||||
"page_handler"
|
||||
);
|
||||
// if let Some(a) = auth_state {
|
||||
// println!("Auth State from root");
|
||||
// }
|
||||
// let uri_path = format!("{}",&uri.path().to_string());
|
||||
// dbg!("uri: {}",&uri_path);
|
||||
req_handler.prepare_response();
|
||||
req_handler.context.insert("with_menu", "1");
|
||||
let tpl_name = if name.contains(".html") {
|
||||
name.replace(".html","")
|
||||
} else {
|
||||
name
|
||||
};
|
||||
let result = req_handler.render_template(&format!("pages/{}.html.j2",&tpl_name), &tpl_name);
|
||||
// if !has_cookie {
|
||||
// let mut u128_pool = [0u8; 16];
|
||||
// match random.lock() {
|
||||
// Ok(mut r) => r.fill_bytes(&mut u128_pool),
|
||||
// Err(e) => println!("Error random: {}",e),
|
||||
// }
|
||||
// let session_token = u128::from_le_bytes(u128_pool).to_string();
|
||||
// let user_data = format!("{}|{}","jesus" ,"admin,dev");
|
||||
// let result_store = SessionStoreDB::store_session_data(&session_token,&user_data,&app_dbs).await;
|
||||
// println!("Rest store: {}",&result_store);
|
||||
// let cookie = Cookie::build(SESSION_COOKIE_NAME, session_token.to_owned())
|
||||
// // .domain(domain)
|
||||
// .path("/")
|
||||
// .secure(true)
|
||||
// .http_only(true)
|
||||
// .finish();
|
||||
// cookies.remove(Cookie::new(SESSION_COOKIE_NAME, ""));
|
||||
// cookies.add(cookie);
|
||||
// }
|
||||
(
|
||||
req_handler.req_header.header,
|
||||
result.to_owned()
|
||||
).into_response()
|
||||
// "Hello, World!"
|
||||
}
|
||||
route("/page/:name", get(page_handler))
|
||||
}
|
1718
src/handlers/users_handlers.rs
Normal file
1718
src/handlers/users_handlers.rs
Normal file
File diff suppressed because it is too large
Load Diff
75
src/login_password.rs
Normal file
75
src/login_password.rs
Normal file
@ -0,0 +1,75 @@
|
||||
// #![no_std]
|
||||
// #![doc = include_str!("../README.md")]
|
||||
// #![doc(
|
||||
// html_logo_url = "https://raw.githubusercontent.com/RustCrypto/media/8f1a9894/logo.svg",
|
||||
// html_favicon_url = "https://raw.githubusercontent.com/RustCrypto/media/8f1a9894/logo.svg"
|
||||
// )]
|
||||
// #![warn(
|
||||
// clippy::checked_conversions,
|
||||
// clippy::integer_arithmetic,
|
||||
// clippy::panic,
|
||||
// clippy::panic_in_result_fn,
|
||||
// clippy::unwrap_used,
|
||||
// missing_docs,
|
||||
// rust_2018_idioms,
|
||||
// unused_lifetimes,
|
||||
// unused_qualifications
|
||||
// )]
|
||||
|
||||
extern crate alloc;
|
||||
#[cfg(feature = "std")]
|
||||
extern crate std;
|
||||
|
||||
use alloc::string::{String, ToString};
|
||||
use core::fmt;
|
||||
use password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString};
|
||||
use rand_core::OsRng;
|
||||
|
||||
// #[cfg(not(any(feature = "argon2", feature = "pbkdf2", feature = "scrypt")))]
|
||||
// compile_error!(
|
||||
// "please enable at least one password hash crate feature, e.g. argon2, pbkdf2, scrypt"
|
||||
// );
|
||||
|
||||
use argon2::Argon2;
|
||||
|
||||
/// Opaque error type.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct VerifyError;
|
||||
|
||||
impl fmt::Display for VerifyError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("password verification error")
|
||||
}
|
||||
}
|
||||
|
||||
// #[cfg(feature = "std")]
|
||||
// #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
|
||||
// impl std::error::Error for VerifyError {}
|
||||
|
||||
/// Generate a password hash for the given password.
|
||||
pub fn generate_hash(password: impl AsRef<[u8]>) -> String {
|
||||
let salt = SaltString::generate(OsRng);
|
||||
generate_phc_hash(password.as_ref(), &salt)
|
||||
.map(|hash| hash.to_string())
|
||||
.expect("password hashing error")
|
||||
}
|
||||
|
||||
/// Generate a PHC hash using the preferred algorithm.
|
||||
#[allow(unreachable_code)]
|
||||
fn generate_phc_hash<'a>(
|
||||
password: &[u8],
|
||||
salt: &'a SaltString,
|
||||
) -> password_hash::Result<PasswordHash<'a>> {
|
||||
return Argon2::default().hash_password(password, salt);
|
||||
}
|
||||
|
||||
/// Verify the provided password against the provided password hash.
|
||||
pub fn verify_password(password: impl AsRef<[u8]>, hash: &str) -> Result<(), VerifyError> {
|
||||
let hash = PasswordHash::new(hash).map_err(|_| VerifyError)?;
|
||||
let algs: &[&dyn PasswordVerifier] = &[
|
||||
&Argon2::default(),
|
||||
];
|
||||
hash.verify_password(algs, password)
|
||||
.map_err(|_| VerifyError)
|
||||
}
|
||||
|
546
src/main.rs
Normal file
546
src/main.rs
Normal file
@ -0,0 +1,546 @@
|
||||
|
||||
//! Run
|
||||
//! ```not_rust
|
||||
//! cargo run -p example-static-file-server
|
||||
//! ```
|
||||
|
||||
// use axum_auth::AuthBasic;
|
||||
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
#![doc(html_logo_url = "../images/docserver.svg")]
|
||||
#[doc = include_str!("../README.md")]
|
||||
// #![doc(html_no_source)]
|
||||
|
||||
// TODO tasks https://docs.rs/async-sqlx-session/latest/async_sqlx_session/struct.SqliteSessionStore.html
|
||||
|
||||
use rand_core::{SeedableRng,OsRng, RngCore};
|
||||
use rand_chacha::ChaCha8Rng;
|
||||
|
||||
use axum::{
|
||||
extract::Host,
|
||||
handler::HandlerWithoutStateExt,
|
||||
routing::MethodRouter,
|
||||
http::{
|
||||
StatusCode,
|
||||
Uri,
|
||||
header::HeaderValue,
|
||||
Method,
|
||||
},
|
||||
BoxError,
|
||||
Extension,
|
||||
response::Redirect,
|
||||
// http::Request, handler::HandlerWithoutStateExt, http::StatusCode, routing::get, Router,
|
||||
Router,
|
||||
};
|
||||
use tower::ServiceBuilder;
|
||||
use axum_server::tls_rustls::RustlsConfig;
|
||||
use std::{
|
||||
net::SocketAddr,
|
||||
path::PathBuf,
|
||||
sync::{Arc, Mutex},
|
||||
path::Path,
|
||||
};
|
||||
// use std::net::SocketAddr;
|
||||
// use tower::ServiceExt;
|
||||
use tower_http::{
|
||||
services::{ServeDir,ServeFile},
|
||||
trace::TraceLayer,
|
||||
cors::CorsLayer,
|
||||
};
|
||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||
use once_cell::sync::Lazy;
|
||||
// use once_cell::sync::{Lazy,OnceCell};
|
||||
// use async_session::{Session, SessionStore, MemoryStore};
|
||||
|
||||
use tera::Context;
|
||||
use async_sqlx_session::SqliteSessionStore;
|
||||
use sqlx::AnyPool;
|
||||
|
||||
mod tera_tpls;
|
||||
mod defs;
|
||||
mod users;
|
||||
mod login_password;
|
||||
mod handlers;
|
||||
mod tools;
|
||||
|
||||
use defs::{
|
||||
AppDBs,
|
||||
SessionStoreDB,
|
||||
FileStore,
|
||||
Config,
|
||||
load_from_file,
|
||||
parse_args,
|
||||
AppConnectInfo,
|
||||
};
|
||||
use users::UserStore;
|
||||
|
||||
use tera_tpls::init_tera;
|
||||
use tower_cookies::CookieManagerLayer;
|
||||
use handlers::{
|
||||
handle_404,
|
||||
rewrite_request_uri,
|
||||
admin_router_handlers,
|
||||
users_router_handlers,
|
||||
pages_router_handlers,
|
||||
};
|
||||
use crate::tools::get_socket_addr;
|
||||
|
||||
pub const USER_AGENT: &str = "user-agent";
|
||||
pub const SESSION_COOKIE_NAME: &str = "doc_session";
|
||||
pub const CFG_FILE_EXTENSION: &str = ".toml";
|
||||
pub const FILE_SCHEME: &str = "file:///";
|
||||
|
||||
pub const PKG_NAME: &str = env!("CARGO_PKG_NAME");
|
||||
// static WEBSERVER: AtomicUsize = AtomicUsize::new(0);
|
||||
pub const PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
// const PKG_VERSION: Option&<&'static str> = option_env!("CARGO_PKG_VERSION");
|
||||
// const PKG_DESCRIPTION: &str = env!("CARGO_PKG_DESCRIPTION");
|
||||
// const COOKIE_NAME: &str = "lc_authz";
|
||||
// const COOKIE_SEP: &str = ":tk:";
|
||||
const GIT_VERSION: &str = ""; //git_version::git_version!();
|
||||
static GIT_VERSION_NAME: Lazy<String> = Lazy::new(|| {
|
||||
format!("v{} [build: {}]",PKG_VERSION,GIT_VERSION)
|
||||
});
|
||||
static PKG_FULLNAME: Lazy<String> = Lazy::new(|| {
|
||||
format!("{}: TII CL Rust",PKG_NAME)
|
||||
});
|
||||
|
||||
pub const USERS_TABLENAME: &str = "users";
|
||||
pub const USERS_FILESTORE: &str = "users";
|
||||
pub const DEFAULT_ROLES: &str = "user";
|
||||
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct Ports {
|
||||
http: u16,
|
||||
https: u16,
|
||||
}
|
||||
|
||||
pub fn route(path: &str, method_router: MethodRouter) -> Router {
|
||||
Router::new().route(path, method_router)
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let config_path = parse_args();
|
||||
if config_path.is_empty() {
|
||||
eprintln!("No config-file found");
|
||||
std::process::exit(2)
|
||||
}
|
||||
// pretty_env_logger::init();
|
||||
let config = {
|
||||
let mut server_cfg: Config = load_from_file(&config_path, "server-config").unwrap_or_else(|e|{
|
||||
eprintln!("Settings error: {}",e);
|
||||
std::process::exit(2)
|
||||
});
|
||||
server_cfg.load_items();
|
||||
//config.fix_root_path::<ServerConfig>(String::new());
|
||||
server_cfg
|
||||
};
|
||||
if config.verbose > 1 { dbg!("{:?}",&config); }
|
||||
tracing_subscriber::registry()
|
||||
.with(
|
||||
tracing_subscriber::EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| "example_static_file_server=debug,tower_http=debug".into()),
|
||||
)
|
||||
.with(tracing_subscriber::fmt::layer())
|
||||
.init();
|
||||
|
||||
// you can convert handler function to service
|
||||
|
||||
// let store = FileStore {
|
||||
// sess_path: "store".to_owned(),
|
||||
// ses_file: "data".to_owned(),
|
||||
// // sess_path: sessions_config.session_store_uri.replace(FILE_SCHEME,"").to_owned(),
|
||||
// // ses_file: sessions_config.session_store_file.to_owned(),
|
||||
// };
|
||||
// let session_store = SessionStoreDB::connect_file_store(store);
|
||||
// let session_store = SessionStoreDB::connect_memory_store();
|
||||
let session_store = if config.session_store_uri.starts_with(FILE_SCHEME) {
|
||||
let store = FileStore {
|
||||
sess_path: config.session_store_uri.replace(FILE_SCHEME,"").to_owned(),
|
||||
ses_file: config.session_store_file.to_owned(),
|
||||
};
|
||||
if let Err(e) = store.check_paths() {
|
||||
eprintln!("Error creation File Store: {}",e);
|
||||
std::process::exit(2)
|
||||
}
|
||||
SessionStoreDB::connect_file_store(store)
|
||||
} else if config.session_store_uri.starts_with("sqlite:") {
|
||||
let store = SqliteSessionStore::new(&config.session_store_uri).await.unwrap_or_else(|e|{
|
||||
eprintln!("Error session database {}: {}",
|
||||
config.session_store_uri,e
|
||||
);
|
||||
std::process::exit(2)
|
||||
});
|
||||
let _ = store.migrate().await;
|
||||
let _ = store.cleanup().await;
|
||||
SessionStoreDB::connect_sqlite_store(store)
|
||||
} else if config.session_store_uri.starts_with("memory") {
|
||||
SessionStoreDB::connect_memory_store()
|
||||
} else {
|
||||
SessionStoreDB::None
|
||||
};
|
||||
let user_store = if config.users_store_uri.starts_with(FILE_SCHEME) {
|
||||
let users_store_uri = config.users_store_uri.replace(FILE_SCHEME,"").to_owned();
|
||||
if ! Path::new(&users_store_uri).exists() {
|
||||
if let Err(e) = std::fs::File::create(Path::new(&users_store_uri)) {
|
||||
eprintln!("Error creation Users store {}: {}",
|
||||
&users_store_uri,e);
|
||||
std::process::exit(2)
|
||||
}
|
||||
}
|
||||
UserStore::File(users_store_uri)
|
||||
// } else if config.users_store_uri.starts_with("sqlite:") {
|
||||
} else if config.users_store_uri.contains("sql") {
|
||||
//let m = Migrator::new(Path::new("./migrations")).await?;
|
||||
//}
|
||||
let pool = AnyPool::connect(&config.users_store_uri).await.unwrap_or_else(|e|{
|
||||
eprintln!("Error pool database {}: {}",
|
||||
config.users_store_uri,e
|
||||
);
|
||||
std::process::exit(2)
|
||||
});
|
||||
// let pool = SqlitePool::connect(&config.users_store_uri).await.unwrap_or_else(|e|{
|
||||
// eprintln!("Error pool database {}: {}",
|
||||
// config.users_store_uri,e
|
||||
// );
|
||||
// std::process::exit(2)
|
||||
// });
|
||||
UserStore::Sql(pool)
|
||||
} else {
|
||||
eprintln!("User store {}: Not defined",&config.users_store_uri);
|
||||
std::process::exit(2)
|
||||
};
|
||||
#[cfg(feature = "casbin")]
|
||||
let enforcer = if !config.authz_model_path.is_empty() && ! config.authz_policy_path.is_empty() {
|
||||
AppDBs::create_enforcer(
|
||||
Box::leak(config.authz_model_path.to_owned().into_boxed_str()),
|
||||
Box::leak(config.authz_policy_path.to_owned().into_boxed_str())
|
||||
).await
|
||||
} else {
|
||||
eprintln!("Error auth enforcer {} + {}",
|
||||
config.authz_model_path, config.authz_policy_path
|
||||
);
|
||||
std::process::exit(2);
|
||||
};
|
||||
|
||||
let ports = Ports {
|
||||
http: 7878,
|
||||
https: 8800,
|
||||
};
|
||||
// optional: spawn a second server to redirect http requests to this server
|
||||
if config.protocol.contains("http") {
|
||||
tokio::spawn(redirect_http_to_https(ports));
|
||||
}
|
||||
|
||||
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 mut context = Context::new();
|
||||
context.insert("server_name","DOC Server");
|
||||
context.insert("pkg_name",&PKG_NAME);
|
||||
context.insert("pkg_version",&PKG_VERSION);
|
||||
context.insert("git_version",&GIT_VERSION);
|
||||
context.insert("git_version_name",GIT_VERSION_NAME.as_str());
|
||||
context.insert("pkg_fullname",PKG_FULLNAME.as_str());
|
||||
|
||||
// let app = Router::new().route("/", get(handler));
|
||||
#[cfg(feature = "authstore")]
|
||||
let app_dbs = Arc::new(
|
||||
AppDBs::new(&config, session_store, user_store,
|
||||
init_tera(&config.templates_path), context
|
||||
)
|
||||
);
|
||||
#[cfg(feature = "casbin")]
|
||||
let app_dbs = Arc::new(
|
||||
AppDBs::new(&config, session_store, user_store, enforcer,
|
||||
init_tera(&config.templates_path), context
|
||||
)
|
||||
);
|
||||
let middleware =
|
||||
axum::middleware::from_fn_with_state(app_dbs.clone(),rewrite_request_uri);
|
||||
// apply the layer around the whole `Router`
|
||||
// this way the middleware will run before `Router` receives the request
|
||||
|
||||
let mut web_router = Router::new();
|
||||
|
||||
// Parse serv_paths to add static paths as service
|
||||
for item in &config.serv_paths {
|
||||
// Try to check src_path ...
|
||||
let src_path: String;
|
||||
if Path::new(&item.src_path).exists() {
|
||||
src_path = format!("{}",item.src_path);
|
||||
} else {
|
||||
src_path = if item.src_path.starts_with("/") {
|
||||
format!("{}",item.src_path)
|
||||
} else {
|
||||
format!("{}/{}",&config.root_path,item.src_path)
|
||||
};
|
||||
if ! Path::new(&src_path).exists() {
|
||||
eprintln!("File path {}: not found", &src_path);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// Add ServeDir with not_found page ...
|
||||
if item.not_found.is_empty() {
|
||||
web_router = web_router.nest_service(
|
||||
&item.url_path,
|
||||
ServeDir::new(&src_path).not_found_service(handle_404.into_service())
|
||||
);
|
||||
println!("Added path {} => {}", &src_path,&item.url_path);
|
||||
} else {
|
||||
web_router = web_router.nest_service(
|
||||
&item.url_path,
|
||||
ServeDir::new(&src_path).not_found_service(ServeFile::new(&item.not_found))
|
||||
);
|
||||
println!("Added path {} => {} ({})", &src_path,&item.url_path,&item.not_found);
|
||||
}
|
||||
}
|
||||
let mut key = [0u8; 16];
|
||||
let mut os_rng = OsRng{};
|
||||
os_rng.fill_bytes(&mut key);
|
||||
let random = ChaCha8Rng::seed_from_u64(OsRng.next_u64());
|
||||
|
||||
web_router = web_router
|
||||
.merge(users_router_handlers())
|
||||
.merge(admin_router_handlers())
|
||||
.merge(pages_router_handlers())
|
||||
.layer(ServiceBuilder::new().layer(middleware))
|
||||
.layer(CookieManagerLayer::new())
|
||||
.layer(Extension(app_dbs))
|
||||
.layer(Extension(Arc::new(Mutex::new(random))))
|
||||
.fallback_service(handle_404.into_service())
|
||||
;
|
||||
|
||||
// if !config.html_path.is_empty() && !config.html_url.is_empty() {
|
||||
// MAIN_URL.set(config.server.html_url.to_owned()).unwrap_or_default();
|
||||
// println!("SpaRoutert local path {} to {}",&config.server.html_path,&config.server.html_url);
|
||||
// web_router = web_router.merge(
|
||||
// )
|
||||
// .fallback(fallback);
|
||||
// }
|
||||
|
||||
if config.verbose > 2 { dbg!("{:?}",&origins); }
|
||||
if config.allow_origin.len() > 0 {
|
||||
web_router = web_router.layer(CorsLayer::new()
|
||||
.allow_origin(origins)
|
||||
.allow_methods(vec![Method::GET, Method::POST])
|
||||
.allow_headers(tower_http::cors::Any)
|
||||
);
|
||||
}
|
||||
let addr = get_socket_addr(&config.bind,config.port);
|
||||
tracing::debug!("listening on {}", addr);
|
||||
println!("listening on {}", addr);
|
||||
if config.protocol.as_str() == "http" {
|
||||
// let app_with_middleware = middleware.layer(app);
|
||||
// run https server
|
||||
//let addr = SocketAddr::from(([127, 0, 0, 1], ports.http));
|
||||
//tracing::debug!("listening on {}", addr);
|
||||
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
||||
axum::serve(listener, web_router.layer(TraceLayer::new_for_http())
|
||||
.into_make_service_with_connect_info::<SocketAddr>()
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
} else {
|
||||
// configure certificate and private key used by https
|
||||
// let tls_config = RustlsConfig::from_pem_file(
|
||||
// PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
// .join("self_signed_certs")
|
||||
// .join("cert.pem"),
|
||||
// PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
// .join("self_signed_certs")
|
||||
// .join("key.pem"),
|
||||
// )
|
||||
let tls_config = RustlsConfig::from_pem_file(
|
||||
PathBuf::from(&config.cert_file),
|
||||
PathBuf::from(&config.key_file)
|
||||
)
|
||||
.await
|
||||
.unwrap_or_else(|e|{
|
||||
eprintln!("Error TLS config: {}",e);
|
||||
std::process::exit(2)
|
||||
});
|
||||
//let addr = SocketAddr::from(([127, 0, 0, 1], ports.https));
|
||||
//tracing::debug!("listening on {}", addr);
|
||||
//let app_with_middleware = middleware.layer(app);
|
||||
// apply the layer around the whole `Router`middleware.layer(app);
|
||||
// .serve(web_router.into_make_service())
|
||||
axum_server::bind_rustls(addr, tls_config)
|
||||
.serve(
|
||||
// web_router.layer(TraceLayer::new_for_http())
|
||||
web_router
|
||||
.into_make_service_with_connect_info::<AppConnectInfo>()
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
/*
|
||||
// let port = 3002;
|
||||
|
||||
// tokio::join!(
|
||||
// serve(using_serve_dir(), 3001),
|
||||
// serve(using_serve_dir_with_assets_fallback(), 3002),
|
||||
// serve(using_serve_dir_only_from_root_via_fallback(), 3003),
|
||||
// serve(using_serve_dir_with_handler_as_service(), 3004),
|
||||
// serve(two_serve_dirs(), 3005),
|
||||
// serve(calling_serve_dir_from_a_handler(), 3006),
|
||||
// );
|
||||
|
||||
//let addr = SocketAddr::from(([127, 0, 0, 1], port));
|
||||
let addr = SocketAddr::from(([192,168,1,4], port));
|
||||
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
||||
tracing::debug!("listening on {}", listener.local_addr().unwrap());
|
||||
|
||||
// let _ = axum::Server::bind(&addr)
|
||||
// .serve(
|
||||
// // router.layer(TraceLayer::new_for_http())
|
||||
// //router.into_make_service_with_connect_info::<SocketAddr>()
|
||||
// router.into_make_service()
|
||||
// )
|
||||
// .await
|
||||
// .expect("server failed");
|
||||
|
||||
// axum_server::bind(addr)
|
||||
// .serve(router.into_make_service())
|
||||
// .await
|
||||
// .unwrap();
|
||||
axum::serve(listener, router.layer(TraceLayer::new_for_http()))
|
||||
.await
|
||||
.unwrap();
|
||||
// let _ = axum::Server::bind(&addr)
|
||||
// .serve(
|
||||
// app.layer(TraceLayer::new_for_http()).into_make_service()
|
||||
// // web_router.into_make_service_with_connect_info::<SocketAddr>()
|
||||
// // web_router.into_make_service()
|
||||
// )
|
||||
// .await
|
||||
// .unwrap();
|
||||
*/
|
||||
}
|
||||
|
||||
async fn redirect_http_to_https(ports: Ports) {
|
||||
fn make_https(host: String, uri: Uri, ports: Ports) -> Result<Uri, BoxError> {
|
||||
let mut parts = uri.into_parts();
|
||||
|
||||
parts.scheme = Some(axum::http::uri::Scheme::HTTPS);
|
||||
|
||||
if parts.path_and_query.is_none() {
|
||||
parts.path_and_query = Some("/".parse().unwrap());
|
||||
}
|
||||
|
||||
let https_host = host.replace(&ports.http.to_string(), &ports.https.to_string());
|
||||
parts.authority = Some(https_host.parse()?);
|
||||
|
||||
Ok(Uri::from_parts(parts)?)
|
||||
}
|
||||
|
||||
let redirect = move |Host(host): Host, uri: Uri| async move {
|
||||
match make_https(host, uri, ports) {
|
||||
Ok(uri) => Ok(Redirect::permanent(&uri.to_string())),
|
||||
Err(error) => {
|
||||
tracing::warn!(%error, "failed to convert URI to HTTPS");
|
||||
Err(StatusCode::BAD_REQUEST)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let addr = SocketAddr::from(([127, 0, 0, 1], ports.http));
|
||||
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
||||
tracing::debug!("listening on {}", listener.local_addr().unwrap());
|
||||
axum::serve(listener, redirect.into_make_service_with_connect_info::<SocketAddr>())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/*
|
||||
fn using_serve_dir() -> Router {
|
||||
// serve the file in the "assets" directory under `/assets`
|
||||
Router::new().nest_service("/assets", ServeDir::new("assets"))
|
||||
}
|
||||
|
||||
fn using_serve_dir_with_assets_fallback() -> Router {
|
||||
// `ServeDir` allows setting a fallback if an asset is not found
|
||||
// so with this `GET /assets/doesnt-exist.jpg` will return `index.html`
|
||||
// rather than a 404
|
||||
let serve_dir = ServeDir::new("assets").not_found_service(ServeFile::new("assets/index.html"));
|
||||
|
||||
Router::new()
|
||||
.route("/foo", get(|| async { "Hi from /foo" }))
|
||||
.nest_service("/assets", serve_dir.clone())
|
||||
.fallback_service(serve_dir)
|
||||
}
|
||||
|
||||
fn using_serve_dir_only_from_root_via_fallback() -> Router {
|
||||
// you can also serve the assets directly from the root (not nested under `/assets`)
|
||||
// by only setting a `ServeDir` as the fallback
|
||||
let serve_dir = ServeDir::new("assets").not_found_service(ServeFile::new("assets/index.html"));
|
||||
|
||||
Router::new()
|
||||
.route("/foo", get(|| async { "Hi from /foo" }))
|
||||
.fallback_service(serve_dir)
|
||||
}
|
||||
|
||||
fn using_serve_dir_with_handler_as_service() -> Router {
|
||||
async fn handle_404() -> (StatusCode, &'static str) {
|
||||
(StatusCode::NOT_FOUND, "Not found")
|
||||
}
|
||||
|
||||
// you can convert handler function to service
|
||||
let service = handle_404.into_service();
|
||||
|
||||
let serve_dir = ServeDir::new("assets").not_found_service(service);
|
||||
|
||||
Router::new()
|
||||
.route("/foo", get(|| async { "Hi from /foo" }))
|
||||
.fallback_service(serve_dir)
|
||||
}
|
||||
|
||||
fn two_serve_dirs() -> Router {
|
||||
// you can also have two `ServeDir`s nested at different paths
|
||||
let serve_dir_from_assets = ServeDir::new("assets");
|
||||
let serve_dir_from_dist = ServeDir::new("dist");
|
||||
|
||||
Router::new()
|
||||
.nest_service("/assets", serve_dir_from_assets)
|
||||
.nest_service("/dist", serve_dir_from_dist)
|
||||
}
|
||||
|
||||
#[allow(clippy::let_and_return)]
|
||||
fn calling_serve_dir_from_a_handler() -> Router {
|
||||
// via `tower::Service::call`, or more conveniently `tower::ServiceExt::oneshot` you can
|
||||
// call `ServeDir` yourself from a handler
|
||||
Router::new().nest_service(
|
||||
"/foo",
|
||||
get(|request: Request<_>| async {
|
||||
let service = ServeDir::new("assets");
|
||||
let result = service.oneshot(request).await;
|
||||
result
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
async fn serve(app: Router, port: u16) {
|
||||
let addr = SocketAddr::from(([127, 0, 0, 1], port));
|
||||
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
||||
tracing::debug!("listening on {}", listener.local_addr().unwrap());
|
||||
axum::serve(listener, app.layer(TraceLayer::new_for_http()))
|
||||
.await
|
||||
.unwrap();
|
||||
// let _ = axum::Server::bind(&addr)
|
||||
// .serve(
|
||||
// app.layer(TraceLayer::new_for_http()).into_make_service()
|
||||
// // web_router.into_make_service_with_connect_info::<SocketAddr>()
|
||||
// // web_router.into_make_service()
|
||||
// )
|
||||
// .await
|
||||
// .unwrap();
|
||||
}
|
||||
*/
|
||||
|
26
src/tera_tpls.rs
Normal file
26
src/tera_tpls.rs
Normal file
@ -0,0 +1,26 @@
|
||||
use std::collections::HashMap;
|
||||
use tera::{Tera, try_get_value};
|
||||
use serde_json::value::{to_value, Value};
|
||||
|
||||
pub fn do_nothing_filter(value: &Value, _: &HashMap<String, Value>) -> tera::Result<Value> {
|
||||
let s = try_get_value!("do_nothing_filter", "value", String, value);
|
||||
Ok(to_value(&s).unwrap())
|
||||
}
|
||||
|
||||
pub fn init_tera(path: &str) -> Tera {
|
||||
let tpl_path = format!("{}/**/*",&path);
|
||||
let mut tera = match Tera::new(&tpl_path) {
|
||||
Ok(t) => {
|
||||
println!("Templates loaded from: {}",&tpl_path);
|
||||
log::info!("Templates loaded from: {}",&tpl_path);
|
||||
t
|
||||
},
|
||||
Err(e) => {
|
||||
println!("Tempates from {} parsing error(s): {}",&tpl_path, e);
|
||||
::std::process::exit(1);
|
||||
}
|
||||
};
|
||||
tera.autoescape_on(vec![]);
|
||||
tera.register_filter("do_nothing", do_nothing_filter);
|
||||
tera
|
||||
}
|
58
src/tools.rs
Normal file
58
src/tools.rs
Normal file
@ -0,0 +1,58 @@
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr, ToSocketAddrs};
|
||||
use chrono::{DateTime,Utc,NaiveDateTime};
|
||||
//use std::time::{UNIX_EPOCH, Duration};
|
||||
|
||||
pub fn get_socket_addr(bind: &str, port: u16) -> SocketAddr {
|
||||
let url = format!("{}:{}",&bind,&port);
|
||||
match url.to_socket_addrs() {
|
||||
Ok(addrs_op) => if let Some(addr) = addrs_op.to_owned().next() {
|
||||
addr
|
||||
} else {
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), port.to_owned())
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Evironment load error: {} {}", e, url);
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), port.to_owned())
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn generate_uuid(alphabet: String) -> String {
|
||||
if alphabet.is_empty() {
|
||||
return String::from("");
|
||||
}
|
||||
let mut code = String::new();
|
||||
for _ in 0..32 {
|
||||
// 16 is the length of the above Hex alphabet
|
||||
let number = rand::random::<f32>() * (16 as f32);
|
||||
let number = number.round() as usize;
|
||||
if let Some(character) = alphabet.chars().nth(number) {
|
||||
code.push(character)
|
||||
}
|
||||
}
|
||||
code.insert(20, '-');
|
||||
code.insert(16, '-');
|
||||
code.insert(12, '-');
|
||||
code.insert(8, '-');
|
||||
|
||||
code
|
||||
}
|
||||
#[allow(dead_code)]
|
||||
pub fn path_timestamp(filepath: &str) -> u32 {
|
||||
let arr_path = filepath.split("-").collect::<Vec<&str>>();
|
||||
if let Some(timestamp) = arr_path.last() {
|
||||
timestamp.split(".").collect::<Vec<&str>>()[0].parse().unwrap_or_default()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn str_date_from_timestamp(timestamp: &str) -> String {
|
||||
if timestamp.is_empty() { return String::from(""); }
|
||||
let val: i64 = timestamp.parse().unwrap_or_default();
|
||||
let dt = NaiveDateTime::from_timestamp_opt(val, 0).unwrap_or_default();
|
||||
let datetime = DateTime::<Utc>::from_utc(dt, Utc);
|
||||
// let val = u64::try_from(timestamp.to_owned()).unwrap_or_default();
|
||||
// let str_timestamp = UNIX_EPOCH + Duration::from_millis(val);
|
||||
// let datetime = DateTime::<Utc>::from(str_timestamp);
|
||||
datetime.format("%Y-%m-%d %H:%M:%S").to_string()
|
||||
}
|
17
src/users.rs
Normal file
17
src/users.rs
Normal file
@ -0,0 +1,17 @@
|
||||
mod entries;
|
||||
mod userdata;
|
||||
mod user;
|
||||
mod userstore;
|
||||
mod userstatus;
|
||||
mod user_role;
|
||||
|
||||
pub(crate) use user::User;
|
||||
pub(crate) use userstore::UserStore;
|
||||
pub(crate) use userstatus::UserStatus;
|
||||
pub(crate) use userdata::{
|
||||
UserData,
|
||||
UserLogin,
|
||||
UserItem,
|
||||
UserInvitation,
|
||||
};
|
||||
pub(crate) use user_role::UserRole;
|
263
src/users/NO/user_id.rs
Normal file
263
src/users/NO/user_id.rs
Normal file
@ -0,0 +1,263 @@
|
||||
use std::{
|
||||
io::Write,
|
||||
// sync::Arc,
|
||||
fmt::Debug,
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
io::{Error, ErrorKind},
|
||||
|
||||
};
|
||||
use log::error;
|
||||
// use async_session::{MemoryStore, Session, SessionStore};
|
||||
use serde::{Deserialize, Serialize};
|
||||
//use tiitls_utils::logs::file;
|
||||
use uuid::Uuid;
|
||||
use crate::defs::{
|
||||
FILE_SCHEME,
|
||||
SID_SETTINGS_FILE,
|
||||
SID_REQUESTS_FILE,
|
||||
SID_TRACE_FILE,
|
||||
// SID_UI_FILE,
|
||||
// UI_SETTINGS_FILE,
|
||||
// SidSettings,
|
||||
Config as SessionsConfig,
|
||||
TraceData,
|
||||
UserAction,
|
||||
};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
|
||||
pub struct UserId(Uuid);
|
||||
|
||||
impl UserId {
|
||||
#[allow(dead_code)]
|
||||
pub fn new() -> Self {
|
||||
Self(Uuid::new_v4())
|
||||
}
|
||||
pub fn from(data: &str) -> Option<Self> {
|
||||
match Uuid::parse_str(data) {
|
||||
Ok(uuid) => Some(Self(uuid)),
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for UserId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
impl UserId {
|
||||
|
||||
/// Log user actions to config.user_store_access|reqs_store_file/user_id/config.user_store_access path
|
||||
/// Add a line with timestamp
|
||||
/// It also get ReqHeaderMap to add more data ?
|
||||
#[allow(dead_code)]
|
||||
pub fn log_user_id_action(&self, action: UserAction, sessions_config: &SessionsConfig) -> std::io::Result<()> {
|
||||
let id_path = format!("{}/{}",
|
||||
sessions_config.users_store_uri.replace(FILE_SCHEME, ""),
|
||||
self);
|
||||
if ! Path::new(&id_path).exists() {
|
||||
fs::create_dir(&id_path)?;
|
||||
}
|
||||
let now = chrono::Utc::now().timestamp().to_string();
|
||||
let (log_data, file_path) = match action {
|
||||
UserAction::Access => (
|
||||
format!("{}\n",now),
|
||||
format!("{}/{}",&id_path,sessions_config.user_store_access)
|
||||
),
|
||||
UserAction::Log(info) => (
|
||||
format!("notity:{}:{}\n",now,info),
|
||||
format!("{}/{}",&id_path,sessions_config.user_store_access)
|
||||
),
|
||||
UserAction::Request(info) => (
|
||||
format!("req:{}:{}\n",now,info),
|
||||
format!("{}/{}",&id_path,sessions_config.user_store_access)
|
||||
),
|
||||
UserAction::View(info) => (
|
||||
format!("view:{}:{}\n",now,info),
|
||||
format!("{}/{}",&id_path,sessions_config.user_store_access)
|
||||
),
|
||||
UserAction::List(info) => (
|
||||
format!("list:{}:{}\n",now,info),
|
||||
format!("{}/{}",&id_path,sessions_config.user_store_access)
|
||||
),
|
||||
UserAction::Profile(info) => (
|
||||
format!("profile:{}:{}\n",now,info),
|
||||
format!("{}/{}",&id_path,sessions_config.user_store_access)
|
||||
),
|
||||
UserAction::Other => (
|
||||
format!("other:{}::\n",now),
|
||||
format!("{}/{}",&id_path,sessions_config.user_store_access)
|
||||
),
|
||||
};
|
||||
self.write_data(&file_path, &log_data, false)
|
||||
// if ! Path::new(&file_path).exists() {
|
||||
// fs::write(&file_path, log_data)?;
|
||||
// } else {
|
||||
// let access_id_file = fs::OpenOptions::new()
|
||||
// .write(true)
|
||||
// .append(true) // This is needed to append to file
|
||||
// .open(&file_path);
|
||||
// if let Ok(mut file) = access_id_file {
|
||||
// let _ = file.write_all(log_data.as_bytes())?;
|
||||
// }
|
||||
// }
|
||||
// Ok(())
|
||||
}
|
||||
fn id_path(&self, file: &str, sessions_config: &SessionsConfig) -> String {
|
||||
format!("{}/{}/{}",
|
||||
sessions_config.users_store_uri.replace(FILE_SCHEME, ""),
|
||||
self,file)
|
||||
}
|
||||
#[allow(dead_code)]
|
||||
fn write_data(&self, file_path: &str, data: &str, overwrite: bool) -> std::io::Result<()> {
|
||||
let check_path = |path: &Path| -> std::io::Result<()> {
|
||||
if ! Path::new(&path).exists() {
|
||||
if let Err(e) = std::fs::create_dir(&path) {
|
||||
return Err(Error::new( ErrorKind::InvalidInput,
|
||||
format!("Error create path {}: {}",&path.display(), e)
|
||||
));
|
||||
// std::process::exit(2)
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
if file_path.is_empty() || data.is_empty() {
|
||||
return Err(Error::new(
|
||||
ErrorKind::InvalidInput,
|
||||
format!("Error save {}",&file_path)
|
||||
));
|
||||
}
|
||||
if ! Path::new(&file_path).exists() {
|
||||
let path = PathBuf::from(&file_path);
|
||||
if let Some(dir_path) = path.parent() {
|
||||
if ! Path::new(&dir_path).exists() {
|
||||
if let Some(parent_dir_path) = dir_path.parent() {
|
||||
if ! Path::new(&parent_dir_path).exists() {
|
||||
let res = check_path(&parent_dir_path);
|
||||
if res.is_err() { return res; }
|
||||
}
|
||||
}
|
||||
let res = check_path(&dir_path);
|
||||
if res.is_err() { return res; }
|
||||
}
|
||||
}
|
||||
}
|
||||
if overwrite || ! Path::new(&file_path).exists() {
|
||||
fs::write(&file_path, data)?;
|
||||
println!("Overwrite: {}",&file_path);
|
||||
} else {
|
||||
let sid_settings_file = fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.append(true) // This is needed to append to file
|
||||
.open(&file_path);
|
||||
if let Ok(mut file) = sid_settings_file {
|
||||
file.write_all(data.as_bytes())?;
|
||||
}
|
||||
println!("write: {}",&file_path);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn sid_settings_path(&self, sessions_config: &SessionsConfig) -> String {
|
||||
self.id_path(SID_SETTINGS_FILE, sessions_config)
|
||||
}
|
||||
#[allow(dead_code)]
|
||||
pub fn sid_settings_content(&self, sessions_config: &SessionsConfig) -> String {
|
||||
let file_path = self.sid_settings_path(sessions_config);
|
||||
if ! Path::new(&file_path).exists() {
|
||||
String::from("")
|
||||
} else {
|
||||
match std::fs::read_to_string(&file_path) {
|
||||
Ok(content) => content,
|
||||
Err(e) => {
|
||||
error!("Error read {}: {}",&file_path,e);
|
||||
String::from("")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// pub fn sid_settings(&self, sessions_config: &SessionsConfig) -> SidSettings {
|
||||
// let file_path = self.id_path(SID_SETTINGS_FILE, sessions_config);
|
||||
// if Path::new(&file_path).exists() {
|
||||
// match std::fs::read_to_string(&file_path) {
|
||||
// Ok(content) => sessions_config.sid_settings.from_json(&content,&file_path),
|
||||
// Err(e) => {
|
||||
// error!("Error read {}: {}",&file_path,e);
|
||||
// sessions_config.sid_settings.to_owned()
|
||||
// }
|
||||
// }
|
||||
// } else {
|
||||
// //dbg!(&sessions_config.sid_settings);
|
||||
// sessions_config.sid_settings.to_owned()
|
||||
// }
|
||||
// }
|
||||
// pub fn save_sid_settings(&self, sid_settings: SidSettings, sessions_config: &SessionsConfig) -> std::io::Result<()> {
|
||||
// let file_path = self.id_path(SID_SETTINGS_FILE, sessions_config);
|
||||
// let data = sid_settings.to_json();
|
||||
// self.write_data(&file_path, &data, true)
|
||||
// }
|
||||
#[allow(dead_code)]
|
||||
pub fn save_trace_data(&self, trace_data: TraceData, sessions_config: &SessionsConfig) -> std::io::Result<()> {
|
||||
// let file_curenv_path = self.id_path(&format!("{}/{}",trace_data.server,SID_CURENV_FILE), sessions_config);
|
||||
// let res_curenv = self.write_data(&file_curenv_path, &trace_data.curenv, true);
|
||||
// let _ = res_curenv.as_ref().unwrap_or_else(|e|{
|
||||
// println!("Error save trace curenv: {}",&e);
|
||||
// &()
|
||||
// });
|
||||
// if res_curenv.is_err() {
|
||||
// return res_curenv;
|
||||
// }
|
||||
// if trace_data.ui.len() > 0 {
|
||||
// let file_ui_path = self.id_path(&format!("{}/{}",trace_data.server,SID_UI_FILE), sessions_config);
|
||||
// let res_ui = self.write_data(&file_ui_path, &trace_data.ui, true);
|
||||
// let _ = res_ui.as_ref().unwrap_or_else(|e|{
|
||||
// println!("Error save trace ui: {}",&e);
|
||||
// &()
|
||||
// });
|
||||
// if res_ui.is_err() { return res_ui; }
|
||||
// }
|
||||
let file_trace_path = self.id_path(&format!("{}/{}",trace_data.server,SID_TRACE_FILE), sessions_config);
|
||||
let contents = trace_data.contents_to_json();
|
||||
let mut result = Ok(());
|
||||
let lines = contents.len() - 1;
|
||||
for (idx, line) in contents.iter().enumerate() {
|
||||
let prfx = if idx == 0 && Path::new(&file_trace_path).exists() {
|
||||
",\n"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let sfx = if idx == lines {
|
||||
""
|
||||
} else {
|
||||
",\n"
|
||||
};
|
||||
result = self.write_data(
|
||||
&file_trace_path,
|
||||
format!("{}{}{}",&prfx,&line,&sfx).as_str(),
|
||||
false
|
||||
);
|
||||
let _ = result.as_ref().unwrap_or_else(|e|{
|
||||
println!("Error save trace contets: {} line {}",&e, &idx);
|
||||
&()
|
||||
});
|
||||
if result.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
#[allow(dead_code)]
|
||||
pub fn save_sid_request(&self, data: &str, sessions_config: &SessionsConfig) -> std::io::Result<()> {
|
||||
let file_path = self.id_path(SID_REQUESTS_FILE, sessions_config);
|
||||
println!("---- {}",&file_path);
|
||||
self.write_data(&file_path, &data, false)
|
||||
}
|
||||
#[allow(dead_code)]
|
||||
pub fn read_sid_requests(&self, sessions_config: &SessionsConfig) -> Result<Vec<String>, Box<dyn std::error::Error>> {
|
||||
let file_path = self.id_path(SID_REQUESTS_FILE, sessions_config);
|
||||
let data = fs::read_to_string(file_path)?;
|
||||
Ok(data.split("\n").map(|s| s.to_string()).collect())
|
||||
}
|
||||
}
|
67
src/users/NO/usernotifydata.rs
Normal file
67
src/users/NO/usernotifydata.rs
Normal file
@ -0,0 +1,67 @@
|
||||
use std::collections::HashMap;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::defs::Config;
|
||||
use pasetoken_lib::ConfigPaSeToken;
|
||||
|
||||
use crate::defs::{
|
||||
CLAIM_UID,
|
||||
CLAIM_AUTH,
|
||||
CLAIM_APP_KEY,
|
||||
};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct UserNotifyData {
|
||||
pub key: String,
|
||||
pub id: String,
|
||||
pub auth: String,
|
||||
}
|
||||
|
||||
impl UserNotifyData {
|
||||
pub fn data_claim(&self,_config: &Config) -> HashMap<String,String> {
|
||||
HashMap::from([
|
||||
(String::from(CLAIM_UID), self.id.to_owned()),
|
||||
(String::from(CLAIM_AUTH), self.auth.to_owned()),
|
||||
(String::from(CLAIM_APP_KEY), self.key.to_owned()),
|
||||
])
|
||||
}
|
||||
pub fn token(&self, server_config: &Config, paseto_config: &ConfigPaSeToken, expire: bool) -> String {
|
||||
paseto_config.generate_token("", &self.data_claim(server_config), expire).unwrap_or_else(|e|{
|
||||
eprintln!("Error generating token: {}", e);
|
||||
String::from("")
|
||||
})
|
||||
}
|
||||
pub fn from_token(token: &str,paseto_config: &ConfigPaSeToken) -> (String,Self) {
|
||||
match paseto_config.pasetoken() {
|
||||
Ok(paseto) => {
|
||||
match paseto.trusted(token, false) {
|
||||
Ok(trusted_token) => {
|
||||
// dbg!(&trusted_token.payload_claims());
|
||||
if let Some(claims) = trusted_token.payload_claims() {
|
||||
// dbg!(&claims);
|
||||
let uid = claims.get_claim(CLAIM_UID).unwrap_or(&json!("")).to_string().replace("\"","");
|
||||
(
|
||||
uid.to_owned(),
|
||||
Self {
|
||||
key: claims.get_claim(CLAIM_APP_KEY).unwrap_or(&json!("")).to_string().replace("\"",""),
|
||||
id: uid,
|
||||
auth: claims.get_claim(CLAIM_AUTH).unwrap_or(&json!("")).to_string().replace("\"",""),
|
||||
}
|
||||
)
|
||||
} else {
|
||||
(String::from(""), Self::default())
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
println!("Token not trusted: {}",e);
|
||||
(String::from(""), Self::default())
|
||||
},
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
println!("Error collecting notify data: {}",e);
|
||||
(String::from(""), Self::default())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
111
src/users/entries.rs
Normal file
111
src/users/entries.rs
Normal file
@ -0,0 +1,111 @@
|
||||
/// Generic `Iterator` over implementor's of
|
||||
/// [`Entry`](trait.Entry.html)'s.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// #### Iterate over /etc/passwd printing usernames
|
||||
///
|
||||
/// ```
|
||||
/// use std::path::Path;
|
||||
/// use pgs_files::passwd::PasswdEntry;
|
||||
/// use pgs_files::Entries;
|
||||
///
|
||||
/// for entry in Entries::<PasswdEntry>::new(&Path::new("/etc/passwd")) {
|
||||
/// println!("{}", entry.name);
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
use std::{
|
||||
io::{BufRead,BufReader,Write},
|
||||
fs::{OpenOptions,File},
|
||||
path::Path,
|
||||
marker::PhantomData,
|
||||
num::ParseIntError,
|
||||
};
|
||||
|
||||
pub struct Entries<T> {
|
||||
path: String,
|
||||
cursor: BufReader<File>,
|
||||
marker: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T> Entries<T> {
|
||||
pub fn new(file_path: &str) -> Entries<T> {
|
||||
let file = Path::new(&file_path);
|
||||
if ! file.exists() {
|
||||
File::create(file).unwrap_or_else(|e|{
|
||||
eprintln!("Error file: {}",e);
|
||||
std::process::exit(2)
|
||||
});
|
||||
}
|
||||
let reader = BufReader::new(File::open(file).ok().unwrap());
|
||||
Entries {
|
||||
path: file_path.to_owned(),
|
||||
cursor: reader,
|
||||
marker: PhantomData,
|
||||
}
|
||||
}
|
||||
pub fn append(&self, entry: String) -> anyhow::Result<()> {
|
||||
let file = Path::new(&self.path);
|
||||
if ! file.exists() {
|
||||
std::fs::write(file,format!("{}\n",entry).as_bytes())?;
|
||||
} else {
|
||||
let target_file = OpenOptions::new()
|
||||
.write(true)
|
||||
.append(true) // This is needed to append to file
|
||||
.open(&file);
|
||||
if let Ok(mut file) = target_file {
|
||||
file.write_all( format!("{}\n",entry).as_bytes())?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
pub fn write(&self, entries: &Vec<String>) -> anyhow::Result<()> {
|
||||
let file = Path::new(&self.path);
|
||||
std::fs::write(file,format!("{}\n",entries.clone().join("\n")).as_bytes())?;
|
||||
// let target_file = OpenOptions::new()
|
||||
// .write(true)
|
||||
// .open(&file);
|
||||
// if let Ok(mut file) = target_file {
|
||||
// file.write_all(entries.clone().join("\n").as_bytes())?;
|
||||
// }
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl<T: Entry> Iterator for Entries<T> {
|
||||
|
||||
type Item = T;
|
||||
|
||||
fn next(&mut self) -> Option<T> {
|
||||
let mut line = String::new();
|
||||
loop {
|
||||
// We might need to make multiple loops to drain off
|
||||
// comment lines. Start with an empty string per loop.
|
||||
line.clear();
|
||||
match self.cursor.read_line(&mut line){
|
||||
Ok(0) => return None,
|
||||
Ok(_) => (),
|
||||
_ => return None,
|
||||
}
|
||||
|
||||
if line.starts_with("#") {
|
||||
continue;
|
||||
}
|
||||
|
||||
match T::from_line(&line) {
|
||||
Ok(entry) => return Some(entry),
|
||||
// Parse Error. Just ignore this entry.
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// A Trait to represent an entry of data from an
|
||||
/// /etc/{`passwd`,`group`,`shadow`} file.
|
||||
pub trait Entry: Sized {
|
||||
fn from_line(line: &str) -> anyhow::Result<Self, ParseIntError>;
|
||||
}
|
631
src/users/user.rs
Normal file
631
src/users/user.rs
Normal file
@ -0,0 +1,631 @@
|
||||
use serde::{Deserialize,Serialize};
|
||||
use sqlx::Row;
|
||||
use futures::TryStreamExt;
|
||||
use anyhow::{anyhow,Context, Result};
|
||||
use std::{
|
||||
fmt,
|
||||
collections::HashMap,
|
||||
};
|
||||
// use std::{
|
||||
// // sync::Arc,
|
||||
// fmt::Debug,
|
||||
// // io::Write,
|
||||
// // fs,
|
||||
// // path::{Path, PathBuf},
|
||||
// // io::{Error, ErrorKind},
|
||||
// collections::HashMap,
|
||||
// };
|
||||
// use async_session::{MemoryStore, Session, SessionStore};
|
||||
//use tiitls_utils::logs::file;
|
||||
// use uuid::Uuid;
|
||||
// SID_UI_FILE,
|
||||
// UI_SETTINGS_FILE,
|
||||
// SidSettings,
|
||||
// Config as SessionsConfig,
|
||||
use std::num::ParseIntError;
|
||||
use crate::{
|
||||
users::{
|
||||
entries::{Entries,Entry},
|
||||
UserData,
|
||||
UserStore,
|
||||
UserStatus,
|
||||
},
|
||||
USERS_TABLENAME,
|
||||
tools::str_date_from_timestamp,
|
||||
};
|
||||
|
||||
const DISPLAY_SEPARATOR: &str = "=";
|
||||
|
||||
fn default_user_status() -> UserStatus {
|
||||
UserStatus::default()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, PartialOrd, Serialize, Deserialize,Default)]
|
||||
pub struct User {
|
||||
pub id: i64,
|
||||
pub name: String,
|
||||
pub fullname: String,
|
||||
pub email: String,
|
||||
pub description: String,
|
||||
pub password: String,
|
||||
pub otp_enabled: bool,
|
||||
pub otp_verified: bool,
|
||||
pub otp_base32: String,
|
||||
pub otp_auth_url: String,
|
||||
pub otp_defs: String,
|
||||
pub roles: String,
|
||||
pub created: String,
|
||||
pub lastaccess: String,
|
||||
#[serde(default = "default_user_status")]
|
||||
pub status: UserStatus,
|
||||
pub items: String,
|
||||
pub isadmin: bool,
|
||||
}
|
||||
|
||||
impl fmt::Display for User {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
// Write strictly the first element into the supplied output
|
||||
// stream: `f`. Returns `fmt::Result` which indicates whether the
|
||||
// operation succeeded or failed. Note that `write!` uses syntax which
|
||||
// is very similar to `println!`.
|
||||
let sep = DISPLAY_SEPARATOR;
|
||||
let content = format!(
|
||||
"ID{} {}
|
||||
Name{} {}
|
||||
FullName{} {}
|
||||
Description{} {}
|
||||
Email{} {}
|
||||
Password{} {}
|
||||
otp_enabled{} {}
|
||||
otp_verified{} {}
|
||||
otp_base32{} {}
|
||||
otp_auth_url{} {}
|
||||
otp_defs{} {}
|
||||
Roles{} {}
|
||||
Created{} {}
|
||||
Last access{} {}
|
||||
Status{} {}
|
||||
Items{} {}
|
||||
IsAdmin{} {}",
|
||||
sep, self.id,
|
||||
sep, self.name, sep, self.fullname,
|
||||
sep, self.description,
|
||||
sep, self.email,
|
||||
sep, self.password,
|
||||
sep, self.otp_enabled,
|
||||
sep, self.otp_verified,
|
||||
sep, self.otp_base32,
|
||||
sep, self.otp_auth_url,
|
||||
sep, self.otp_defs,
|
||||
sep, self.roles,
|
||||
sep, self.created, sep, self.lastaccess,
|
||||
sep, self.status,
|
||||
sep, self.items,
|
||||
sep, self.isadmin
|
||||
);
|
||||
write!(f, "{}", content)
|
||||
}
|
||||
}
|
||||
|
||||
impl Entry for User {
|
||||
fn from_line(line: &str) -> Result<User, ParseIntError> {
|
||||
let parts: Vec<&str> = line.split(":").map(|part| part.trim()).collect();
|
||||
Ok(User {
|
||||
id: parts[0].to_string().parse::<i64>().unwrap_or_default(),
|
||||
name: parts[1].to_string(),
|
||||
fullname: parts[2].to_string(),
|
||||
description: parts[3].to_string(),
|
||||
email: parts[4].to_string(),
|
||||
password: parts[5].to_string(),
|
||||
otp_enabled: if parts[6] == "TRUE" { true } else { false},
|
||||
otp_verified: if parts[7] == "TRUE" { true } else { false},
|
||||
otp_base32: parts[8].to_string(),
|
||||
otp_auth_url: parts[9].to_string(),
|
||||
otp_defs: parts[10].to_string(),
|
||||
roles: parts[11].to_string(),
|
||||
created: parts[12].to_string(),
|
||||
lastaccess: parts[13].to_string(),
|
||||
status: UserStatus::from_str(&parts[14].to_string()),
|
||||
items: parts[15].to_string(),
|
||||
isadmin: if parts[16] == "TRUE" { true } else { false},
|
||||
})
|
||||
}
|
||||
}
|
||||
impl User {
|
||||
pub async fn add(self, store: &UserStore) -> Result<i64> {
|
||||
|
||||
match store {
|
||||
UserStore::Sql(pool) => {
|
||||
let query_result = sqlx::query(
|
||||
format!("INSERT INTO {} (
|
||||
name, fullname, email, description, password, otp_enabled, otp_verified,
|
||||
otp_base32,
|
||||
otp_auth_url,
|
||||
otp_defs,
|
||||
roles, created, lastaccess, status, items, isadmin
|
||||
) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", USERS_TABLENAME).as_str()
|
||||
)
|
||||
.bind(self.name)
|
||||
.bind(self.fullname)
|
||||
.bind(self.email)
|
||||
.bind(self.description)
|
||||
.bind(self.password)
|
||||
.bind(self.otp_enabled)
|
||||
.bind(self.otp_verified)
|
||||
.bind(self.otp_base32)
|
||||
.bind(self.otp_auth_url)
|
||||
.bind(self.otp_defs)
|
||||
.bind(self.roles)
|
||||
.bind(self.created)
|
||||
.bind(self.lastaccess)
|
||||
.bind(format!("{}",self.status))
|
||||
.bind(self.items)
|
||||
.bind(self.isadmin)
|
||||
.execute(pool).await?;
|
||||
Ok(query_result.last_insert_id().unwrap_or_default())
|
||||
},
|
||||
UserStore::File(file_path) => {
|
||||
// use itertools::Itertools;
|
||||
// let entries: Vec<String> = concat(vec![
|
||||
// Entries::new(Path::new(&file_path)).map(|user: User|{
|
||||
// user.line_format()
|
||||
// }).collect(),
|
||||
// vec![ self.line_format()]
|
||||
// ]);
|
||||
// Entries::<User>::write(Path::new(&file_path), &entries);
|
||||
let all: Vec<User> = Entries::new(&file_path).collect();
|
||||
let id = if all.len() > 0 {
|
||||
all[all.len()-1].id + 1
|
||||
} else {
|
||||
1
|
||||
};
|
||||
let mut new_user = self.to_owned();
|
||||
new_user.id = id;
|
||||
let entries: Entries<User> = Entries::new(&file_path);
|
||||
match entries.append(new_user.line_format()) {
|
||||
Ok(_) => Ok(id),
|
||||
Err(e) => {
|
||||
println!("Error add item to file: {}",e);
|
||||
Err(anyhow!("No data added")).context("User add")
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
pub async fn select(field: &str, value: &str, human: bool, store: &UserStore) -> Result<Self> {
|
||||
match store {
|
||||
UserStore::Sql(pool) => {
|
||||
let query_str = format!("SELECT * FROM {} WHERE {} = ? ", USERS_TABLENAME, field);
|
||||
let mut stream = sqlx::query(
|
||||
&query_str
|
||||
)
|
||||
.bind(value)
|
||||
//.map(|row: PgRow| {
|
||||
// map the row into a user-defined domain type
|
||||
//})
|
||||
.fetch(pool);
|
||||
if let Some(row) = stream.try_next().await? {
|
||||
let str_status: String = row.try_get("status")?;
|
||||
let status = UserStatus::from_str(&str_status);
|
||||
let created = if human {
|
||||
let created: String = row.try_get("created")?;
|
||||
str_date_from_timestamp(&created)
|
||||
} else {
|
||||
row.try_get("created")?
|
||||
};
|
||||
let lastaccess = if human {
|
||||
let lastaccess: String = row.try_get("lastaccess")?;
|
||||
str_date_from_timestamp(&lastaccess)
|
||||
} else {
|
||||
row.try_get("lastaccess")?
|
||||
};
|
||||
Ok(Self{
|
||||
id: row.try_get("id")?,
|
||||
name: row.try_get("name")?,
|
||||
fullname: row.try_get("fullname")?,
|
||||
email: row.try_get("email")?,
|
||||
description: row.try_get("description")?,
|
||||
password: row.try_get("password")?,
|
||||
otp_enabled: row.try_get("otp_enabled")?,
|
||||
otp_verified: row.try_get("otp_verified")?,
|
||||
otp_base32: row.try_get("otp_base32")?,
|
||||
otp_auth_url: row.try_get("otp_auth_url")?,
|
||||
otp_defs: row.try_get("otp_defs")?,
|
||||
roles: row.try_get("roles")?,
|
||||
created,
|
||||
lastaccess,
|
||||
status,
|
||||
items: row.try_get("items")?,
|
||||
isadmin: row.try_get("isadmin")?,
|
||||
})
|
||||
} else {
|
||||
Err(anyhow!("No data found")).context("User select")
|
||||
}
|
||||
},
|
||||
UserStore::File(file_path) => {
|
||||
if let Some(user) =
|
||||
Entries::<User>::new(&file_path).find(|it|
|
||||
match field {
|
||||
"id" => it.id == value.parse::<i64>().unwrap_or_default(),
|
||||
"name" => it.name == value,
|
||||
"fullname" => it.fullname == value,
|
||||
"email" => it.email == value,
|
||||
"description" => it.description == value,
|
||||
"otp_base32" => it.otp_base32 == value,
|
||||
"roles" => it.roles == value,
|
||||
"items" => it.items == value,
|
||||
"isadmin" => match value {
|
||||
"TRUE" => it.isadmin,
|
||||
_ => !it.isadmin,
|
||||
},
|
||||
_ => false,
|
||||
}
|
||||
)
|
||||
{
|
||||
Ok(user)
|
||||
} else {
|
||||
Err(anyhow!("No data found")).context("User select")
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete(id: i64, store: &UserStore) -> Result<bool> {
|
||||
match store {
|
||||
UserStore::Sql(pool) => {
|
||||
let query_str = format!("DELETE FROM {} WHERE id = ?", USERS_TABLENAME);
|
||||
let query_result = sqlx::query(
|
||||
&query_str
|
||||
)
|
||||
.bind(id)
|
||||
.execute(pool).await?;
|
||||
Ok(query_result.rows_affected() > 0)
|
||||
},
|
||||
UserStore::File(file_path) => {
|
||||
let new_entries: Vec<String> =
|
||||
Entries::<User>::new(&file_path).filter(|it| it.id != id).map(|user|
|
||||
user.line_format()
|
||||
).collect();
|
||||
let entries = Entries::<User>::new(&file_path);
|
||||
match entries.write(&new_entries) {
|
||||
Ok(_) => Ok(true),
|
||||
Err(e) => {
|
||||
println!("Error data delete '{}': {}", id, e);
|
||||
Err(anyhow!("No data delete").context("User delete"))
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
pub async fn update(self, store: &UserStore) -> anyhow::Result<bool> {
|
||||
match store {
|
||||
UserStore::Sql(pool) => {
|
||||
let query_str = format!("UPDATE {} SET name = ?, fullname = ?,
|
||||
email = ?, description = ?, password = ?,
|
||||
otp_enabled = ?, otp_verified = ?,
|
||||
otp_base32 = ?, otp_auth_url = ?,
|
||||
otp_defs = ?,
|
||||
roles = ?,
|
||||
created = ?, lastaccess = ?,
|
||||
status = ?,
|
||||
items = ?,
|
||||
isadmin = ?
|
||||
WHERE id = ? ",
|
||||
USERS_TABLENAME
|
||||
);
|
||||
let query_result = sqlx::query(
|
||||
&query_str
|
||||
)
|
||||
.bind(self.name)
|
||||
.bind(self.fullname)
|
||||
.bind(self.email)
|
||||
.bind(self.description)
|
||||
.bind(self.password)
|
||||
.bind(self.otp_enabled)
|
||||
.bind(self.otp_verified)
|
||||
.bind(self.otp_base32)
|
||||
.bind(self.otp_auth_url)
|
||||
.bind(self.otp_defs)
|
||||
.bind(self.roles)
|
||||
.bind(self.created)
|
||||
.bind(self.lastaccess)
|
||||
.bind(format!("{}",self.status))
|
||||
.bind(self.items)
|
||||
.bind(self.isadmin)
|
||||
.bind(self.id)
|
||||
.execute(pool).await?;
|
||||
Ok(query_result.rows_affected() > 0)
|
||||
},
|
||||
UserStore::File(file_path) => {
|
||||
let new_entries: Vec<String> = Entries::new(&file_path).map(|user: User|{
|
||||
if user.id == self.id {
|
||||
self.to_owned().line_format()
|
||||
} else {
|
||||
user.line_format()
|
||||
}
|
||||
}).collect();
|
||||
let entries = Entries::<User>::new(&file_path);
|
||||
match entries.write(&new_entries) {
|
||||
Ok(_) => Ok(true),
|
||||
Err(e) => {
|
||||
println!("Error data update '{}': {}", self.id, e);
|
||||
Err(anyhow!("No data updated").context("User update"))
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
pub fn show(&self, sep: &str) {
|
||||
let content = if sep.is_empty() {
|
||||
format!( "{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}",
|
||||
self.id,
|
||||
self.name, self.fullname,
|
||||
self.description,
|
||||
self.email,
|
||||
self.password,
|
||||
self.otp_enabled,
|
||||
self.otp_verified,
|
||||
self.otp_base32,
|
||||
self.otp_auth_url,
|
||||
self.otp_defs,
|
||||
self.roles,
|
||||
self.created,
|
||||
self.lastaccess,
|
||||
self.status,
|
||||
self.items,
|
||||
self.isadmin
|
||||
)
|
||||
} else {
|
||||
format!("{}",&self).replace(DISPLAY_SEPARATOR, sep)
|
||||
};
|
||||
println!("{}\n_____________________________",content);
|
||||
}
|
||||
pub async fn list(store: &UserStore, human: bool, show: bool, sep: &str) -> anyhow::Result<Vec<User>> {
|
||||
let mut usrs: Vec<User> = Vec::new();
|
||||
match store {
|
||||
UserStore::Sql(pool) => {
|
||||
let query_str = format!("SELECT * FROM {}", USERS_TABLENAME);
|
||||
let mut stream = sqlx::query(
|
||||
&query_str
|
||||
)
|
||||
//.map(|row: PgRow| {
|
||||
// map the row into a user-defined domain type
|
||||
//})
|
||||
.fetch(pool);
|
||||
while let Some(row) = stream.try_next().await? {
|
||||
let str_status: String = row.try_get("status")?;
|
||||
let status = UserStatus::from_str(&str_status);
|
||||
let created = if human {
|
||||
let created: String = row.try_get("created")?;
|
||||
str_date_from_timestamp(&created)
|
||||
} else {
|
||||
row.try_get("created")?
|
||||
};
|
||||
let lastaccess = if human {
|
||||
let lastaccess: String = row.try_get("lastaccess")?;
|
||||
str_date_from_timestamp(&lastaccess)
|
||||
} else {
|
||||
row.try_get("lastaccess")?
|
||||
};
|
||||
let user = Self{
|
||||
id: row.try_get("id")?,
|
||||
name: row.try_get("name")?,
|
||||
fullname: row.try_get("fullname")?,
|
||||
email: row.try_get("email")?,
|
||||
description: row.try_get("description")?,
|
||||
password: row.try_get("password")?,
|
||||
otp_enabled: row.try_get("otp_enabled")?,
|
||||
otp_verified: row.try_get("otp_verified")?,
|
||||
otp_base32: row.try_get("otp_base32")?,
|
||||
otp_auth_url: row.try_get("otp_auth_url")?,
|
||||
otp_defs: row.try_get("otp_defs")?,
|
||||
roles: row.try_get("roles")?,
|
||||
created,
|
||||
lastaccess,
|
||||
status,
|
||||
items: row.try_get("items")?,
|
||||
isadmin: row.try_get("isadmin")?,
|
||||
};
|
||||
if show { user.show(sep); }
|
||||
usrs.push(user);
|
||||
}
|
||||
Ok(usrs)
|
||||
},
|
||||
UserStore::File(file_path) => {
|
||||
let all: Vec<User> = Entries::new(&file_path).collect();
|
||||
if show {
|
||||
for user in all.to_owned() { if show { user.show(sep); } }
|
||||
}
|
||||
Ok(all)
|
||||
},
|
||||
// UserStore::None => Err(anyhow!("No store set")).context("Users list"),
|
||||
}
|
||||
}
|
||||
pub async fn count(store: &UserStore) -> anyhow::Result<i64> {
|
||||
match store {
|
||||
UserStore::Sql(pool) => {
|
||||
|
||||
let query_str = format!("SELECT count(*) as total FROM {}", USERS_TABLENAME);
|
||||
let row = sqlx::query(
|
||||
&query_str
|
||||
)
|
||||
.fetch_one(pool).await?;
|
||||
|
||||
let total: i64 = row.try_get("total")?;
|
||||
Ok(total)
|
||||
},
|
||||
UserStore::File(file_path) => {
|
||||
let all: Vec<User> = Entries::new(&file_path).collect();
|
||||
Ok(all.len() as i64)
|
||||
},
|
||||
}
|
||||
}
|
||||
fn line_format(self) -> String {
|
||||
format!( "{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}",
|
||||
self.id,
|
||||
self.name, self.fullname,
|
||||
self.description,
|
||||
self.email,
|
||||
self.password,
|
||||
self.otp_enabled,
|
||||
self.otp_verified,
|
||||
self.otp_base32,
|
||||
self.otp_auth_url,
|
||||
self.otp_defs,
|
||||
self.roles,
|
||||
self.created, self.lastaccess,
|
||||
self.status,
|
||||
self.items,
|
||||
self.isadmin
|
||||
)
|
||||
}
|
||||
pub fn hash_items(items: &str) -> HashMap<String,String> {
|
||||
if items.is_empty() {
|
||||
HashMap::new()
|
||||
} else {
|
||||
serde_json::from_str(items).unwrap_or_else(|e|{
|
||||
println!("Error to convert user items to json: {}",e);
|
||||
HashMap::new()
|
||||
})
|
||||
}
|
||||
}
|
||||
pub fn items(&self) -> HashMap<String,String> {
|
||||
Self::hash_items(&self.items)
|
||||
}
|
||||
pub fn json_items(items: HashMap<String,String>) -> String {
|
||||
serde_json::to_string(&items).unwrap_or_else(|e|{
|
||||
println!("Error to convert user items to string: {}",e);
|
||||
String::from("")
|
||||
})
|
||||
}
|
||||
pub fn from_data(&mut self, user_data: UserData) {
|
||||
if !user_data.name.is_empty() {
|
||||
self.name = user_data.name.to_owned();
|
||||
}
|
||||
if !user_data.fullname.is_empty() {
|
||||
self.fullname = user_data.fullname.to_owned();
|
||||
}
|
||||
if !user_data.description.is_empty() {
|
||||
self.description = user_data.description.to_owned();
|
||||
}
|
||||
if !user_data.email.is_empty() {
|
||||
self.email = user_data.email.to_owned();
|
||||
}
|
||||
if !user_data.otp_code.is_empty() {
|
||||
self.otp_base32 = user_data.otp_code.to_owned();
|
||||
}
|
||||
if !user_data.otp_url.is_empty() {
|
||||
self.otp_auth_url = user_data.otp_url.to_owned();
|
||||
}
|
||||
if !user_data.roles.is_empty() {
|
||||
self.roles = user_data.roles.to_owned();
|
||||
}
|
||||
if !user_data.items.is_empty() {
|
||||
let mut items_hash = self.items();
|
||||
for (key,val) in user_data.items {
|
||||
items_hash.insert(key,val);
|
||||
}
|
||||
self.items = Self::json_items(items_hash);
|
||||
}
|
||||
}
|
||||
pub fn disable_totp(&mut self) {
|
||||
self.otp_base32 = String::from("");
|
||||
self.otp_auth_url = String::from("");
|
||||
self.otp_defs = String::from("");
|
||||
self.otp_verified = false;
|
||||
self.otp_enabled = false;
|
||||
}
|
||||
pub fn from_user(&mut self, new_user: User) {
|
||||
if !new_user.name.is_empty() {
|
||||
self.name = new_user.name.to_owned();
|
||||
}
|
||||
if !new_user.fullname.is_empty() {
|
||||
self.fullname = new_user.fullname.to_owned();
|
||||
}
|
||||
if !new_user.description.is_empty() {
|
||||
self.description = new_user.description.to_owned();
|
||||
}
|
||||
if !new_user.email.is_empty() {
|
||||
self.email = new_user.email.to_owned();
|
||||
}
|
||||
if !new_user.password.is_empty() {
|
||||
self.password = new_user.password.to_owned();
|
||||
}
|
||||
if new_user.otp_enabled {
|
||||
self.disable_totp();
|
||||
} else {
|
||||
if !new_user.otp_base32.is_empty() {
|
||||
self.otp_base32 = new_user.otp_base32.to_owned();
|
||||
}
|
||||
if !new_user.otp_auth_url.is_empty() {
|
||||
self.otp_auth_url = new_user.otp_auth_url.to_owned();
|
||||
}
|
||||
if !new_user.otp_defs.is_empty() {
|
||||
self.otp_defs = new_user.otp_defs.to_owned();
|
||||
}
|
||||
self.otp_verified = new_user.otp_verified;
|
||||
}
|
||||
if !new_user.roles.is_empty() {
|
||||
self.roles = new_user.roles.to_owned();
|
||||
}
|
||||
if !new_user.items.is_empty() {
|
||||
self.items = new_user.items.to_owned();
|
||||
}
|
||||
if new_user.status != self.status {
|
||||
self.status = new_user.status.to_owned();
|
||||
}
|
||||
}
|
||||
pub fn session_data(&self) -> String {
|
||||
format!("{}|{}|{}|{}|{}|{}|{}",
|
||||
&self.id,
|
||||
&self.name,
|
||||
&self.email,
|
||||
&self.roles,
|
||||
&self.items,
|
||||
&self.isadmin,
|
||||
&self.status
|
||||
)
|
||||
}
|
||||
pub fn estimate_password(word: &str) -> String {
|
||||
match zxcvbn::zxcvbn(word, &[]) {
|
||||
Ok(estimate) => {
|
||||
if let Some(feedback) = estimate.feedback() {
|
||||
let arr_suggestions: Vec<String> = feedback.suggestions().iter().map(|s| format!("{}",s)).collect();
|
||||
let suggestions = arr_suggestions.join("\n");
|
||||
if let Some(warning) = feedback.warning() {
|
||||
let warning = format!("{}", warning);
|
||||
format!("{}|{}|{}",estimate.score(),suggestions,warning)
|
||||
} else {
|
||||
format!("{}|{}|",estimate.score(),suggestions)
|
||||
}
|
||||
} else {
|
||||
format!("{}||",estimate.score())
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
println!("Error password strength estimator: {}", e);
|
||||
String::from("-1|||")
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn password_score(word: &str) -> u8 {
|
||||
match zxcvbn::zxcvbn(word, &[]) {
|
||||
Ok(estimate) => {
|
||||
estimate.score()
|
||||
},
|
||||
Err(e) => {
|
||||
println!("Error password strength estimator: {}", e);
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
#[allow(dead_code)]
|
||||
pub fn has_auth_role(&self, auth_roles: Vec<String>) -> bool {
|
||||
for role in auth_roles {
|
||||
if self.roles.contains(&role) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
58
src/users/user_action.rs
Normal file
58
src/users/user_action.rs
Normal file
@ -0,0 +1,58 @@
|
||||
|
||||
// use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum UserAction {
|
||||
Access,
|
||||
Log(String),
|
||||
Request(String),
|
||||
View(String),
|
||||
List(String),
|
||||
Profile(String),
|
||||
Other,
|
||||
}
|
||||
impl Default for UserAction {
|
||||
fn default() -> Self {
|
||||
UserAction::Other
|
||||
}
|
||||
}
|
||||
impl std::fmt::Display for UserAction {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
UserAction::Access => write!(f,"access"),
|
||||
UserAction::Log(info) =>write!(f,"log: {}", info),
|
||||
UserAction::Request(info) =>write!(f,"request: {}", info),
|
||||
UserAction::View(info) => write!(f,"view: {}", info),
|
||||
UserAction::List(info) => write!(f,"list: {}", info),
|
||||
UserAction::Profile( info) => write!(f,"profile: {}", info),
|
||||
UserAction::Other => write!(f,"other"),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl UserAction {
|
||||
#[allow(dead_code)]
|
||||
pub fn from_str(value: &str, info: String) -> UserAction {
|
||||
match value {
|
||||
"access" | "Access" => UserAction::Access,
|
||||
"log" | "Log" => UserAction::Log(info),
|
||||
"request" | "Request" => UserAction::Request(info),
|
||||
"view" | "View" => UserAction::View(info),
|
||||
"list" | "List" => UserAction::List(info),
|
||||
"profile" | "Profile " => UserAction::Profile(info),
|
||||
"other" | "Other " => UserAction::Other,
|
||||
_ => UserAction::default(),
|
||||
}
|
||||
}
|
||||
#[allow(dead_code)]
|
||||
pub fn info(&self) -> String {
|
||||
match self {
|
||||
UserAction::Access => String::from(""),
|
||||
UserAction::Log(info) => info.to_owned(),
|
||||
UserAction::Request(info) => info.to_owned(),
|
||||
UserAction::View(info) => info.to_owned(),
|
||||
UserAction::List(info) => info.to_owned(),
|
||||
UserAction::Profile(info) => info.to_owned(),
|
||||
UserAction::Other => String::from(""),
|
||||
}
|
||||
}
|
||||
}
|
60
src/users/user_role.rs
Normal file
60
src/users/user_role.rs
Normal file
@ -0,0 +1,60 @@
|
||||
use serde::{Deserialize,Serialize,Deserializer};
|
||||
|
||||
// #[derive(Error, Debug)]
|
||||
// pub enum AuthError {
|
||||
// #[error("error")]
|
||||
// SomeError(),
|
||||
// #[error("no authorization header found")]
|
||||
// NoAuthHeaderFoundError,
|
||||
// #[error("wrong authorization header format")]
|
||||
// InvalidAuthHeaderFormatError,
|
||||
// #[error("no user found for this token")]
|
||||
// InvalidTokenError,
|
||||
// #[error("error during authorization")]
|
||||
// AuthorizationError,
|
||||
// #[error("user is not unauthorized")]
|
||||
// UnauthorizedError,
|
||||
// #[error("no user found with this name")]
|
||||
// UserNotFoundError,
|
||||
// }
|
||||
|
||||
#[derive(Eq, PartialEq, Clone, Serialize, Debug, Deserialize)]
|
||||
pub enum UserRole {
|
||||
SuperUser,
|
||||
Developer,
|
||||
User,
|
||||
Anonymous,
|
||||
}
|
||||
impl Default for UserRole {
|
||||
fn default() -> Self {
|
||||
UserRole::Anonymous
|
||||
}
|
||||
}
|
||||
impl std::fmt::Display for UserRole {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
UserRole::SuperUser => write!(f,"superuser"),
|
||||
UserRole::Developer => write!(f,"developer"),
|
||||
UserRole::User => write!(f,"user"),
|
||||
UserRole::Anonymous => write!(f,"anonymous"),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl UserRole {
|
||||
#[allow(dead_code)]
|
||||
pub fn from_str(value: &str) -> UserRole {
|
||||
match value {
|
||||
"superuser" | "SuperUser" | "superUser" | "admin" => UserRole::SuperUser,
|
||||
"developer" | "Developer" => UserRole::Developer,
|
||||
"user" | "User" => UserRole::User,
|
||||
"anonymous" | "Anonymous" => UserRole::Anonymous,
|
||||
_ => UserRole::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
#[allow(dead_code)]
|
||||
pub fn deserialize_user_role<'de, D>(deserializer: D) -> Result<UserRole, D::Error>
|
||||
where D: Deserializer<'de> {
|
||||
let buf = String::deserialize(deserializer)?;
|
||||
Ok(UserRole::from_str(&buf))
|
||||
}
|
96
src/users/userdata.rs
Normal file
96
src/users/userdata.rs
Normal file
@ -0,0 +1,96 @@
|
||||
use std::collections::HashMap;
|
||||
use serde::{Deserialize,Serialize};
|
||||
|
||||
// use crate::defs::AppDBs;
|
||||
|
||||
fn default_empty() -> String {
|
||||
"".to_string()
|
||||
}
|
||||
fn default_items() -> HashMap<String,String> {
|
||||
HashMap::new()
|
||||
}
|
||||
fn default_expire() -> u64 {
|
||||
300
|
||||
}
|
||||
fn default_send_email() -> bool {
|
||||
false
|
||||
}
|
||||
fn default_isadmin() -> bool {
|
||||
false
|
||||
}
|
||||
fn default_otp_empty() -> String {
|
||||
String::from("")
|
||||
}
|
||||
#[derive(Default,Deserialize,Serialize,Debug,Clone)]
|
||||
pub struct UserData {
|
||||
#[serde(default = "default_empty")]
|
||||
pub id: String,
|
||||
#[serde(default = "default_empty")]
|
||||
pub name: String,
|
||||
#[serde(default = "default_empty")]
|
||||
pub fullname: String,
|
||||
#[serde(default = "default_empty")]
|
||||
pub description: String,
|
||||
#[serde(default = "default_empty")]
|
||||
pub email: String,
|
||||
#[serde(default = "default_empty")]
|
||||
pub password: String,
|
||||
#[serde(default = "default_otp_empty")]
|
||||
pub otp_code: String,
|
||||
#[serde(default = "default_otp_empty")]
|
||||
pub otp_url: String,
|
||||
#[serde(default = "default_otp_empty")]
|
||||
pub otp_auth: String,
|
||||
#[serde(default = "default_empty")]
|
||||
pub roles: String,
|
||||
#[serde(default = "default_items")]
|
||||
pub items: HashMap<String,String>,
|
||||
}
|
||||
// impl UserData {
|
||||
// pub fn from_id(id: String, _app_dbs: &AppDBs) -> Self {
|
||||
// Self {
|
||||
// id,
|
||||
// name: String::from(""),
|
||||
// fullname: String::from(""),
|
||||
// description: String::from(""),
|
||||
// email: String::from(""),
|
||||
// password: String::from(""),
|
||||
// roles: String::from(""),
|
||||
// items: Vec::new()
|
||||
// }
|
||||
// }
|
||||
// // pub fn contents_to_json(&self) -> Vec<String> {
|
||||
// // self.items.clone().into_iter().map(|item| item.to_json()).collect()
|
||||
// // }
|
||||
//}
|
||||
#[derive(Default,Deserialize,Serialize,Debug,Clone)]
|
||||
pub struct UserLogin {
|
||||
#[serde(default = "default_empty")]
|
||||
pub name: String,
|
||||
#[serde(default = "default_empty")]
|
||||
pub password: String,
|
||||
#[serde(default = "default_empty")]
|
||||
pub otp_auth: String,
|
||||
}
|
||||
|
||||
#[derive(Default,Deserialize,Serialize,Debug,Clone)]
|
||||
pub struct UserItem {
|
||||
#[serde(default = "default_empty")]
|
||||
pub name: String,
|
||||
#[serde(default = "default_empty")]
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[derive(Default,Deserialize,Serialize,Debug,Clone)]
|
||||
pub struct UserInvitation {
|
||||
#[serde(default = "default_empty")]
|
||||
pub email: String,
|
||||
#[serde(default = "default_empty")]
|
||||
pub roles: String,
|
||||
#[serde(default = "default_expire")]
|
||||
pub expire: u64,
|
||||
#[serde(default = "default_send_email")]
|
||||
pub send_email: bool,
|
||||
#[serde(default = "default_isadmin")]
|
||||
pub isadmin: bool,
|
||||
}
|
222
src/users/userstatus.rs
Normal file
222
src/users/userstatus.rs
Normal file
@ -0,0 +1,222 @@
|
||||
use serde::{Deserialize,Serialize,Deserializer};
|
||||
//use sqlx::{sqlite::SqlitePool, Row, Sqlite};
|
||||
// use sqlx::{
|
||||
// FromRow,
|
||||
// decode::Decode,
|
||||
// any::{AnyRow, AnyValueRef},
|
||||
// database::{
|
||||
// Database,
|
||||
// HasValueRef,
|
||||
// },
|
||||
// Row,
|
||||
// // Error,
|
||||
// };
|
||||
// use std::error::Error;
|
||||
// use std::str::FromStr;
|
||||
|
||||
//use super::User;
|
||||
|
||||
// #[derive(sqlx::FromRow)][derive(sqlx::Type)]
|
||||
//#[derive(sqlx::Type)]
|
||||
//#[sqlx(type_name = "userstatus_type")]
|
||||
// #[derive(sqlx::FromRow)]
|
||||
//#[sqlx(display_fromstr)]
|
||||
#[derive(Debug, Clone, PartialEq, PartialOrd, Serialize, Deserialize)]
|
||||
pub enum UserStatus {
|
||||
Created,
|
||||
Active,
|
||||
Pending,
|
||||
Lock,
|
||||
Unknown,
|
||||
}
|
||||
impl UserStatus {
|
||||
#[allow(dead_code)]
|
||||
pub fn from_str(status: &str) -> Self {
|
||||
match status {
|
||||
"created"|"Created" => UserStatus::Created,
|
||||
"active"|"Active" => UserStatus::Active,
|
||||
"pending"|"Pending" => UserStatus::Pending,
|
||||
"lock"|"Lock" => UserStatus::Lock,
|
||||
_ => UserStatus::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl std::fmt::Display for UserStatus {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
UserStatus::Created => write!(f,"Created"),
|
||||
UserStatus::Active => write!(f,"Active"),
|
||||
UserStatus::Pending => write!(f,"Pending"),
|
||||
UserStatus::Lock => write!(f,"Lock"),
|
||||
UserStatus::Unknown => write!(f,"Unknown"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for UserStatus {
|
||||
fn default() -> Self {
|
||||
UserStatus::Unknown
|
||||
}
|
||||
}
|
||||
#[allow(dead_code)]
|
||||
pub fn deserialize_user_status<'de, D>(deserializer: D) -> Result<UserStatus, D::Error>
|
||||
where D: Deserializer<'de> {
|
||||
let buf = String::deserialize(deserializer)?;
|
||||
Ok(UserStatus::from_str(&buf))
|
||||
}
|
||||
// impl sqlx::Type<sqlx::Any> for UserStatus {
|
||||
// fn type_info() -> sqlx::any::AnyTypeInfo {
|
||||
// //let res: sqlx::any::AnyTypeInfo = String::type_info();
|
||||
// let res = <str as sqlx::Type<sqlx::Any>>::type_info();
|
||||
// // let res = sqlx::any::AnyTypeInfo::default(); // String::type_info();
|
||||
// res
|
||||
// // String::type_info()
|
||||
// }
|
||||
|
||||
// // fn compatible(ty: &<sqlx::Any>::AnyTypeInfo) -> bool {
|
||||
// // *ty == Self::type_info()
|
||||
// // }
|
||||
// }
|
||||
/*
|
||||
impl sqlx::Type<sqlx::Sqlite> for UserStatus {
|
||||
fn type_info() -> sqlx::sqlite::SqliteTypeInfo {
|
||||
let t = String::type_info();
|
||||
t
|
||||
}
|
||||
}
|
||||
impl sqlx::Type<sqlx::Mssql> for UserStatus {
|
||||
fn type_info() -> sqlx::mssql::MssqlTypeInfo {
|
||||
String::type_info()
|
||||
}
|
||||
}
|
||||
impl sqlx::Type<sqlx::MySql> for UserStatus {
|
||||
fn type_info() -> sqlx::mysql::MySqlTypeInfo {
|
||||
String::type_info()
|
||||
}
|
||||
}
|
||||
impl sqlx::Type<sqlx::Postgres> for UserStatus {
|
||||
fn type_info() -> sqlx::postgres::PgTypeInfo {
|
||||
String::type_info()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'q> sqlx::Encode<'q, sqlx::Any> for UserStatus {
|
||||
fn encode_by_ref(
|
||||
&self,
|
||||
args: &mut sqlx::any::AnyArgumentBuffer<'q>,
|
||||
) -> sqlx::encode::IsNull {
|
||||
self.encode_by_ref(args)
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
/*
|
||||
impl<'r> sqlx::Decode<'r, sqlx::Any> for UserStatus {
|
||||
fn decode(
|
||||
// value: <DB as HasValueRef<'r>>::AnyValueRef,
|
||||
value: sqlx::any::AnyValueRef<'r>,
|
||||
) -> Result<Self, sqlx::error::BoxDynError> {
|
||||
//let string = value. try_decode()?;
|
||||
//let string = <&str as Decode<AnyValueRef>>::decode(value)?;
|
||||
let v = value.to_owned();
|
||||
let string = value.parse()?;
|
||||
let value = UserStatus::from_str(&string);
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl<'r, DB: Database> Decode<'r, DB> for UserStatus
|
||||
where
|
||||
// we want to delegate some of the work to string decoding so let's make sure strings
|
||||
// are supported by the database
|
||||
&'r str: Decode<'r, DB>
|
||||
{
|
||||
fn decode(
|
||||
value: <DB as HasValueRef<'r>>::ValueRef,
|
||||
) -> Result<UserStatus, Box<dyn Error + 'static + Send + Sync>> {
|
||||
// the interface of ValueRef is largely unstable at the moment
|
||||
// so this is not directly implementable
|
||||
// however, you can delegate to a type that matches the format of the type you want
|
||||
// to decode (such as a UTF-8 string)
|
||||
let val = <&str as Decode<DB>>::decode(value)?;
|
||||
// now you can parse this into your type (assuming there is a `FromStr`)
|
||||
//Ok(value.parse()?)
|
||||
Ok(UserStatus::from_str(&val))
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
impl<'r> Decode<'r, sqlx::Any> for UserStatus {
|
||||
fn decode(
|
||||
value: sqlx::any::AnyValueRef<'r>,
|
||||
) -> Result<Self, sqlx::error::BoxDynError> {
|
||||
//let mut decoder = <dyn sqlx::any::AnyDecode>::new(value)?;
|
||||
//let string = decoder.try_decode::<String>()?;
|
||||
//let username = decoder.try_decode::<String>()?;
|
||||
//let photo = decoder.try_decode::<Option<String>>()?;
|
||||
let string = <&str as Decode<sqlx::Any>>::decode(value)?;
|
||||
let value = UserStatus::from_str(&string);
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
impl<'r> Decode<'r, sqlx::Sqlite> for UserStatus {
|
||||
fn decode(
|
||||
value: sqlx::sqlite::SqliteValueRef<'r>,
|
||||
) -> Result<Self, sqlx::error::BoxDynError> {
|
||||
let string = <&str as Decode<sqlx::Sqlite>>::decode(value)?;
|
||||
let value = UserStatus::from_str(string);
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
// impl<'r> Decode<'r, sqlx::Mssql> for UserStatus {
|
||||
// fn decode(
|
||||
// value: sqlx::mssql::MssqlValueRef<'r>,
|
||||
// ) -> Result<UserStatus, Box<dyn Error + 'static + Send + Sync>> {
|
||||
// //) -> Result<Self, sqlx::error::BoxDynError> {
|
||||
// let string = <&str as Decode<sqlx::Mssql>>::decode(value)?;
|
||||
// let value = UserStatus::from_str(string);
|
||||
// Ok(value)
|
||||
// }
|
||||
// }
|
||||
impl<'r> Decode<'r, sqlx::MySql> for UserStatus {
|
||||
fn decode(
|
||||
value: sqlx::mysql::MySqlValueRef<'r>,
|
||||
) -> Result<Self, sqlx::error::BoxDynError> {
|
||||
let string = <&str as Decode<sqlx::MySql>>::decode(value)?;
|
||||
let value = UserStatus::from_str(string);
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
impl<'r> Decode<'r, sqlx::Postgres> for UserStatus {
|
||||
fn decode(
|
||||
value: sqlx::postgres::PgValueRef<'r>,
|
||||
) -> Result<Self, sqlx::error::BoxDynError> {
|
||||
//let mut decoder = sqlx::postgres::types::PgRecordDecoder::new(value)?;
|
||||
////let username = decoder.try_decode::<String>()?;
|
||||
//let string = decoder.try_decode::<String>()?;
|
||||
let string = <&str as Decode<sqlx::Postgres>>::decode(value)?;
|
||||
let value = UserStatus::from_str(string);
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromRow<'_, AnyRow> for UserStatus {
|
||||
fn from_row(row: &AnyRow) -> sqlx::Result<Self> {
|
||||
let v: String = row.try_get("status")?;
|
||||
Ok(
|
||||
UserStatus::from_str(&v)
|
||||
)
|
||||
}
|
||||
}
|
||||
impl FromStr for UserStatus {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Ok(UserStatus::from_str(s))
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
35
src/users/userstore.rs
Normal file
35
src/users/userstore.rs
Normal file
@ -0,0 +1,35 @@
|
||||
use sqlx::AnyPool;
|
||||
use std::env;
|
||||
|
||||
use crate::USERS_FILESTORE;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum UserStore {
|
||||
File(String),
|
||||
Sql(AnyPool),
|
||||
// SqlLite(SqlitePool),
|
||||
// None
|
||||
}
|
||||
impl UserStore {
|
||||
#[allow(dead_code)]
|
||||
pub async fn from_str(store: &str) -> Self {
|
||||
match store {
|
||||
"fs"|"file"|"FS"|"FILE" => {
|
||||
println!("From File: {}",USERS_FILESTORE);
|
||||
UserStore::File(String::from(USERS_FILESTORE))
|
||||
}
|
||||
"db"|"DB"|_ => {
|
||||
let db_url = env::var("DATABASE_URL").unwrap_or_else(|e|{
|
||||
eprintln!("Error env DATABASE_URL: {}",e);
|
||||
std::process::exit(2)
|
||||
});
|
||||
let pool = AnyPool::connect(&db_url).await.unwrap_or_else(|e|{
|
||||
eprintln!("Error pool DATABASE_URL: {}",e);
|
||||
std::process::exit(2)
|
||||
});
|
||||
println!("From DB: {}",&db_url);
|
||||
UserStore::Sql(pool)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user