User management completed. Leptos upgraded to 0.5.0.

This commit is contained in:
2023-10-09 17:33:20 +02:00
parent a7188e8153
commit e7af2d402d
18 changed files with 697 additions and 334 deletions
+28
View File
@@ -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<MenuOpener>
}
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! {
<Header/>
+13 -1
View File
@@ -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<String>,
password_ver: Option<String>,
full_name: String,
#[validate(email(message = "Enter valid email address"))]
email: String,
get_emails: Option<String>
get_emails: Option<String>,
admin: Option<String>
}
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<String> {
&self.password
}
}
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Validate, Default)]
+9 -1
View File
@@ -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::<ResponseOptions>();
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<PgPool, ServerFnError> {
extract(|data: Data<AppData>| async move { data.db_pool().clone() }).await
}
}
}
+92 -23
View File
@@ -60,15 +60,13 @@ cfg_if! { if #[cfg(feature = "ssr")] {
#[server(Login, "/api")]
pub async fn login(username: String, password: String) -> Result<ApiResponse<()>, 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<AppData>| 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<Option<User>, ServerFnError> {
#[server(GetUsers, "/api", "Url", "get_users")]
pub async fn get_users() -> Result<ApiResponse<Vec<User>>, 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<AppData>| 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<ApiResponse<()>, 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());
let pool = extract(|data: Data<AppData>| async move { data.db_pool().clone() }).await?;
sqlx::query(r#"UPDATE "user" SET full_name = $1, email = $2, get_emails = $3 WHERE login = $4"#)
if !usr.admin && user.admin() {
let response = expect_context::<ResponseOptions>();
response.set_status(StatusCode::FORBIDDEN);
return Ok(ApiResponse::Error("You can't escalate your privileges".to_string()))
}
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<ApiResponse<()>, 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<AppData>| 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::<ResponseOptions>();
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<ApiResponse<()>, 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::<ResponseOptions>();
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<ApiResponse<()>, 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::<ResponseOptions>();
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(()))
}
+34 -1
View File
@@ -39,7 +39,9 @@ pub fn data_form<T: 'static + server_fn::ServerFn<()> + Clone + ForValidation>(
</ModalBody>
<ModalFooter>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal"
on:click=move |_| {validator.reset(); opener.hide()}>
on:click=move |_| {
validator.reset();
opener.hide();}>
{trl("Close")}
</button>
<button type="submit" class="btn btn-primary">
@@ -50,3 +52,34 @@ pub fn data_form<T: 'static + server_fn::ServerFn<()> + Clone + ForValidation>(
</ActionForm>
}
}
#[component]
pub fn question_dialog<T: 'static + server_fn::ServerFn<()> + Clone>(
opener: DialogOpener,
action: Action<T, Result<ApiResponse<()>, ServerFnError>>,
title: &'static str,
children: Children
) -> impl IntoView {
let upd_val = action.value();
view! {
<ActionForm action=action>
<ModalDialog opener=opener title=title>
<ModalBody>
<ServerErr result={upd_val} opener=opener/>
{children()}
</ModalBody>
<ModalFooter>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal"
on:click=move |_| {
opener.hide();}>
{trl("No")}
</button>
<button type="submit" class="btn btn-primary">
{trl("Yes")}
</button>
</ModalFooter>
</ModalDialog>
</ActionForm>
}
}
+5 -5
View File
@@ -7,11 +7,11 @@ pub fn Header() -> impl IntoView {
<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", "/")])
attributes=vec![
("data-theme", "theme-default".into_attribute()),
("class", "light-style layout-menu-fixed".into_attribute()),
("data-template", "vertical-menu-template-free".into_attribute()),
("data-assets-path", "/".into_attribute())]
/>
<Title text="Rezervovator"/>
<Meta charset="utf-8"/>
+32
View File
@@ -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);
}
}
+8 -11
View File
@@ -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>}
}
+81 -45
View File
@@ -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>
}
}
+53 -51
View File
@@ -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>
}
}
+1
View File
@@ -8,3 +8,4 @@ pub mod profile_edit;
pub mod change_pwd;
pub mod users;
pub mod user_edit;
pub mod user_delete;
+28 -2
View File
@@ -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>
}
}
+4 -30
View File
@@ -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>
}
+17
View File
@@ -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>
}
}
+101
View File
@@ -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>
}
}
+116
View File
@@ -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>
}
}