diff --git a/Cargo.lock b/Cargo.lock index 24e2380..7e1698b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -382,9 +382,9 @@ dependencies = [ [[package]] name = "attribute-derive" -version = "0.6.1" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c124f12ade4e670107b132722d0ad1a5c9790bcbc1b265336369ea05626b4498" +checksum = "0c94f43ede6f25dab1dea046bff84d85dea61bd49aba7a9011ad66c0d449077b" dependencies = [ "attribute-derive-macro", "proc-macro2", @@ -394,13 +394,13 @@ dependencies = [ [[package]] name = "attribute-derive-macro" -version = "0.6.1" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b217a07446e0fb086f83401a98297e2d81492122f5874db5391bd270a185f88" +checksum = "b409e2b2d2dc206d2c0ad3575a93f001ae21a1593e2d0c69b69c308e63f3b422" dependencies = [ "collection_literals", "interpolator", - "proc-macro-error", + "manyhow", "proc-macro-utils", "proc-macro2", "quote", @@ -414,12 +414,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" -[[package]] -name = "base64" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" - [[package]] name = "base64" version = "0.20.0" @@ -742,16 +736,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d379af7f68bfc21714c6c7dea883544201741d2ce8274bb12fa54f89507f52a7" dependencies = [ "async-trait", - "json5", "lazy_static", "nom", "pathdiff", - "ron", - "rust-ini", "serde", - "serde_json", "toml", - "yaml-rust", ] [[package]] @@ -1002,12 +991,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "dlv-list" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" - [[package]] name = "dotenvy" version = "0.15.7" @@ -1603,6 +1586,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.6" @@ -1627,17 +1619,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "json5" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" -dependencies = [ - "pest", - "pest_derive", - "serde", -] - [[package]] name = "language-tags" version = "0.3.2" @@ -1655,9 +1636,9 @@ dependencies = [ [[package]] name = "leptos" -version = "0.5.0-rc1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d811de15430df8e4886afe09e5e741a886876c51ea32b8f11e0963ba9415e4b" +checksum = "4d3885e75a25bbf43c95350cf2f6b9f5228a3d911e28512c44c2a6c8aa49e9c9" dependencies = [ "cfg-if", "leptos_config", @@ -1675,9 +1656,9 @@ dependencies = [ [[package]] name = "leptos_actix" -version = "0.5.0-rc1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82ae271646ca49e2464ee454428775064a330658559e1c92d20170ee9c12b674" +checksum = "bd04ab9afac818fe45695b8e1f103714e612135b42519a21761c730fc9223c14" dependencies = [ "actix-http", "actix-web", @@ -1695,9 +1676,9 @@ dependencies = [ [[package]] name = "leptos_config" -version = "0.5.0-rc1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92015175317cafe8e651289a48bc3cdd68c276c6054229f20dbee73a81fea21f" +checksum = "d3936a83035a4ec03487792d8c9c2c5ad00c269d09701d102630ac5c31caa463" dependencies = [ "config", "regex", @@ -1708,9 +1689,9 @@ dependencies = [ [[package]] name = "leptos_dom" -version = "0.5.0-rc1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba03b4357fd08d2c84da3572f66f47973703e0ef6166c2dd0db4766f83310a72" +checksum = "6cbea8aeea07633b3559818fa963c03857751fbafc6bb4a73c995662836070e1" dependencies = [ "async-recursion", "cfg-if", @@ -1719,7 +1700,7 @@ dependencies = [ "getrandom", "html-escape", "indexmap 2.0.0", - "itertools", + "itertools 0.10.5", "js-sys", "leptos_reactive", "once_cell", @@ -1738,9 +1719,9 @@ dependencies = [ [[package]] name = "leptos_hot_reload" -version = "0.5.0-rc1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21cade49ea1a5e72b3a546b4507b3ef4e43586d306b7f20c180ed6a3bf855665" +checksum = "b56ec18e255737108b4f4d570c1c4f036f54a9989befe2658758500b636ebda4" dependencies = [ "anyhow", "camino", @@ -1756,9 +1737,9 @@ dependencies = [ [[package]] name = "leptos_integration_utils" -version = "0.5.0-rc1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb2d6a0d32b073fa1a9326f83b99f02f60a0e159fe0a7ce53d2708318114a6a3" +checksum = "77e1faf41644272929c47993af12928a51c0a03a1ed7ee55afcacf4a3a02073c" dependencies = [ "futures", "leptos", @@ -1770,15 +1751,15 @@ dependencies = [ [[package]] name = "leptos_macro" -version = "0.5.0-rc1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "befb8d664269550a918a6a160c10792951a37e95b542ca454c191ec7480505de" +checksum = "dae8be584ba63e002cec113e0a831f2ba17ad452104781a2b1b65555db049779" dependencies = [ "attribute-derive", "cfg-if", "convert_case 0.6.0", "html-escape", - "itertools", + "itertools 0.11.0", "leptos_hot_reload", "prettyplease", "proc-macro-error", @@ -1793,9 +1774,9 @@ dependencies = [ [[package]] name = "leptos_meta" -version = "0.5.0-rc1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c77f3ae209084fae41024f3023fa816a6fe91a45da00729ff349dd0ddb9b2c0" +checksum = "013e23a79d48c6eee6063b162e5ba0beb7d1a42c07361e4c16effb916160a5f0" dependencies = [ "cfg-if", "indexmap 2.0.0", @@ -1807,15 +1788,16 @@ dependencies = [ [[package]] name = "leptos_reactive" -version = "0.5.0-rc1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4382426f6e79d209408e362883b9542f934bafb3b98cc957f94e085f707508a0" +checksum = "90ec5366c79892fa8232dcfa6f05d610d0fd780af155fea8c466e77da18e744f" dependencies = [ "base64 0.21.2", "cfg-if", "futures", "indexmap 2.0.0", "js-sys", + "pin-project", "rustc-hash", "self_cell", "serde", @@ -1832,9 +1814,9 @@ dependencies = [ [[package]] name = "leptos_router" -version = "0.5.0-rc1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b250d706ddbfb9991c5615b35dd27596e5b23024d8ee50fe4717aa21f67bb3b1" +checksum = "f55951a1e4ee0b9c26e4ebc0c09ecc4b7fffbefecb912c72db0c5dfa33b1584c" dependencies = [ "cached", "cfg-if", @@ -1843,6 +1825,8 @@ dependencies = [ "js-sys", "lazy_static", "leptos", + "leptos_integration_utils", + "leptos_meta", "linear-map", "lru", "once_cell", @@ -1861,9 +1845,9 @@ dependencies = [ [[package]] name = "leptos_server" -version = "0.5.0-rc1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dfe03995c38441e45dc0d44828b657dbcff3b87c8ce5c2f4219297226c752b0" +checksum = "520f4f7221a323c029877ffb09e97d38cc805f1a5821f9554ecf0e7f6852100c" dependencies = [ "inventory", "lazy_static", @@ -1908,12 +1892,6 @@ dependencies = [ "serde_test", ] -[[package]] -name = "linked-hash-map" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" - [[package]] name = "linux-raw-sys" version = "0.4.5" @@ -1963,6 +1941,29 @@ dependencies = [ "hashbrown 0.14.0", ] +[[package]] +name = "manyhow" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516b76546495d933baa165075b95c0a15e8f7ef75e53f56b19b7144d80fd52bd" +dependencies = [ + "manyhow-macros", + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "manyhow-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ba072c0eadade3160232e70893311f1f8903974488096e2eb8e48caba2f0cf1" +dependencies = [ + "proc-macro-utils", + "proc-macro2", + "quote", +] + [[package]] name = "md-5" version = "0.9.1" @@ -2123,16 +2124,6 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" -[[package]] -name = "ordered-multimap" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a" -dependencies = [ - "dlv-list", - "hashbrown 0.12.3", -] - [[package]] name = "pad-adapter" version = "0.1.1" @@ -2189,50 +2180,6 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" -[[package]] -name = "pest" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f73935e4d55e2abf7f130186537b19e7a4abc886a0252380b59248af473a3fc9" -dependencies = [ - "thiserror", - "ucd-trie", -] - -[[package]] -name = "pest_derive" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aef623c9bbfa0eedf5a0efba11a5ee83209c326653ca31ff019bec3a95bfff2b" -dependencies = [ - "pest", - "pest_generator", -] - -[[package]] -name = "pest_generator" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3e8cba4ec22bada7fc55ffe51e2deb6a0e0db2d0b7ab0b103acc80d2510c190" -dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn 2.0.28", -] - -[[package]] -name = "pest_meta" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a01f71cb40bd8bb94232df14b946909e14660e33fc05db3e50ae2a82d7ea0ca0" -dependencies = [ - "once_cell", - "pest", - "sha2 0.10.7", -] - [[package]] name = "pin-project" version = "1.1.0" @@ -2616,17 +2563,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "ron" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88073939a61e5b7680558e6be56b419e208420c2adb92be54921fa6b72283f1a" -dependencies = [ - "base64 0.13.1", - "bitflags 1.3.2", - "serde", -] - [[package]] name = "rsa" version = "0.9.2" @@ -2663,16 +2599,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "rust-ini" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df" -dependencies = [ - "cfg-if", - "ordered-multimap", -] - [[package]] name = "rust_decimal" version = "1.31.0" @@ -2883,9 +2809,9 @@ dependencies = [ [[package]] name = "server_fn" -version = "0.5.0-rc1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c5839ea169c9ac14c08d368ed523d0896b050cf18d754fcbde19923995f19be" +checksum = "29eefae61211e81059a092a3428612c475a3a28e0ea4fb3fd49b0a940d837f84" dependencies = [ "ciborium", "const_format", @@ -2908,9 +2834,9 @@ dependencies = [ [[package]] name = "server_fn_macro" -version = "0.5.0-rc1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80116501286c018b8d6330be8b2bc286ed801f59439f2fbfad2b79d96942c2a2" +checksum = "f68140099f8e55bd526dc176d17d341189bf669d45216c4797ddc344610a84a4" dependencies = [ "const_format", "proc-macro-error", @@ -2923,9 +2849,9 @@ dependencies = [ [[package]] name = "server_fn_macro_default" -version = "0.5.0-rc1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "709cc458808a38f20b9d0121a3022fa35b704e5e577d352b92b5dd4ad78f4923" +checksum = "eee874f357d640ad221ba0c27c2559fa3d1434f7f7bbf688a34118518c5924b7" dependencies = [ "server_fn_macro", "syn 2.0.28", @@ -3080,7 +3006,7 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c12bc9199d1db8234678b7051747c07f517cdcf019262d1847b94ec8b1aee3e" dependencies = [ - "itertools", + "itertools 0.10.5", "nom", "unicode_categories", ] @@ -3561,12 +3487,6 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" -[[package]] -name = "ucd-trie" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81" - [[package]] name = "unicase" version = "2.6.0" @@ -3963,15 +3883,6 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "735a71d46c4d68d71d4b24d03fdc2b98e38cea81730595801db779c04fe80d70" -[[package]] -name = "yaml-rust" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" -dependencies = [ - "linked-hash-map", -] - [[package]] name = "yansi" version = "1.0.0-rc.1" diff --git a/Cargo.toml b/Cargo.toml index a3dcd2c..e7b1719 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,10 +12,10 @@ actix-web = { version = "4.4.0", optional = true, features = ["macros"] } actix-session = { version = "0.8.0", optional = true, features = ["cookie-session"] } console_error_panic_hook = "0.1" cfg-if = "1" -leptos = { version = "0.5.0-rc1" } -leptos_meta = { version = "0.5.0-rc1" } -leptos_actix = { version = "0.5.0-rc1", optional = true } -leptos_router = { version = "0.5.0-rc1" } +leptos = { version = "0.5.0" } +leptos_meta = { version = "0.5.0" } +leptos_actix = { version = "0.5.0", optional = true } +leptos_router = { version = "0.5.0" } serde = { version = "1", features = ["derive"] } wasm-bindgen = "=0.2.87" web-sys = { version = "0.3.61", features = ["Navigator"] } diff --git a/src/app.rs b/src/app.rs index 0552ba5..0865544 100644 --- a/src/app.rs +++ b/src/app.rs @@ -6,14 +6,42 @@ use leptos_meta::*; use leptos_router::*; use crate::components::admin_portal::AdminPortal; use crate::components::header::Header; +use crate::components::user_menu::MenuOpener; use crate::pages::login::Login; use crate::pages::public::Public; +#[derive(Clone, Copy)] +pub struct MenuHelper { + opened: RwSignal +} + +impl MenuHelper { + pub fn new() -> Self { + let opened = create_rw_signal(MenuOpener::new()); + Self { + opened + } + } + + pub fn set_opened(&self, opened: MenuOpener) { + self.opened.set(opened); + } + + pub fn close(&self) { + self.opened.get().close(); + } + + pub fn reset(&self) { + self.opened.set(MenuOpener::new()); + } +} + #[component] pub fn App() -> impl IntoView { // Provides context that manages stylesheets, titles, meta tags, etc. provide_meta_context(); init_locales(); + provide_context(MenuHelper::new()); view! {
diff --git a/src/backend/data.rs b/src/backend/data.rs index 32672f6..d028c97 100644 --- a/src/backend/data.rs +++ b/src/backend/data.rs @@ -1,5 +1,6 @@ //use chrono::{NaiveDate, NaiveTime, Weekday}; //use rust_decimal::Decimal; +#![allow(unused_variables)] use serde::{Deserialize, Serialize}; //use uuid::Uuid; use validator::Validate; @@ -51,11 +52,16 @@ impl User { #[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Validate, Default)] pub struct UserProfile { + #[validate(length(min = 1,message = "Username cannot be empty"))] login: String, + #[validate(must_match(other = "password_ver", message = "Passwords doesn't match"))] + password: Option, + password_ver: Option, full_name: String, #[validate(email(message = "Enter valid email address"))] email: String, - get_emails: Option + get_emails: Option, + admin: Option } impl UserProfile { @@ -75,6 +81,12 @@ impl UserProfile { pub fn get_emails(&self) -> bool { self.get_emails.is_some() } + pub fn admin(&self) -> bool { + self.admin.is_some() + } + pub fn password(&self) -> &Option { + &self.password + } } #[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Validate, Default)] diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 56b20fb..b46214f 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -28,8 +28,9 @@ macro_rules! user_check { use crate::backend::user::logged_in_user; perm_check!(is_logged_in); + let user = logged_in_user().await.unwrap_or(User::default()); - if logged_in_user().await.unwrap_or(User::default()).login != $check { + if !user.admin && user.login != $check { let response = expect_context::(); response.set_status(StatusCode::FORBIDDEN); @@ -41,6 +42,9 @@ macro_rules! user_check { cfg_if!{ if #[cfg(feature = "ssr")] { use sqlx::PgPool; + use actix_web::web::Data; + use leptos_actix::extract; + use leptos::ServerFnError; #[derive(Clone)] pub struct AppData { @@ -58,5 +62,9 @@ cfg_if!{ &self.db_pool } } + + pub async fn get_pool() -> Result { + extract(|data: Data| async move { data.db_pool().clone() }).await + } } } \ No newline at end of file diff --git a/src/backend/user.rs b/src/backend/user.rs index a5efef3..240877e 100644 --- a/src/backend/user.rs +++ b/src/backend/user.rs @@ -60,15 +60,13 @@ cfg_if! { if #[cfg(feature = "ssr")] { #[server(Login, "/api")] pub async fn login(username: String, password: String) -> Result, ServerFnError> { - use crate::backend::AppData; use actix_session::*; - use actix_web::web::Data; use leptos_actix::extract; use actix_web::http::StatusCode; use leptos_actix::ResponseOptions; + use crate::backend::get_pool; - let pool = extract(|data: Data| async move { data.db_pool().clone() }).await?; - + let pool = get_pool().await?; let user = user_from_login(&pool, &username).await.unwrap_or(User::default()); if !user.login.is_empty() && pwhash::bcrypt::verify(password, &user.password) { @@ -114,41 +112,48 @@ pub async fn get_user() -> Result, ServerFnError> { #[server(GetUsers, "/api", "Url", "get_users")] pub async fn get_users() -> Result>, ServerFnError> { - use crate::backend::AppData; - use actix_web::web::Data; - use leptos_actix::extract; use crate::perm_check; + use crate::backend::get_pool; perm_check!(is_admin); - let pool = extract(|data: Data| async move { data.db_pool().clone() }).await?; - let users = sqlx::query_as::<_, User>(r#"SELECT * FROM "user""#).fetch_all(&pool).await?; + let pool = get_pool().await?; + let users = sqlx::query_as::<_, User>(r#"SELECT * FROM "user" ORDER BY login"#).fetch_all(&pool).await?; Ok(ApiResponse::Data(users)) } #[server] pub async fn update_profile(user: UserProfile) -> Result, ServerFnError> { - use crate::backend::AppData; - use actix_web::web::Data; - use leptos_actix::extract; use crate::user_check; + use crate::backend::get_pool; user_check!(user.login()); + let usr = logged_in_user().await.unwrap_or(User::default()); + + if !usr.admin && user.admin() { + let response = expect_context::(); + response.set_status(StatusCode::FORBIDDEN); + + return Ok(ApiResponse::Error("You can't escalate your privileges".to_string())) + } - let pool = extract(|data: Data| async move { data.db_pool().clone() }).await?; - sqlx::query(r#"UPDATE "user" SET full_name = $1, email = $2, get_emails = $3 WHERE login = $4"#) + let pool = get_pool().await?; + sqlx::query(r#"UPDATE "user" SET full_name = $1, email = $2, get_emails = $3, admin = $4 WHERE login = $5"#) .bind(user.full_name()) .bind(user.email()) .bind(user.get_emails()) + .bind(user.admin()) .bind(user.login()) .execute(&pool) .await?; - let usr = user_from_login(&pool, user.login()).await?; - extract(|session: Session| async move { - let _ = session.insert("user", usr); - }).await?; + if logged_in_user().await.unwrap_or_default().login == user.login() { + let usr = user_from_login(&pool, user.login()).await?; + extract(|session: Session| async move { + let _ = session.insert("user", usr); + }).await?; + } Ok(ApiResponse::Data(())) } @@ -161,17 +166,17 @@ impl ForValidation for UpdateProfile { #[server] pub async fn change_pwd(new_pw: PwdChange) -> Result, ServerFnError> { - use crate::backend::AppData; - use actix_web::web::Data; - use leptos_actix::extract; use crate::user_check; + use crate::backend::get_pool; user_check!(new_pw.login()); - let pool = extract(|data: Data| async move { data.db_pool().clone() }).await?; + let pool = get_pool().await?; let usr = user_from_login(&pool, new_pw.login()).await?; + let user = logged_in_user().await.unwrap_or(User::default()); - if !pwhash::bcrypt::verify(new_pw.old_password(), &usr.password) { + if (!user.admin || user.login == new_pw.login()) + && !pwhash::bcrypt::verify(new_pw.old_password(), &usr.password) { let response = expect_context::(); response.set_status(StatusCode::UNAUTHORIZED); @@ -191,4 +196,68 @@ impl ForValidation for ChangePwd { fn entity(&self) -> &dyn Validate { &self.new_pw } +} + +#[server] +pub async fn create_user(user: UserProfile) -> Result, ServerFnError> { + use crate::perm_check; + use crate::backend::get_pool; + + perm_check!(is_admin); + + let pool = get_pool().await?; + let count: (i64,) = sqlx::query_as(r#"SELECT COUNT(id) FROM "user" WHERE login = $1"#) + .bind(user.login()) + .fetch_one(&pool) + .await?; + + if count.0 != 0 { + let response = expect_context::(); + response.set_status(StatusCode::CONFLICT); + + return Ok(ApiResponse::Error("Username already exists".to_string())); + } + + let usr_pw = user.password().clone(); + + sqlx::query(r#"INSERT INTO "user"(login, password, full_name, email, admin, get_emails) VALUES($1, $2, $3, $4, $5, $6)"#) + .bind(user.login()) + .bind(pwhash::bcrypt::hash(usr_pw.unwrap_or("".to_string())).unwrap()) + .bind(user.full_name()) + .bind(user.email()) + .bind(user.admin()) + .bind(user.get_emails()) + .execute(&pool) + .await?; + + Ok(ApiResponse::Data(())) +} + +impl ForValidation for CreateUser { + fn entity(&self) -> &dyn Validate { + &self.user + } +} + +#[server] +pub async fn delete_user(id: i32) -> Result, ServerFnError> { + use crate::perm_check; + use crate::backend::get_pool; + + perm_check!(is_admin); + let user = logged_in_user().await.unwrap_or_default(); + + if user.id() == id { + let response = expect_context::(); + response.set_status(StatusCode::NOT_ACCEPTABLE); + + return Ok(ApiResponse::Error("You can't delete yourself".to_string())) + } + + sqlx::query(r#"DELETE FROM "user" WHERE id=$1"#) + .bind(id) + .execute(&get_pool().await?) + .await?; + + Ok(ApiResponse::Data(())) } \ No newline at end of file diff --git a/src/components/data_form.rs b/src/components/data_form.rs index a6f3966..f3d1458 100644 --- a/src/components/data_form.rs +++ b/src/components/data_form.rs @@ -39,7 +39,9 @@ pub fn data_form + Clone + ForValidation>( + + + + + } +} diff --git a/src/components/header.rs b/src/components/header.rs index 86ccd1b..151e57a 100644 --- a/src/components/header.rs +++ b/src/components/header.rs @@ -7,11 +7,11 @@ pub fn Header() -> impl IntoView { <Meta charset="utf-8"/> diff --git a/src/components/modal_box.rs b/src/components/modal_box.rs index f14235a..0ad0295 100644 --- a/src/components/modal_box.rs +++ b/src/components/modal_box.rs @@ -5,14 +5,27 @@ use leptos::*; pub struct DialogOpener { visible: ReadSignal<bool>, set_visible: WriteSignal<bool>, + empty: ReadSignal<String>, + set_empty: WriteSignal<String>, + not_checked: ReadSignal<Option<String>>, + set_not_checked: WriteSignal<Option<String>>, + show_err: RwSignal<bool> } impl DialogOpener { pub fn new() -> Self { let (visible, set_visible) = create_signal(false); + let (empty, set_empty) = create_signal("".to_string()); + let (not_checked, set_not_checked) = create_signal(None); + let show_err = create_rw_signal(false); DialogOpener { visible, set_visible, + empty, + set_empty, + not_checked, + set_not_checked, + show_err } } @@ -26,6 +39,25 @@ impl DialogOpener { pub fn hide(&self) { self.set_visible.update(|state| *state = false); + self.set_empty.set("".to_string()); + self.set_not_checked.set(None); + self.show_err.set(false); + } + + pub fn empty(&self) -> String { + self.empty.get() + } + + pub fn not_checked(&self) -> Option<String> { + self.not_checked.get() + } + + pub fn show_err(&self) -> bool { + self.show_err.get() + } + + pub fn display_err(&self) { + self.show_err.set(true); } } diff --git a/src/components/server_err.rs b/src/components/server_err.rs index 158b38d..3cf1721 100644 --- a/src/components/server_err.rs +++ b/src/components/server_err.rs @@ -12,8 +12,15 @@ pub fn ServerErr( if let Some(val) = result.get() { match val { Ok(resp) => if let ApiResponse::Error(err) = resp { + opener.display_err(); view! { - <div class="alert alert-danger"> + <div class="alert alert-danger" style={move || { + if opener.show_err() { + "" + } else { + "display: none" + } + }}> {trl(&err)} </div> } @@ -30,16 +37,6 @@ pub fn ServerErr( } } } - /*if let Err(e) = val { - view! { - <div class="alert alert-danger"> - "Server error: " {e.to_string()} - </div> - } - } else { - opener.hide(); - view! {<div></div>} - }*/ } else { view! {<div></div>} } diff --git a/src/components/user_menu.rs b/src/components/user_menu.rs index c0780c9..8e84383 100644 --- a/src/components/user_menu.rs +++ b/src/components/user_menu.rs @@ -1,10 +1,11 @@ use leptos::*; +use crate::app::MenuHelper; use crate::backend::data::User; use crate::backend::user::{get_user, logout}; use crate::components::modal_box::DialogOpener; use crate::locales::trl; -#[derive(Copy, Clone)] +#[derive(Copy, Clone, Debug)] pub struct MenuOpener { visible: ReadSignal<bool>, set_visible: WriteSignal<bool>, @@ -23,9 +24,23 @@ impl MenuOpener { self.visible.get() } - pub fn toggle(&self) { + pub fn toggle(self) { let visible = self.visible.get(); - self.set_visible.update(|v| *v = !visible) + self.set_visible.update(|v| *v = !visible); + + let helper = use_context::<MenuHelper>().expect("No menu helper"); + if !visible { + helper.close(); + helper.set_opened(self); + } else { + helper.reset(); + } + } + + pub fn close(&self) { + self.set_visible.set(false); + let helper = use_context::<MenuHelper>().expect("No menu helper"); + helper.reset(); } } @@ -38,55 +53,76 @@ pub fn UserMenu( let user = create_resource(move || opener.visible(), move |_| get_user()); view! { - <ul class={move || if opener.visible() {"dropdown-menu dropdown-menu-end show"} else - {"dropdown-menu dropdown-menu-end"}} - data-bs-popper="none"> + <ul + class=move || { + if opener.visible() { + "dropdown-menu dropdown-menu-end show" + } else { + "dropdown-menu dropdown-menu-end" + } + } + data-bs-popper="none" + on:mouseleave=move |_| opener.close() + > <li> - <a class="dropdown-item" href="#" on:click=move |_| {editor.show(); opener.toggle()}> - <div class="d-flex"> - <div class="flex-shrink-0 me-3"> - <i class="bx bxs-user-account" /> - </div> - <div class="flex-grow-1"> - <Suspense fallback=move || view! {<span>"Loading..."</span>}> - {move || { - user.get().map(|u| match u { - Ok(user) => { - let usr = user.unwrap_or(User::default()); - user_profile.update(|u| *u = usr.clone()); - view! { - <span class="fw-semibold d-block"> - {usr.full_name.unwrap_or("".to_string())} - </span> - //<small class="text-muted">"Admin"</small> - }}, - Err(_) => view! {<span>"Error loading user"</span>} - }) - }} - </Suspense> - </div> - </div> - </a> + <a class="dropdown-item" href="#" on:click=move |_| { editor.show() }> + <div class="d-flex"> + <div class="flex-shrink-0 me-3"> + <i class="bx bxs-user-account"></i> + </div> + <div class="flex-grow-1"> + <Suspense fallback=move || { + view! { <span>"Loading..."</span> } + }> + {move || { + user.get() + .map(|u| match u { + Ok(user) => { + let usr = user.unwrap_or(User::default()); + user_profile.update(|u| *u = usr.clone()); + view! { + <span class="fw-semibold d-block"> + {usr.full_name} + </span> + } + } + Err(_) => { + view! { + // <small class="text-muted">"Admin"</small> + <span>"Error loading user"</span> + } + } + }) + }} + + </Suspense> + </div> + </div> + </a> </li> <li> - <a class="dropdown-item" href="#" on:click=move |_| {pw_dialog.show(); opener.toggle()}> - <i class="bx bx-lock me-2"></i> - <span class="align-middle">{trl("Change password")}</span> - </a> + <a class="dropdown-item" href="#" on:click=move |_| { pw_dialog.show() }> + <i class="bx bx-lock me-2"></i> + <span class="align-middle">{trl("Change password")}</span> + </a> </li> <li> - <div class="dropdown-divider"></div> + <div class="dropdown-divider"></div> </li> <li> - <a class="dropdown-item" href="/login" on:click=move |_| { - spawn_local(async move { - let _ = logout().await; - }); - }> - <i class="bx bx-power-off me-2"></i> - <span class="align-middle">{trl("Log Out")}</span> - </a> + <a + class="dropdown-item" + href="/login" + on:click=move |_| { + spawn_local(async move { + let _ = logout().await; + }); + } + > + <i class="bx bx-power-off me-2"></i> + <span class="align-middle">{trl("Log Out")}</span> + </a> </li> - </ul> + </ul> } } \ No newline at end of file diff --git a/src/pages/change_pwd.rs b/src/pages/change_pwd.rs index 7ce6926..e108f5c 100644 --- a/src/pages/change_pwd.rs +++ b/src/pages/change_pwd.rs @@ -1,62 +1,64 @@ use leptos::*; -use crate::backend::data::{ApiResponse, User}; -use crate::backend::user::ChangePwd; +use crate::backend::data::User; +use crate::backend::user::{ChangePwd, get_user}; use crate::components::data_form::DataForm; use crate::components::modal_box::DialogOpener; #[component] pub fn change_password(user: ReadSignal<User>, opener: DialogOpener) -> impl IntoView { let change_pwd = create_server_action::<ChangePwd>(); - let empty = create_rw_signal("".to_string()); + let logged_in = create_resource(||(), move |_| get_user()); view! { - {move || { - if let Some(res) = change_pwd.value().get() { - if let Ok(r) = res { - if let ApiResponse::Data(_) = r { empty.update(|e| *e = "".to_string())} - } - } - view! { - <DataForm opener=opener action=change_pwd title="Change password"> - <input type="hidden" value={move || user.get().login} name="new_pw[login]"/> - <div class="row"> - <div class="col mb-3"> - <label for="oldPw" class="form-label">"Old password"</label> - <input - type="password" - id="oldPw" - class="form-control" - name="new_pw[old_password]" - prop:value={move || empty.get()} - /> - </div> - </div> - <div class="row"> - <div class="col mb-3"> - <label for="newPw" class="form-label">"New password"</label> - <input - type="password" - id="newPw" - class="form-control" - name="new_pw[password]" - prop:value={move || empty.get()} - /> - </div> - </div> - <div class="row"> - <div class="col mb-3"> - <label for="verPw" class="form-label">"Verify password"</label> - <input - type="password" - id="verPw" - class="form-control" - name="new_pw[password_ver]" - prop:value={move || empty.get()} - /> - </div> - </div> - </DataForm> - } - }} + <DataForm opener=opener action=change_pwd title="Change password"> + <input type="hidden" value={move || user.get().login} name="new_pw[login]"/> + <Suspense fallback=move || view! {<div></div>}> + <div class="row"> + <div class="col mb-3" style={move || { + logged_in.get().map(|u| { match u { + Ok(u) => { + if let Some(u) = u { + if u.login != user.get().login && u.admin { "display: none" } else { "" } + } else { "" } + } + Err(_) => "" + }}) + }}> + <label for="oldPw" class="form-label">"Old password"</label> + <input + type="password" + id="oldPw" + class="form-control" + name="new_pw[old_password]" + prop:value={move || opener.empty()} + /> + </div> + </div> + </Suspense> + <div class="row"> + <div class="col mb-3"> + <label for="newPw" class="form-label">"New password"</label> + <input + type="password" + id="newPw" + class="form-control" + name="new_pw[password]" + prop:value={move || opener.empty()} + /> + </div> + </div> + <div class="row"> + <div class="col mb-3"> + <label for="verPw" class="form-label">"Verify password"</label> + <input + type="password" + id="verPw" + class="form-control" + name="new_pw[password_ver]" + prop:value={move || opener.empty()} + /> + </div> + </div> + </DataForm> } } \ No newline at end of file diff --git a/src/pages/mod.rs b/src/pages/mod.rs index 44fa8d5..045ff92 100644 --- a/src/pages/mod.rs +++ b/src/pages/mod.rs @@ -8,3 +8,4 @@ pub mod profile_edit; pub mod change_pwd; pub mod users; pub mod user_edit; +pub mod user_delete; diff --git a/src/pages/profile_edit.rs b/src/pages/profile_edit.rs index 633e1cb..2c480eb 100644 --- a/src/pages/profile_edit.rs +++ b/src/pages/profile_edit.rs @@ -1,16 +1,17 @@ use leptos::*; use crate::backend::data::User; -use crate::backend::user::UpdateProfile; +use crate::backend::user::{get_user, UpdateProfile}; use crate::components::data_form::DataForm; use crate::components::modal_box::DialogOpener; #[component] pub fn ProfileEdit(user: ReadSignal<User>, opener: DialogOpener) -> impl IntoView { let update_user = create_server_action::<UpdateProfile>(); + let logged_in = create_resource(||(), move |_| get_user()); view! { <DataForm opener=opener action=update_user title="Edit profile"> - <input type="hidden" value={move || user.get().login} name="user[login]"/> + <input type="hidden" prop:value={move || user.get().login} name="user[login]"/> <div class="row"> <div class="col mb-3"> <label for="name" class="form-label">"Full name"</label> @@ -50,6 +51,31 @@ pub fn ProfileEdit(user: ReadSignal<User>, opener: DialogOpener) -> impl IntoVie <label class="form-check-label" for="getMail">"Get emails"</label> </div> </div> + <Suspense fallback=move || view! {<div></div>}> + <div class="row" style={move || { + logged_in.get().map(|u| match u { + Ok(usr) => { + let usr = usr.unwrap_or_default(); + if usr.login == user.get().login && !usr.login.is_empty() { "display: none" } + else {""} + } + Err(_) => "" + }) + } }> + <div class="col mb-3"> + <input + class="form-check-input" + type="checkbox" + id="admin" + prop:value={move || if user.get().admin {"true"} else {"false"}} + prop:checked={move || user.get().admin} + name="user[admin]" + //disabled + /> + <label class="form-check-label" for="admin">"Admin"</label> + </div> + </div> + </Suspense> </DataForm> } } \ No newline at end of file diff --git a/src/pages/settings.rs b/src/pages/settings.rs index b4f17c5..2ed9a86 100644 --- a/src/pages/settings.rs +++ b/src/pages/settings.rs @@ -1,44 +1,18 @@ use leptos::*; use crate::locales::trl; use crate::pages::company_info::CompanyInfo; +use crate::pages::users::Users; #[component] pub fn Settings() -> impl IntoView { view! { <h1>{trl("Settings")}</h1> <div class="row mb-5"> - <div class="col-md-6 col-lg-4 mb-3"> + <div class="col-md"> <CompanyInfo/> </div> - <div class="col-md-6 col-lg-4 mb-3"> - <div class="card h-100"> - <div class="card-body"> - <h5 class="card-title">"Card title"</h5> - <h6 class="card-subtitle text-muted">"Support card subtitle"</h6> - </div> - <img class="img-fluid" src="../assets/img/elements/13.jpg" alt="Card image cap" /> - <div class="card-body"> - <p class="card-text">"Bear claw sesame snaps gummies chocolate."</p> - <a href="javascript:void(0);" class="card-link">"Card link"</a> - <a href="javascript:void(0);" class="card-link">"Another link"</a> - </div> - </div> - </div> - <div class="col-md-6 col-lg-4 mb-3"> - <div class="card h-100"> - <div class="card-body"> - <h5 class="card-title">"Card title"</h5> - <h6 class="card-subtitle text-muted">"Support card subtitle"</h6> - <img - class="img-fluid d-flex mx-auto my-4" - src="../assets/img/elements/4.jpg" - alt="Card image cap" - /> - <p class="card-text">"Bear claw sesame snaps gummies chocolate."</p> - <a href="javascript:void(0);" class="card-link">Card link</a> - <a href="javascript:void(0);" class="card-link">Another link</a> - </div> - </div> + <div class="col-md"> + <Users/> </div> </div> } diff --git a/src/pages/user_delete.rs b/src/pages/user_delete.rs new file mode 100644 index 0000000..78099a1 --- /dev/null +++ b/src/pages/user_delete.rs @@ -0,0 +1,17 @@ +use leptos::*; +use crate::backend::data::User; +use crate::backend::user::DeleteUser; +use crate::components::data_form::QuestionDialog; +use crate::components::modal_box::DialogOpener; + +#[component] +pub fn user_delete(user: ReadSignal<User>, opener: DialogOpener) -> impl IntoView { + let del_user = create_server_action::<DeleteUser>(); + + view! { + <QuestionDialog opener=opener action=del_user title="Delete user"> + <input type="hidden" prop:value={move || user.get().id()} name="id"/> + <div>"Are you sure you want to delete user "{move || user.get().full_name}"?"</div> + </QuestionDialog> + } +} \ No newline at end of file diff --git a/src/pages/user_edit.rs b/src/pages/user_edit.rs new file mode 100644 index 0000000..4eae4fd --- /dev/null +++ b/src/pages/user_edit.rs @@ -0,0 +1,101 @@ +use leptos::*; +use crate::backend::user::CreateUser; +use crate::components::data_form::DataForm; +use crate::components::modal_box::DialogOpener; + +#[component] +pub fn user_edit(opener: DialogOpener) -> impl IntoView { + let create_usr = create_server_action::<CreateUser>(); + view! { + <DataForm opener=opener action=create_usr title="Create user"> + //<input type="hidden" value={move || company.get().id()} name="company[id]"/> + <div class="row"> + <div class="col mb-3"> + <label for="username" class="form-label">"Username"</label> + <input + type="text" + id="username" + class="form-control" + placeholder="Enter username" + prop:value={move || opener.empty()} + name="user[login]" + /> + </div> + </div> + <div class="row"> + <div class="col mb-3"> + <label for="password" class="form-label">"Password"</label> + <input + type="password" + id="password" + class="form-control" + prop:value={move || opener.empty()} + name="user[password]" + /> + </div> + </div> + <div class="row"> + <div class="col mb-3"> + <label for="passwordVer" class="form-label">"Verify password"</label> + <input + type="password" + id="passwordVer" + class="form-control" + prop:value={move || opener.empty()} + name="user[password_ver]" + /> + </div> + </div> + <div class="row"> + <div class="col mb-3"> + <label for="fullName" class="form-label">"Full name"</label> + <input + type="text" + id="fullName" + class="form-control" + placeholder="Enter full name" + prop:value={move || opener.empty()} + name="user[full_name]" + /> + </div> + </div> + <div class="row"> + <div class="col mb-3"> + <label for="email" class="form-label">"Email"</label> + <input + type="text" + id="email" + class="form-control" + placeholder="Enter email" + prop:value={move || opener.empty()} + name="user[email]" + /> + </div> + </div> + <div class="row"> + <div class="col mb-3"> + <input + type="checkbox" + id="admin" + class="form-check-input" + prop:checked={move || opener.not_checked()} + name="user[admin]" + /> + <label for="admin" class="form-label">"Admin"</label> + </div> + </div> + <div class="row"> + <div class="col mb-3"> + <input + type="checkbox" + id="getEmails" + class="form-check-input" + prop:checked={move || opener.not_checked()} + name="user[get_emails]" + /> + <label for="geEmails" class="form-label">"Get emails"</label> + </div> + </div> + </DataForm> + } +} \ No newline at end of file diff --git a/src/pages/users.rs b/src/pages/users.rs new file mode 100644 index 0000000..6bb3517 --- /dev/null +++ b/src/pages/users.rs @@ -0,0 +1,116 @@ +use leptos::*; +use crate::backend::data::{ApiResponse, User}; +use crate::backend::user::get_users; +use crate::components::modal_box::DialogOpener; +use crate::components::user_menu::MenuOpener; +use crate::locales::trl; +use crate::pages::change_pwd::ChangePassword; +use crate::pages::profile_edit::ProfileEdit; +use crate::pages::user_delete::UserDelete; +use crate::pages::user_edit::UserEdit; + +#[component] +pub fn users() -> impl IntoView { + let editor = DialogOpener::new(); + let profile_editor = DialogOpener::new(); + let pwd_dialog = DialogOpener::new(); + let delete_dialog = DialogOpener::new(); + let users = create_blocking_resource( + move || editor.visible() || profile_editor.visible() || delete_dialog.visible(), move |_| {get_users()}); + let (usr, set_usr) = create_signal::<Vec<User>>(vec![]); + let (profile, set_profile) = create_signal(User::default()); + + view! { + <UserEdit opener=editor/> + <ProfileEdit user=profile opener=profile_editor/> + <ChangePassword opener=pwd_dialog user=profile/> + <UserDelete opener=delete_dialog user=profile/> + <div class="card mb-3"> + <div class="card-body"> + <h5 class="card-title"><i class="bx bx-user"></i>" "{trl("Users")}</h5> + <Transition fallback=move || view! {<p>{trl("Loading...")}</p> }> + <table class="table card-table"> + <thead> + <tr> + <th>{trl("Username")}</th> + <th>{trl("Full name")}</th> + <th>{trl("Admin")}</th> + <th>{trl("Actions")}</th> + </tr> + </thead> + {move || { + users.get().map(|u| match u { + Err(e) => { + let err = if e.to_string().contains("403") { + "Only admin can edit users".to_string() + } else { + e.to_string() + }; + view! {<tbody class="table-border-bottom-0"> + <tr><td colspan=4>{trl(&err)}</td></tr></tbody>}} + Ok(u) => { + match u { + ApiResponse::Data(u) => { + set_usr.update(|users| *users = u.clone()); + view! {<tbody class="table-border-bottom-0"> + <For each=move || usr.get() + key=|user| user.id() + children=move |user: User| { + let menu = MenuOpener::new(); + let user_profile = user.clone(); + let user_passwd = user.clone(); + let user_delete = user.clone(); + view! { + <tr> + <td>{&user.login}</td> + <td>{&user.full_name.unwrap_or("".to_string())}</td> + <td>{if user.admin {view! {<i class="bx bx-check"></i>}} + else {view! {<i></i>}}}</td> + <td> + <div class="dropdown"> + <button type="button" class="btn p-0 dropdown-toggle hide-arrow" + on:click=move |_| menu.toggle()> + <i class="bx bx-dots-vertical-rounded"></i> + </button> + <div class={move || if menu.visible() {"dropdown-menu show"} else {"dropdown-menu"} } + style="position: absolute; insert: 0px 0px auto; margin: 0px; transform: translate3d(-160px, 0px, 0px);" + on:mouseleave=move |_| menu.toggle()> + <a class="dropdown-item" href="javascript:void(0);" on:click=move |_| { + set_profile.set(user_profile.clone()); + profile_editor.show(); + }> + <i class="bx bx-edit-alt me-1"></i> {trl("Edit")}</a> + <a class="dropdown-item" href="javascript:void(0);" on:click=move |_| { + set_profile.set(user_passwd.clone()); + pwd_dialog.show(); + }> + <i class="bx bx-lock me-1"></i> {trl("Change password")}</a> + <a class="dropdown-item" href="javascript:void(0);" on:click=move |_| { + set_profile.set(user_delete.clone()); + delete_dialog.show(); + }> + <i class="bx bx-trash me-1"></i> {trl("Delete")}</a> + </div> + </div> + </td> + </tr> + } + }/></tbody> + } + } + ApiResponse::Error(e) => {view! {<tbody class="table-border-bottom-0"> + <tr><td colspan=4>{trl(&e)}</td></tr></tbody>}} + } + } + }) + }} + </table> + </Transition> + <a href="#" class="card-link" on:click=move |_| editor.show()> + <i class="bx bx-plus-circle fs-4 lh-0"></i> + </a> + + </div> + </div> + } +} \ No newline at end of file