diff --git a/Cargo.lock b/Cargo.lock index 0a7b568..24e2380 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -44,9 +44,9 @@ dependencies = [ [[package]] name = "actix-http" -version = "3.3.1" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2079246596c18b4a33e274ae10c0e50613f4d32a4198e09c7b93771013fed74" +checksum = "a92ef85799cba03f76e4f7c10f533e66d87c9a7e7055f3391f09000ad8351bc9" dependencies = [ "actix-codec", "actix-rt", @@ -54,7 +54,7 @@ dependencies = [ "actix-utils", "ahash 0.8.3", "base64 0.21.2", - "bitflags 1.3.2", + "bitflags 2.4.0", "brotli", "bytes", "bytestring", @@ -127,7 +127,7 @@ dependencies = [ "futures-util", "mio", "num_cpus", - "socket2", + "socket2 0.4.9", "tokio", "tracing", ] @@ -145,9 +145,9 @@ dependencies = [ [[package]] name = "actix-session" -version = "0.7.2" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43da8b818ae1f11049a4d218975345fe8e56ce5a5f92c11f972abcff5ff80e87" +checksum = "2e6a28f813a6671e1847d005cad0be36ae4d016287690f765c303379837c13d6" dependencies = [ "actix-service", "actix-utils", @@ -172,9 +172,9 @@ dependencies = [ [[package]] name = "actix-web" -version = "4.3.1" +version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd3cb42f9566ab176e1ef0b8b3a896529062b4efc6be0123046095914c4c1c96" +checksum = "0e4a5b5e29603ca8c94a77c65cf874718ceb60292c5a5c3e5f4ace041af462b9" dependencies = [ "actix-codec", "actix-http", @@ -185,7 +185,7 @@ dependencies = [ "actix-service", "actix-utils", "actix-web-codegen", - "ahash 0.7.6", + "ahash 0.8.3", "bytes", "bytestring", "cfg-if", @@ -194,7 +194,6 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "http", "itoa", "language-tags", "log", @@ -206,7 +205,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "smallvec", - "socket2", + "socket2 0.5.4", "time 0.3.22", "url", ] @@ -246,7 +245,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" dependencies = [ "cfg-if", - "cipher", + "cipher 0.4.4", "cpufeatures", ] @@ -258,7 +257,7 @@ checksum = "209b47e8954a928e1d72e86eca7000ebb6655fe1436d33eefc2201cad027e237" dependencies = [ "aead", "aes", - "cipher", + "cipher 0.4.4", "ctr", "ghash", "subtle", @@ -466,6 +465,15 @@ dependencies = [ "wyz", ] +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -475,6 +483,17 @@ dependencies = [ "generic-array", ] +[[package]] +name = "blowfish" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32fa6a061124e37baba002e496d203e23ba3d7b73750be82dbfbc92913048a5b" +dependencies = [ + "byteorder", + "cipher 0.2.5", + "opaque-debug", +] + [[package]] name = "borsh" version = "0.10.3" @@ -685,6 +704,15 @@ dependencies = [ "half", ] +[[package]] +name = "cipher" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f8e7987cbd042a63249497f41aed09f8e65add917ea6566effbc56578d6801" +dependencies = [ + "generic-array", +] + [[package]] name = "cipher" version = "0.4.4" @@ -786,10 +814,10 @@ dependencies = [ "aes-gcm", "base64 0.20.0", "hkdf", - "hmac", + "hmac 0.12.1", "percent-encoding", "rand", - "sha2", + "sha2 0.10.7", "subtle", "time 0.3.22", "version_check", @@ -864,13 +892,23 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-mac" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bff07008ec701e8028e2ceb8f83f0e4274ee62bd2dbdc4fefff2e9a91824081a" +dependencies = [ + "generic-array", + "subtle", +] + [[package]] name = "ctr" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" dependencies = [ - "cipher", + "cipher 0.4.4", ] [[package]] @@ -943,13 +981,22 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", + "block-buffer 0.10.4", "const-oid", "crypto-common", "subtle", @@ -1342,7 +1389,17 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437" dependencies = [ - "hmac", + "hmac 0.12.1", +] + +[[package]] +name = "hmac" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15" +dependencies = [ + "crypto-mac", + "digest 0.9.0", ] [[package]] @@ -1351,7 +1408,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest", + "digest 0.10.7", ] [[package]] @@ -1429,7 +1486,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2", + "socket2 0.4.9", "tokio", "tower-service", "tracing", @@ -1906,13 +1963,24 @@ dependencies = [ "hashbrown 0.14.0", ] +[[package]] +name = "md-5" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5a279bb9607f9f53c22d496eade00d138d1bdcccd07d74650387cf94942a15" +dependencies = [ + "block-buffer 0.9.0", + "digest 0.9.0", + "opaque-debug", +] + [[package]] name = "md-5" version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6365506850d44bff6e2fbcb5176cf63650e48bd45ef2fe2665ae1570e0f4b9ca" dependencies = [ - "digest", + "digest 0.10.7", ] [[package]] @@ -2162,7 +2230,7 @@ checksum = "a01f71cb40bd8bb94232df14b946909e14660e33fc05db3e50ae2a82d7ea0ca0" dependencies = [ "once_cell", "pest", - "sha2", + "sha2 0.10.7", ] [[package]] @@ -2338,6 +2406,21 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "pwhash" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "419a3ad8fa9f9d445e69d9b185a24878ae6e6f55c96e4512f4a0e28cd3bc5c56" +dependencies = [ + "blowfish", + "byteorder", + "hmac 0.10.1", + "md-5 0.9.1", + "rand", + "sha-1", + "sha2 0.9.9", +] + [[package]] name = "quote" version = "1.0.28" @@ -2474,11 +2557,13 @@ dependencies = [ "cfg-if", "chrono", "console_error_panic_hook", + "futures-util", "lazy_static", "leptos", "leptos_actix", "leptos_meta", "leptos_router", + "pwhash", "rust_decimal", "serde", "sqlx", @@ -2550,7 +2635,7 @@ checksum = "6ab43bb47d23c1a631b4b680199a45255dce26fa9ab2fa902581f624ff13e6a8" dependencies = [ "byteorder", "const-oid", - "digest", + "digest 0.10.7", "num-bigint-dig", "num-integer", "num-iter", @@ -2846,6 +2931,19 @@ dependencies = [ "syn 2.0.28", ] +[[package]] +name = "sha-1" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + [[package]] name = "sha1" version = "0.10.5" @@ -2854,7 +2952,20 @@ checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" dependencies = [ "cfg-if", "cpufeatures", - "digest", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", ] [[package]] @@ -2865,7 +2976,7 @@ checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" dependencies = [ "cfg-if", "cpufeatures", - "digest", + "digest 0.10.7", ] [[package]] @@ -2883,7 +2994,7 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500" dependencies = [ - "digest", + "digest 0.10.7", "rand_core", ] @@ -2928,6 +3039,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "socket2" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e" +dependencies = [ + "libc", + "windows-sys", +] + [[package]] name = "spin" version = "0.5.2" @@ -3011,7 +3132,7 @@ dependencies = [ "rustls-pemfile", "serde", "serde_json", - "sha2", + "sha2 0.10.7", "smallvec", "sqlformat", "thiserror", @@ -3050,7 +3171,7 @@ dependencies = [ "quote", "serde", "serde_json", - "sha2", + "sha2 0.10.7", "sqlx-core", "sqlx-mysql", "sqlx-postgres", @@ -3074,7 +3195,7 @@ dependencies = [ "bytes", "chrono", "crc", - "digest", + "digest 0.10.7", "dotenvy", "either", "futures-channel", @@ -3084,10 +3205,10 @@ dependencies = [ "generic-array", "hex", "hkdf", - "hmac", + "hmac 0.12.1", "itoa", "log", - "md-5", + "md-5 0.10.5", "memchr", "once_cell", "percent-encoding", @@ -3096,7 +3217,7 @@ dependencies = [ "rust_decimal", "serde", "sha1", - "sha2", + "sha2 0.10.7", "smallvec", "sqlx-core", "stringprep", @@ -3125,11 +3246,11 @@ dependencies = [ "futures-util", "hex", "hkdf", - "hmac", + "hmac 0.12.1", "home", "itoa", "log", - "md-5", + "md-5 0.10.5", "memchr", "num-bigint", "once_cell", @@ -3138,7 +3259,7 @@ dependencies = [ "serde", "serde_json", "sha1", - "sha2", + "sha2 0.10.7", "smallvec", "sqlx-core", "stringprep", @@ -3188,9 +3309,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "subtle" -version = "2.5.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" @@ -3331,7 +3452,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.4.9", "windows-sys", ] diff --git a/Cargo.toml b/Cargo.toml index 8197603..a3dcd2c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,9 +7,9 @@ edition = "2021" crate-type = ["cdylib", "rlib"] [dependencies] -actix-files = { version = "0.6", optional = true } -actix-web = { version = "4", optional = true, features = ["macros"] } -actix-session = { version = "0.7.2", optional = true, features = ["cookie-session"] } +actix-files = { version = "0.6.2", optional = true } +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" } @@ -25,6 +25,8 @@ sqlx = { version = "0.7.1", optional = true, features = ["runtime-tokio-rustls", rust_decimal = "1.31.0" uuid = {version = "1.4.1", features = ["v4"]} validator = {version = "0.16.1", features = ["derive"]} +pwhash = "1.0.0" +futures-util = "0.3.28" [features] csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"] diff --git a/src/app.rs b/src/app.rs index 2b18bf0..0552ba5 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,9 +1,13 @@ -use crate::locales::{init_locales, trl}; +use crate::locales::init_locales; use crate::pages::home_page::HomePage; use crate::pages::settings::Settings; use leptos::*; use leptos_meta::*; use leptos_router::*; +use crate::components::admin_portal::AdminPortal; +use crate::components::header::Header; +use crate::pages::login::Login; +use crate::pages::public::Public; #[component] pub fn App() -> impl IntoView { @@ -12,210 +16,25 @@ pub fn App() -> impl IntoView { init_locales(); view! { - - - +
- // - - - - - // - - - // - - - - - // - - -
-
- // - - - // -
- // - - - - // -
- // -
- -
- - }/> - }/> - -
-
-
-
-
-
-
- - /* - // injects a stylesheet into the document - // id=leptos means cargo-leptos will hot-reload this stylesheet - - - - - // sets the document title - - - <div class="topbar"> - <img src="/logo.png" width=40 height=40/><h1>"Rezervator admin"</h1> - </div> - - <div class="main"> - <div class="sidebar"> - <p></p> - <a class="active" href="#home">"Dashboard"</a> - <a href="#news">"Opening hours"</a> - <a href="#contact">"Places"</a> - <a href="#about">"About"</a> - </div> - - // content for this welcome page - <div class="content"> <Router> <main> <Routes> - <Route path="" view=|| view! { <HomePage/> }/> + <Route path="" view=|| view! { <Public/> }/> + <Route path="login" view=|| view! { <Login/> }/> + <Route path="admin" view=|| view! { + <AdminPortal> + <HomePage/> + </AdminPortal> + }/> + <Route path="admin/settings" view=|| view! { + <AdminPortal> + <Settings/> + </AdminPortal> + }/> </Routes> </main> </Router> - </div> - </div>*/ } } diff --git a/src/backend/auth_middleware.rs b/src/backend/auth_middleware.rs new file mode 100644 index 0000000..416291c --- /dev/null +++ b/src/backend/auth_middleware.rs @@ -0,0 +1,71 @@ +use cfg_if::cfg_if; +cfg_if! { if #[cfg(feature = "ssr")] { + + use std::future::{Ready, ready}; + use actix_session::SessionExt; + use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform, forward_ready}; + use actix_web::Error; + use actix_web::HttpResponse; + use actix_web::body::EitherBody; + use futures_util::future::LocalBoxFuture; + use actix_web::http::header::LOCATION; + use crate::backend::data::User; + + pub struct Authentication; + + impl<S, B> Transform<S, ServiceRequest> for Authentication + where + S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>, + S::Future: 'static, + B: 'static, + { + type Response = ServiceResponse<EitherBody<B>>; + type Error = Error; + type Transform = AuthenticationMiddleware<S>; + type InitError = (); + type Future = Ready<Result<Self::Transform, Self::InitError>>; + + fn new_transform(&self, service: S) -> Self::Future { + ready(Ok(AuthenticationMiddleware { service })) + } + } + pub struct AuthenticationMiddleware<S> { + service: S, + } + + impl<S, B> Service<ServiceRequest> for AuthenticationMiddleware<S> + where + S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>, + S::Future: 'static, + B: 'static, + { + type Response = ServiceResponse<EitherBody<B>>; + type Error = Error; + type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>; + + forward_ready!(service); + + fn call(&self, req: ServiceRequest) -> Self::Future { + let session = req.get_session(); + let authenticate_pass = !req.path().starts_with("/admin") || + session.get::<User>("user").unwrap_or(None).is_some(); + + if authenticate_pass { + let res = self.service.call(req); + + Box::pin(async move { + res.await.map(ServiceResponse::map_into_left_body) + }) + } else { + let (request, _pl) = req.into_parts(); + + let response = HttpResponse::Found() + .insert_header((LOCATION, "/login")) + .finish() + .map_into_right_body(); + + Box::pin(async { Ok(ServiceResponse::new(request, response)) }) + } + } + } +}} diff --git a/src/backend/company.rs b/src/backend/company.rs index a17c3e3..08d4b8d 100644 --- a/src/backend/company.rs +++ b/src/backend/company.rs @@ -1,11 +1,14 @@ -use crate::backend::data::Company; use leptos::*; +use crate::backend::data::{ApiResponse, Company}; #[server(GetCompany, "/api", "Url", "get_company")] -pub async fn get_company() -> Result<Company, ServerFnError> { +pub async fn get_company() -> Result<ApiResponse<Company>, ServerFnError> { use crate::backend::AppData; use actix_web::web::Data; use leptos_actix::extract; + use crate::perm_check; + + perm_check!(is_logged_in); let pool = extract(|data: Data<AppData>| async move { data.db_pool().clone() }).await?; @@ -13,14 +16,17 @@ pub async fn get_company() -> Result<Company, ServerFnError> { .fetch_one(&pool) .await?; - Ok(cmp) + Ok(ApiResponse::Data(cmp)) } #[server(UpdateCompany, "/api", "Url", "update_company")] -pub async fn update_company(company: Company) -> Result<(), ServerFnError> { +pub async fn update_company(company: Company) -> Result<ApiResponse<()>, ServerFnError> { use crate::backend::AppData; use actix_web::web::Data; use leptos_actix::extract; + use crate::perm_check; + + perm_check!(is_admin); let pool = extract(|data: Data<AppData>| async move { data.db_pool().clone() }).await?; @@ -36,5 +42,5 @@ pub async fn update_company(company: Company) -> Result<(), ServerFnError> { .execute(&pool) .await?; - Ok(()) + Ok(ApiResponse::Data(())) } diff --git a/src/backend/data.rs b/src/backend/data.rs index 73fc0c3..280c64c 100644 --- a/src/backend/data.rs +++ b/src/backend/data.rs @@ -1,9 +1,15 @@ -use chrono::{NaiveDate, NaiveTime, Weekday}; -use rust_decimal::Decimal; +//use chrono::{NaiveDate, NaiveTime, Weekday}; +//use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; -use uuid::Uuid; +//use uuid::Uuid; use validator::Validate; +#[derive(Serialize, Deserialize, Clone, Debug)] +pub enum ApiResponse<T> { + Data(T), + Error(String) +} + #[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Validate, Default)] #[cfg_attr(feature = "ssr", derive(sqlx::FromRow))] pub struct Company { @@ -24,17 +30,25 @@ impl Company { } } +#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Validate, Default)] +#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))] pub struct User { - id: u16, - login: String, - password: String, - full_name: String, - email: String, - admin: bool, - get_emails: bool, + id: i32, + pub login: String, + pub password: String, + pub full_name: Option<String>, + pub email: Option<String>, + pub admin: bool, + pub get_emails: bool, +} + +impl User { + pub fn id(&self) -> i32 { + self.id + } } -pub struct Property { +/*pub struct Property { id: u16, name: String, description: String, @@ -92,4 +106,4 @@ pub struct ReservationSum { customer: Customer, price: Decimal, state: ReservationState, -} \ No newline at end of file +}*/ diff --git a/src/backend/mod.rs b/src/backend/mod.rs index ea4d85e..a5e50de 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -2,6 +2,24 @@ use cfg_if::cfg_if; pub mod data; pub mod company; +pub mod user; +pub mod auth_middleware; + +#[macro_export] +macro_rules! perm_check { + ($check:ident) => { + use crate::backend::user::$check; + use actix_web::http::StatusCode; + use leptos_actix::ResponseOptions; + + if !$check().await { + let response = expect_context::<ResponseOptions>(); + response.set_status(StatusCode::FORBIDDEN); + + return Ok(ApiResponse::Error("Forbidden".to_string())) + } + } +} cfg_if!{ if #[cfg(feature = "ssr")] { diff --git a/src/backend/user.rs b/src/backend/user.rs new file mode 100644 index 0000000..61f552f --- /dev/null +++ b/src/backend/user.rs @@ -0,0 +1,102 @@ +use cfg_if::cfg_if; +use leptos::*; + +cfg_if! { if #[cfg(feature = "ssr")] { + use sqlx::{query_as, Error, PgPool, query}; + use actix_session::*; + use leptos_actix::{extract, redirect}; + use crate::backend::data::User; + + pub async fn has_admin_user(pool: &PgPool) -> Result<bool, Error> { + let count: (i64,) = query_as(r#"SELECT COUNT(id) FROM "user" WHERE admin = $1"#) + .bind(true) + .fetch_one(pool) + .await?; + + Ok(count.0 > 0) + } + + pub async fn create_admin(pool: &PgPool) -> Result<(), Error> { + if !has_admin_user(pool).await? { + let pwd = pwhash::bcrypt::hash("admin"); + query(r#"INSERT INTO "user"(login, password, full_name, admin) VALUES($1, $2, $3, $4)"#) + .bind("admin") + .bind(pwd.unwrap()) + .bind("Admin User") + .bind(true) + .execute(pool).await?; + } + + Ok(()) + } + + pub async fn user_from_login(pool: &PgPool, login: &str) -> Result<User, Error> { + let usr = query_as::<_, User>(r#"SELECT * FROM "user" WHERE login=$1"#) + .bind(login) + .fetch_one(pool).await?; + Ok(usr) + } + + pub async fn logged_in_user() -> Option<User> { + extract(|session: Session| async move { + session.get::<User>("user").unwrap_or(None) + }).await.unwrap_or(None) + } + + pub async fn is_logged_in() -> bool { + logged_in_user().await.is_some() + } + + pub async fn is_admin() -> bool { + if let Some(user) = logged_in_user().await { + user.admin + } else { + false + } + } +}} + +#[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; + + let pool = extract(|data: Data<AppData>| async move { data.db_pool().clone() }).await?; + + let user = user_from_login(&pool, &username).await?; + + if pwhash::bcrypt::verify(password, &user.password) { + extract(|session: Session| async move { + let _ = session.insert("user", user); + }) + .await?; + + redirect("/admin"); + return Ok(()); + } + + Err(ServerFnError::ServerError("Bad login".to_string())) +} + +#[server] +pub async fn logout() -> Result<(), ServerFnError> { + extract(|session: Session| async move { + session.clear(); + }).await?; + + redirect("/login"); + + Ok(()) +} + +#[server] +pub async fn auth_check() -> Result<bool, ServerFnError> { + Ok(is_logged_in().await) +} + +#[server] +pub async fn admin_check() -> Result<bool, ServerFnError> { + Ok(is_admin().await) +} \ No newline at end of file diff --git a/src/components/admin_portal.rs b/src/components/admin_portal.rs new file mode 100644 index 0000000..494c1a5 --- /dev/null +++ b/src/components/admin_portal.rs @@ -0,0 +1,140 @@ +use leptos::*; +use crate::locales::trl; + +#[component] +pub fn AdminPortal(children: Children) -> impl IntoView { + view! { + <div class="layout-wrapper layout-content-navbar"> + <div class="layout-container"> + //<!-- Menu --> + + <aside id="layout-menu" class="layout-menu menu-vertical menu bg-menu-theme"> + <div class="app-brand demo"> + <a href="javascript:void(0);" class="layout-menu-toggle menu-link text-large ms-auto d-block d-xl-none"> + <i class="bx bx-chevron-left bx-sm align-middle"></i> + </a> + </div> + <div class="menu-inner-shadow"></div> + + <ul class="menu-inner py-1"> + //<!-- Dashboard --> + <li class="menu-item"> + <a href="/admin" class="menu-link"> + <i class="menu-icon tf-icons bx bx-home-circle"></i> + <div data-i18n="Analytics">{trl("Dashboard")}</div> + </a> + </li> + <li class="menu-item"> + <a href="/" class="menu-link"> + <i class="menu-icon tf-icons bx bx-time"></i> + <div data-i18n="Analytics">"Opening hours"</div> + </a> + </li> + <li class="menu-item"> + <a href="/" class="menu-link"> + <i class="menu-icon tf-icons bx bx-layer"></i> + <div data-i18n="Analytics">"Places"</div> + </a> + </li> + <li class="menu-item"> + <a href="/" class="menu-link"> + <i class="menu-icon tf-icons bx bx-info-circle"></i> + <div data-i18n="Analytics">"About"</div> + </a> + </li> + </ul> + </aside> + //<!-- Layout container --> + <div class="layout-page"> + //<!-- Navbar --> + + <nav + class="layout-navbar container-xxl navbar navbar-expand-xl navbar-detached align-items-center bg-navbar-theme" + id="layout-navbar"> + <div class="layout-menu-toggle navbar-nav align-items-xl-center me-3 me-xl-0 d-xl-none"> + <a class="nav-item nav-link px-0 me-xl-4" href="javascript:void(0)"> + <i class="bx bx-menu bx-sm"></i> + </a> + </div> + + <div class="navbar-nav-right d-flex align-items-center" id="navbar-collapse"> + //<!-- Search --> + <div class="navbar-nav align-items-center ms-auto mt-auto"> + <div class="nav-item d-flex align-items-center mt-auto"> + <h4 class="mt-3"><i class="bx bx-desktop fs-4 lh-0"></i>" Admin portal"</h4> + </div> + </div> + //<!-- /Search --> + <ul class="navbar-nav flex-row align-items-center ms-auto"> + <li class="nav-item navbar-dropdown dropdown-user dropdown"> + <a class="nav-link dropdown-toggle hide-arrow" href="/admin/settings" data-bs-toggle="dropdown"> + <i class="bx bx-cog fs-3 lh-0"></i> + </a> + </li> + //<!-- User --> + <li class="nav-item navbar-dropdown dropdown-user dropdown"> + <a class="nav-link dropdown-toggle hide-arrow" href="#" data-bs-toggle="dropdown"> + //<div class="avatar avatar-online"> + // <img src="/img/avatars/1.png" alt class="w-px-40 h-auto rounded-circle" /> + //</div> + <i class="bx bx-user fs-3 lh-0"></i> + </a> + <ul class="dropdown-menu dropdown-menu-end"> + <li> + <a class="dropdown-item" href="#"> + <div class="d-flex"> + <div class="flex-shrink-0 me-3"> + <div class="avatar avatar-online"> + <img src="/img/avatars/1.png" alt class="w-px-40 h-auto rounded-circle" /> + </div> + </div> + <div class="flex-grow-1"> + <span class="fw-semibold d-block">"John Doe"</span> + <small class="text-muted">"Admin"</small> + </div> + </div> + </a> + </li> + <li> + <div class="dropdown-divider"></div> + </li> + <li> + <a class="dropdown-item" href="#"> + <i class="bx bx-user me-2"></i> + <span class="align-middle">"My Profile"</span> + </a> + </li> + <li> + <a class="dropdown-item" href="#"> + <i class="bx bx-cog me-2"></i> + <span class="align-middle">"Settings"</span> + </a> + </li> + <li> + <div class="dropdown-divider"></div> + </li> + <li> + <a class="dropdown-item" href="auth-login-basic.html"> + <i class="bx bx-power-off me-2"></i> + <span class="align-middle">"Log Out"</span> + </a> + </li> + </ul> + </li> + //<!--/ User --> + </ul> + </div> + </nav> + + //<!-- Content wrapper --> + <div class="content-wrapper"> + //<!-- Content --> + <div class="container-xxl flex-grow-1 container-p-y"> + {children()} + </div> + </div> + </div> + </div> + </div> + } +} \ No newline at end of file diff --git a/src/components/header.rs b/src/components/header.rs new file mode 100644 index 0000000..d6676ac --- /dev/null +++ b/src/components/header.rs @@ -0,0 +1,38 @@ +use leptos::*; +use leptos_meta::*; + +#[component] +pub fn Header() -> impl IntoView { + view! { + <Html + lang="cz" + dir="ltr" + attributes=AdditionalAttributes::from(vec![ + ("data-theme", "theme-default"), + ("class", "light-style layout-menu-fixed"), + ("data-template", "vertical-menu-template-free"), + ("data-assets-path", "/")]) + /> + <Meta charset="utf-8"/> + <Meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0"/> + + //<!-- Fonts --> + <Link rel="preconnect" href="https://fonts.googleapis.com" /> + <Link rel="preconnect" href="https://fonts.gstatic.com" /> + <Link + href="https://fonts.googleapis.com/css2?family=Public+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,500;1,600;1,700&display=swap" + rel="stylesheet" + /> + + //<!-- Icons. Uncomment required icon fonts --> + <Link rel="stylesheet" href="/vendor/fonts/boxicons.css" /> + + //<!-- Core CSS --> + <Link rel="stylesheet" href="/vendor/css/core.css" /> + <Link rel="stylesheet" href="/vendor/css/theme-default.css" /> + <Link rel="stylesheet" href="/css/demo.css" /> + + //<!-- Vendors CSS --> + <Link rel="stylesheet" href="/vendor/libs/perfect-scrollbar/perfect-scrollbar.css" /> + } +} \ No newline at end of file diff --git a/src/components/mod.rs b/src/components/mod.rs index 8c68b2a..d9381a8 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -1,4 +1,6 @@ pub mod modal_box; pub mod server_err; pub mod validation_err; +pub mod header; +pub mod admin_portal; diff --git a/src/components/server_err.rs b/src/components/server_err.rs index e4476b2..c4d9794 100644 --- a/src/components/server_err.rs +++ b/src/components/server_err.rs @@ -1,9 +1,10 @@ use crate::components::modal_box::DialogOpener; use leptos::*; +use crate::backend::data::ApiResponse; #[component] pub fn ServerErr( - result: RwSignal<Option<Result<(), ServerFnError>>>, + result: RwSignal<Option<Result<ApiResponse<()>, ServerFnError>>>, opener: DialogOpener, ) -> impl IntoView { view! {{move || { diff --git a/src/lib.rs b/src/lib.rs index 5bd1ebf..5f47edd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,4 @@ pub mod app; -pub mod server_fn; pub mod locales; pub mod backend; mod pages; diff --git a/src/locales/mod.rs b/src/locales/mod.rs index 8e6b2f4..d0eacc3 100644 --- a/src/locales/mod.rs +++ b/src/locales/mod.rs @@ -1,5 +1,3 @@ -use std::ops::Deref; -use leptos::*; use crate::locales::catalogues::get_dictionary; mod catalogues; @@ -10,6 +8,7 @@ pub struct Locales(pub Vec<Option<String>>); pub fn init_locales() { #[cfg(not(feature = "ssr"))] { + use leptos::*; let loc = Locales( window() .navigator() diff --git a/src/main.rs b/src/main.rs index 6bcca17..a00fd6e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,6 +13,7 @@ async fn main() -> std::io::Result<()> { use actix_web::web::Data; use sqlx::migrate; use sqlx::postgres::PgPoolOptions; + use rezervator::backend::auth_middleware::Authentication; let conf = get_configuration(None).await.unwrap(); let addr = conf.leptos_options.site_addr; @@ -32,6 +33,7 @@ async fn main() -> std::io::Result<()> { App::new() .app_data(Data::new(AppData::new(pool.clone()))) + .wrap(Authentication) .wrap(SessionMiddleware::new( CookieSessionStore::default(), key.clone() diff --git a/src/pages/company_edit.rs b/src/pages/company_edit.rs index b07c6d8..f68ee9f 100644 --- a/src/pages/company_edit.rs +++ b/src/pages/company_edit.rs @@ -1,7 +1,7 @@ use crate::backend::data::Company; use crate::backend::company::UpdateCompany; use crate::components::modal_box::{ - DialogOpener, DlgNotLoaded, ModalBody, ModalDialog, ModalFooter, + DialogOpener, ModalBody, ModalDialog, ModalFooter, }; use crate::components::server_err::ServerErr; use crate::locales::trl; diff --git a/src/pages/company_info.rs b/src/pages/company_info.rs index 91f87f0..c778adf 100644 --- a/src/pages/company_info.rs +++ b/src/pages/company_info.rs @@ -1,7 +1,6 @@ use leptos::*; -use serde::de::Unexpected::Option; use crate::backend::company::get_company; -use crate::backend::data::Company; +use crate::backend::data::{ApiResponse, Company}; use crate::components::modal_box::DialogOpener; use crate::locales::trl; use crate::pages::company_edit::CompanyEdit; @@ -20,18 +19,23 @@ pub fn CompanyInfo() -> impl IntoView { <p class="card-text"> <Transition fallback=move || view! {<p>{trl("Loading...")}</p> }> {move || { - company.read().map(|c| match c { - Err(e) => {view! {<p>{trl("Error loading data")}</p> - <p>{e.to_string()}</p> + company.get().map(|c| match c { + Err(e) => {view! {<div><p>{trl("Error loading data")}</p> + <p>{e.to_string()}</p></div> }} Ok(c) => { - set_cmp.update(|cmp| *cmp = c.clone()); - view! { - <p><b>{c.name}</b></p> - <p>{c.street}" "{c.house_number}<br/> - {c.zip_code}" "{c.city} - </p> - }} + match c { + ApiResponse::Data(d) => {set_cmp.update(|cmp| *cmp = d.clone()); + view! {<div> + <p><b>{d.name}</b></p> + <p>{d.street}" "{d.house_number}<br/> + {d.zip_code}" "{d.city} + </p></div>} + } + ApiResponse::Error(_) => {view! {<div><p>{trl("Error loading data")}</p></div>}} + } + + } }) }} </Transition> diff --git a/src/pages/home_page.rs b/src/pages/home_page.rs index 57d7a1a..74a0157 100644 --- a/src/pages/home_page.rs +++ b/src/pages/home_page.rs @@ -1,6 +1,5 @@ use leptos::*; use crate::components::modal_box::{DialogOpener, ModalDialog, ModalBody, ModalFooter}; -use crate::server_fn::*; use crate::locales::trl; /// Renders the home page of your application. @@ -65,16 +64,6 @@ pub fn HomePage() -> impl IntoView { <h1>"Welcome to Leptos!"</h1> <button on:click=on_click>"Click Me: " {count}</button> <button on:click=move |_| dialog.show()>"Dialog"</button> - <button on:click=move |_| { - spawn_local(async move { - set_session().await; - }); - }>"Session"</button> - <button on:click=move |_| { - spawn_local(async move { - get_session().await; - }); - }>"Session get"</button> <p>{trl("testik!")}</p> } } \ No newline at end of file diff --git a/src/pages/login.rs b/src/pages/login.rs new file mode 100644 index 0000000..8389713 --- /dev/null +++ b/src/pages/login.rs @@ -0,0 +1,74 @@ +use leptos::*; +use leptos_meta::*; +use leptos_router::ActionForm; +use crate::backend::user::Login; + +#[component] +pub fn Login() -> impl IntoView { + let login = create_server_action::<Login>(); + + view! { + <Link rel="stylesheet" href="/vendor/css/pages/page-auth.css" /> + <div class="authentication-wrapper authentication-basic container-p-y"> + <div class="authentication-inner"> + + <div class="card"> + <div class="card-body"> + //<!-- Logo --> + <div class="app-brand justify-content-center"> + <a href="index.html" class="app-brand-link gap-2"> + <span class="app-brand-logo demo"> + + </span> + //<span class="app-brand-text demo text-body fw-bolder">Sneat</span> + </a> + </div> + //<!-- /Logo --> + <h4 class="mb-2">"Welcome to Rezervator 👋"</h4> + <p class="mb-4">"Please sign-in to your account and start the adventure"</p> + + <ActionForm action=login> + <div class="mb-3"> + <label for="username" class="form-label">"Username"</label> + <input + type="text" + class="form-control" + id="username" + name="username" + placeholder="Enter your username" + autofocus + /> + </div> + <div class="mb-3 form-password-toggle"> + <div class="d-flex justify-content-between"> + <label class="form-label" for="password">"Password"</label> + <a href="auth-forgot-password-basic.html"> + <small>"Forgot Password?"</small> + </a> + </div> + <div class="input-group input-group-merge"> + <input + type="password" + id="password" + class="form-control" + name="password" + aria-describedby="password" + /> + <span class="input-group-text cursor-pointer"><i class="bx bx-hide"></i></span> + </div> + </div> + <div class="mb-3"> + + </div> + <div class="mb-3"> + <button class="btn btn-primary d-grid w-100" type="submit">"Sign in"</button> + </div> + </ActionForm> + + + </div> + </div> + </div> + </div> + } +} \ No newline at end of file diff --git a/src/pages/mod.rs b/src/pages/mod.rs index 39f552d..c352258 100644 --- a/src/pages/mod.rs +++ b/src/pages/mod.rs @@ -2,3 +2,5 @@ pub mod home_page; pub mod settings; pub mod company_info; mod company_edit; +pub mod login; +pub mod public; diff --git a/src/pages/public.rs b/src/pages/public.rs new file mode 100644 index 0000000..124807d --- /dev/null +++ b/src/pages/public.rs @@ -0,0 +1,8 @@ +use leptos::*; + +#[component] +pub fn Public() -> impl IntoView { + view! { + <div>"public"</div> + } +} \ No newline at end of file diff --git a/src/server_fn.rs b/src/server_fn.rs deleted file mode 100644 index f8c8dc9..0000000 --- a/src/server_fn.rs +++ /dev/null @@ -1,33 +0,0 @@ -use leptos::*; - -#[server(SetSession, "/api", "Url", "set_session")] -pub async fn set_session() -> Result<(), ServerFnError> { - use leptos_actix::extract; - use actix_session::*; - use actix_web::web::Data; - use leptos::logging::log; - //use crate::DataPok; - - extract(|session: Session| async move { - log!("extract"); - let pok = session.insert("user", "uzivatel"); - log!("{pok:?}"); - }).await - - //Ok(()) -} - -#[server(GetSession, "/api")] -pub async fn get_session() -> Result<(), ServerFnError> { - use leptos_actix::extract; - use actix_session::*; - use leptos::logging::log; - - extract(|session: Session| async move { - log!("extract"); - let pok = session.get::<String>("user"); - log!("{pok:?}"); - }).await - - //Ok(()) -} \ No newline at end of file diff --git a/src/validator.rs b/src/validator.rs index 91787fa..9ea0e90 100644 --- a/src/validator.rs +++ b/src/validator.rs @@ -3,8 +3,8 @@ use validator::Validate; #[derive(Copy, Clone)] pub struct Validator { - message: ReadSignal<Option<String>>, - set_message: WriteSignal<Option<String>>, + //message: ReadSignal<Option<String>>, + //set_message: WriteSignal<Option<String>>, valid: ReadSignal<bool>, set_valid: WriteSignal<bool>, messages: ReadSignal<Option<Vec<String>>>, @@ -14,11 +14,11 @@ pub struct Validator { impl Validator { pub fn new() -> Self { let (valid, set_valid) = create_signal(true); - let (message, set_message) = create_signal(None); + //let (message, set_message) = create_signal(None); let (messages, set_messages) = create_signal(None); Self { - message, - set_message, + //message, + //set_message, valid, set_valid, messages, @@ -29,7 +29,7 @@ impl Validator { pub fn check(&self, entity: &impl Validate, ev: &web_sys::Event) { if let Err(val_err) = entity.validate() { ev.prevent_default(); - self.set_message.update(|m| *m = Some(val_err.to_string().clone())); + //self.set_message.update(|m| *m = Some(val_err.to_string().clone())); self.set_messages.update(|m| *m = { let mut out: Vec<String> = vec![]; val_err.field_errors().drain().for_each(|e| { @@ -51,9 +51,9 @@ impl Validator { self.valid.get() } - pub fn message(&self) -> Option<String> { + /*pub fn message(&self) -> Option<String> { self.message.get() - } + }*/ pub fn messages(&self) -> Option<Vec<String>> { self.messages.get()