From 2122b3a4f7f46f5a898bea02cb26343e193b9aba Mon Sep 17 00:00:00 2001 From: Josef Rokos Date: Wed, 27 Sep 2023 11:36:52 +0200 Subject: [PATCH] Implemented user menu. Replaced favicon with app logo. --- assets/favicon.ico | Bin 15406 -> 266174 bytes src/backend/data.rs | 54 ++++++++++++++++++ src/backend/mod.rs | 17 ++++++ src/backend/user.rs | 77 +++++++++++++++++++++++--- src/components/admin_portal.rs | 58 ++++++------------- src/components/header.rs | 1 + src/components/mod.rs | 1 + src/components/server_err.rs | 25 ++++++++- src/components/user_menu.rs | 92 +++++++++++++++++++++++++++++++ src/components/validation_err.rs | 2 +- src/locales/catalogues.rs | 2 + src/pages/change_pwd.rs | 90 ++++++++++++++++++++++++++++++ src/pages/company_info.rs | 2 +- src/pages/login.rs | 24 ++++---- src/pages/mod.rs | 2 + src/pages/profile_edit.rs | 83 ++++++++++++++++++++++++++++ 16 files changed, 466 insertions(+), 64 deletions(-) create mode 100644 src/components/user_menu.rs create mode 100644 src/pages/change_pwd.rs create mode 100644 src/pages/profile_edit.rs diff --git a/assets/favicon.ico b/assets/favicon.ico index 2ba8527cb12f5f28f331b8d361eef560492d4c77..f717780ee8680c54edf1be2b4d3ae16941b4f6d4 100644 GIT binary patch literal 266174 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•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 literal 15406 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 k<4FtN!5, + #[validate(email(message = "Enter valid email address"))] pub email: Option, pub admin: bool, pub get_emails: bool, @@ -48,6 +49,59 @@ impl User { } } +#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Validate, Default)] +pub struct UserProfile { + login: String, + full_name: String, + #[validate(email(message = "Enter valid email address"))] + email: String, + get_emails: Option +} + +impl UserProfile { + + pub fn login(&self) -> &str { + &self.login + } + + pub fn full_name(&self) -> &str { + &self.full_name + } + + pub fn email(&self) -> &str { + &self.email + } + + pub fn get_emails(&self) -> bool { + self.get_emails.is_some() + } +} + +#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Validate, Default)] +pub struct PwdChange { + login: String, + old_password: String, + #[validate(length(min = 1, message = "Enter new password"), + must_match(other = "password_ver", message = "Passwords doesn't match"))] + password: String, + password_ver: String +} + +impl PwdChange { + pub fn login(&self) -> &str { + &self.login + } + pub fn old_password(&self) -> &str { + &self.old_password + } + pub fn password(&self) -> &str { + &self.password + } + pub fn password_ver(&self) -> &str { + &self.password_ver + } +} + /*pub struct Property { id: u16, name: String, diff --git a/src/backend/mod.rs b/src/backend/mod.rs index a5e50de..56b20fb 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -21,6 +21,23 @@ macro_rules! perm_check { } } +#[macro_export] +macro_rules! user_check { + ($check:expr) => { + use crate::perm_check; + use crate::backend::user::logged_in_user; + + perm_check!(is_logged_in); + + if logged_in_user().await.unwrap_or(User::default()).login != $check { + let response = expect_context::(); + response.set_status(StatusCode::FORBIDDEN); + + return Ok(ApiResponse::Error("You can change your own profile only".to_string())) + } + } +} + cfg_if!{ if #[cfg(feature = "ssr")] { use sqlx::PgPool; diff --git a/src/backend/user.rs b/src/backend/user.rs index 61f552f..326fddd 100644 --- a/src/backend/user.rs +++ b/src/backend/user.rs @@ -1,11 +1,11 @@ use cfg_if::cfg_if; use leptos::*; +use crate::backend::data::{ApiResponse, PwdChange, User, UserProfile}; 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 { let count: (i64,) = query_as(r#"SELECT COUNT(id) FROM "user" WHERE admin = $1"#) @@ -57,27 +57,32 @@ cfg_if! { if #[cfg(feature = "ssr")] { }} #[server(Login, "/api")] -pub async fn login(username: String, password: String) -> Result<(), ServerFnError> { +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; let pool = extract(|data: Data| async move { data.db_pool().clone() }).await?; - let user = user_from_login(&pool, &username).await?; + let user = user_from_login(&pool, &username).await.unwrap_or(User::default()); - if pwhash::bcrypt::verify(password, &user.password) { + if !user.login.is_empty() && pwhash::bcrypt::verify(password, &user.password) { extract(|session: Session| async move { let _ = session.insert("user", user); }) .await?; redirect("/admin"); - return Ok(()); + return Ok(ApiResponse::Data(())); } - Err(ServerFnError::ServerError("Bad login".to_string())) + let response = expect_context::(); + response.set_status(StatusCode::UNAUTHORIZED); + + return Ok(ApiResponse::Error("Bad username or password".to_string())) } #[server] @@ -87,7 +92,6 @@ pub async fn logout() -> Result<(), ServerFnError> { }).await?; redirect("/login"); - Ok(()) } @@ -99,4 +103,63 @@ pub async fn auth_check() -> Result { #[server] pub async fn admin_check() -> Result { Ok(is_admin().await) +} + +#[server] +pub async fn get_user() -> Result, ServerFnError> { + Ok(logged_in_user().await) +} + +#[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; + + user_check!(user.login()); + + 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"#) + .bind(user.full_name()) + .bind(user.email()) + .bind(user.get_emails()) + .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?; + + Ok(ApiResponse::Data(())) +} + +#[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; + + user_check!(new_pw.login()); + + let pool = extract(|data: Data| async move { data.db_pool().clone() }).await?; + let usr = user_from_login(&pool, new_pw.login()).await?; + + if !pwhash::bcrypt::verify(new_pw.old_password(), &usr.password) { + let response = expect_context::(); + response.set_status(StatusCode::UNAUTHORIZED); + + return Ok(ApiResponse::Error("Invalid old password".to_string())) + } + + sqlx::query(r#"UPDATE "user" SET password = $1 WHERE login = $2"#) + .bind(pwhash::bcrypt::hash(new_pw.password()).unwrap()) + .bind(new_pw.login()) + .execute(&pool) + .await?; + + Ok(ApiResponse::Data(())) } \ No newline at end of file diff --git a/src/components/admin_portal.rs b/src/components/admin_portal.rs index 494c1a5..6da1851 100644 --- a/src/components/admin_portal.rs +++ b/src/components/admin_portal.rs @@ -1,8 +1,18 @@ use leptos::*; +use crate::backend::data::User; +use crate::components::modal_box::DialogOpener; +use crate::components::user_menu::{MenuOpener, UserMenu}; use crate::locales::trl; +use crate::pages::change_pwd::ChangePassword; +use crate::pages::profile_edit::ProfileEdit; #[component] pub fn AdminPortal(children: Children) -> impl IntoView { + let user_menu = MenuOpener::new(); + let (user, set_user) = create_signal(User::default()); + let editor = DialogOpener::new(); + let pw_changer = DialogOpener::new(); + view! {
@@ -13,6 +23,7 @@ pub fn AdminPortal(children: Children) -> impl IntoView { +
@@ -73,53 +84,14 @@ pub fn AdminPortal(children: Children) -> impl IntoView { // // @@ -130,6 +102,8 @@ pub fn AdminPortal(children: Children) -> impl IntoView {
//
+ + {children()}
diff --git a/src/components/header.rs b/src/components/header.rs index d6676ac..86ccd1b 100644 --- a/src/components/header.rs +++ b/src/components/header.rs @@ -13,6 +13,7 @@ pub fn Header() -> impl IntoView { ("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"/> diff --git a/src/components/mod.rs b/src/components/mod.rs index d9381a8..7f3f6f3 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -3,4 +3,5 @@ pub mod server_err; pub mod validation_err; pub mod header; pub mod admin_portal; +pub mod user_menu; diff --git a/src/components/server_err.rs b/src/components/server_err.rs index c4d9794..158b38d 100644 --- a/src/components/server_err.rs +++ b/src/components/server_err.rs @@ -1,6 +1,7 @@ use crate::components::modal_box::DialogOpener; use leptos::*; use crate::backend::data::ApiResponse; +use crate::locales::trl; #[component] pub fn ServerErr( @@ -9,7 +10,27 @@ pub fn ServerErr( ) -> impl IntoView { view! {{move || { if let Some(val) = result.get() { - if let Err(e) = val { + match val { + Ok(resp) => if let ApiResponse::Error(err) = resp { + view! { + <div class="alert alert-danger"> + {trl(&err)} + </div> + } + } + else { + opener.hide(); + view! {<div></div>} + } + Err(e) => { + view! { + <div class="alert alert-danger"> + "Server error: " {e.to_string()} + </div> + } + } + } + /*if let Err(e) = val { view! { <div class="alert alert-danger"> "Server error: " {e.to_string()} @@ -18,7 +39,7 @@ pub fn ServerErr( } else { opener.hide(); view! {<div></div>} - } + }*/ } else { view! {<div></div>} } diff --git a/src/components/user_menu.rs b/src/components/user_menu.rs new file mode 100644 index 0000000..c0780c9 --- /dev/null +++ b/src/components/user_menu.rs @@ -0,0 +1,92 @@ +use leptos::*; +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)] +pub struct MenuOpener { + visible: ReadSignal<bool>, + set_visible: WriteSignal<bool>, +} + +impl MenuOpener { + pub fn new() -> Self { + let (visible, set_visible) = create_signal(false); + MenuOpener { + visible, + set_visible, + } + } + + pub fn visible(&self) -> bool { + self.visible.get() + } + + pub fn toggle(&self) { + let visible = self.visible.get(); + self.set_visible.update(|v| *v = !visible) + } +} + +#[component] +pub fn UserMenu( + opener: MenuOpener, + editor: DialogOpener, + pw_dialog: DialogOpener, + user_profile: WriteSignal<User>) -> impl IntoView { + 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"> + <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> + </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> + </li> + <li> + <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> + </li> + </ul> + } +} \ No newline at end of file diff --git a/src/components/validation_err.rs b/src/components/validation_err.rs index 8e83b49..fc88d92 100644 --- a/src/components/validation_err.rs +++ b/src/components/validation_err.rs @@ -24,7 +24,7 @@ pub fn ValidationErr( } else { view! { <div class="alert alert-danger"> - "Validation error" + {trl("Validation error")} </div> } } diff --git a/src/locales/catalogues.rs b/src/locales/catalogues.rs index 549d56b..fdb57ac 100644 --- a/src/locales/catalogues.rs +++ b/src/locales/catalogues.rs @@ -14,6 +14,8 @@ lazy_static! { ("Save changes", "Uložit změny"), ("Company info", "Organizace"), ("Name cannot be empty", "Jméno nesmí být prázdné"), + ("Invalid old password", "Neplatné staré heslo"), + ("Please sign-in to your account", "Přihlaste se prosím k uživatelskému účtu"), ])), ("sk", HashMap::from( [ ("Dashboard", "Prehlad"), diff --git a/src/pages/change_pwd.rs b/src/pages/change_pwd.rs new file mode 100644 index 0000000..fef25d1 --- /dev/null +++ b/src/pages/change_pwd.rs @@ -0,0 +1,90 @@ +use leptos::*; +use leptos_router::*; +use crate::backend::data::{ApiResponse, User}; +use crate::backend::user::ChangePwd; +use crate::components::modal_box::{DialogOpener, ModalBody, ModalDialog, ModalFooter}; +use crate::components::server_err::ServerErr; +use crate::components::validation_err::ValidationErr; +use crate::locales::trl; +use crate::validator::Validator; + +#[component] +pub fn change_password(user: ReadSignal<User>, opener: DialogOpener) -> impl IntoView { + let change_pwd = create_server_action::<ChangePwd>(); + let upd_val = change_pwd.value(); + let validator = Validator::new(); + let empty = create_rw_signal("".to_string()); + + view! { + {move || { + if let Some(res) = upd_val.get() { + if let Ok(r) = res { + if let ApiResponse::Data(_) = r { empty.update(|e| *e = "".to_string())} + } + } + view! { + <ActionForm + on:submit=move |ev| { + let act = ChangePwd::from_event(&ev); + if !act.is_err() { + validator.check(&act.unwrap().new_pw, &ev); + } + } + action=change_pwd> + <ModalDialog opener=opener title="Change password"> + <ModalBody> + <ServerErr result={upd_val} opener=opener/> + <ValidationErr validator=validator /> + <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> + </ModalBody> + <ModalFooter> + <button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal" + on:click=move |_| {validator.reset(); opener.hide(); empty.update(|e| *e = "".to_string())}> + {trl("Close")} + </button> + <button type="submit" class="btn btn-primary"> + {trl("Save changes")} + </button> + </ModalFooter> + </ModalDialog> + </ActionForm> + } + }} + } +} \ No newline at end of file diff --git a/src/pages/company_info.rs b/src/pages/company_info.rs index c778adf..de41dde 100644 --- a/src/pages/company_info.rs +++ b/src/pages/company_info.rs @@ -8,7 +8,7 @@ use crate::pages::company_edit::CompanyEdit; #[component] pub fn CompanyInfo() -> impl IntoView { let editor = DialogOpener::new(); - let company = create_resource(move|| editor.visible(), move |_| { get_company() }); + let company = create_blocking_resource(move|| editor.visible(), move |_| { get_company() }); let (cmp, set_cmp) = create_signal(Company::default()); view! { diff --git a/src/pages/login.rs b/src/pages/login.rs index 8389713..fe5f5b9 100644 --- a/src/pages/login.rs +++ b/src/pages/login.rs @@ -2,49 +2,51 @@ use leptos::*; use leptos_meta::*; use leptos_router::ActionForm; use crate::backend::user::Login; +use crate::components::modal_box::DialogOpener; +use crate::components::server_err::ServerErr; +use crate::locales::trl; #[component] pub fn Login() -> impl IntoView { let login = create_server_action::<Login>(); - + let login_val = login.value(); 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"> - + <img src="/rezervovator_l.svg" width="200"/> </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> + <p class="mb-4">{trl("Please sign-in to your account")}</p> <ActionForm action=login> + <ServerErr result=login_val opener=DialogOpener::new()/> <div class="mb-3"> - <label for="username" class="form-label">"Username"</label> + <label for="username" class="form-label">{trl("Username")}</label> <input type="text" class="form-control" id="username" name="username" - placeholder="Enter your username" + placeholder={trl("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"> + <label class="form-label" for="password">{trl("Password")}</label> + /*<a href="auth-forgot-password-basic.html"> <small>"Forgot Password?"</small> - </a> + </a>*/ </div> <div class="input-group input-group-merge"> <input @@ -61,7 +63,7 @@ pub fn Login() -> impl IntoView { </div> <div class="mb-3"> - <button class="btn btn-primary d-grid w-100" type="submit">"Sign in"</button> + <button class="btn btn-primary d-grid w-100" type="submit">{trl("Sign in")}</button> </div> </ActionForm> diff --git a/src/pages/mod.rs b/src/pages/mod.rs index c352258..f919cd1 100644 --- a/src/pages/mod.rs +++ b/src/pages/mod.rs @@ -4,3 +4,5 @@ pub mod company_info; mod company_edit; pub mod login; pub mod public; +pub mod profile_edit; +pub mod change_pwd; diff --git a/src/pages/profile_edit.rs b/src/pages/profile_edit.rs new file mode 100644 index 0000000..ffb7413 --- /dev/null +++ b/src/pages/profile_edit.rs @@ -0,0 +1,83 @@ +use leptos::*; +use leptos_router::*; +use crate::backend::data::User; +use crate::backend::user::UpdateProfile; +use crate::components::modal_box::{DialogOpener, ModalBody, ModalDialog, ModalFooter}; +use crate::components::server_err::ServerErr; +use crate::components::validation_err::ValidationErr; +use crate::locales::trl; +use crate::validator::Validator; + +#[component] +pub fn ProfileEdit(user: ReadSignal<User>, opener: DialogOpener) -> impl IntoView { + let update_user = create_server_action::<UpdateProfile>(); + let upd_val = update_user.value(); + let validator = Validator::new(); + + view! { + <ActionForm + on:submit=move |ev| { + let act = UpdateProfile::from_event(&ev); + if !act.is_err() { + validator.check(&act.unwrap().user, &ev); + } + } + action=update_user> + <ModalDialog opener=opener title="Edit profile"> + <ModalBody> + <ServerErr result={upd_val} opener=opener/> + <ValidationErr validator=validator /> + <input type="hidden" 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> + <input + type="text" + id="name" + class="form-control" + placeholder="Enter Full name" + prop:value={move || user.get().full_name} + 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="name" + class="form-control" + placeholder="Enter email" + prop:value={move || user.get().email.unwrap_or("".to_string())} + name="user[email]" + /> + </div> + </div> + <div class="row"> + <div class="col mb-3"> + <input + class="form-check-input" + type="checkbox" + id="getMail" + prop:value={move || if user.get().get_emails {"true"} else {"false"}} + prop:checked={move || user.get().get_emails} + name="user[get_emails]" + /> + <label class="form-check-label" for="getMail">"Get emails"</label> + </div> + </div> + </ModalBody> + <ModalFooter> + <button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal" + on:click=move |_| {validator.reset(); opener.hide()}> + {trl("Close")} + </button> + <button type="submit" class="btn btn-primary"> + {trl("Save changes")} + </button> + </ModalFooter> + </ModalDialog> + </ActionForm> + } +} \ No newline at end of file