Implemented user authentication.

This commit is contained in:
2023-09-22 21:26:13 +02:00
parent 0bac17f2eb
commit 17f628739f
23 changed files with 704 additions and 326 deletions
+18 -199
View File
@@ -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! {
<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"/>
<Header/>
//<!-- 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" />
<Body class="testik"/>
<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="/" 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="/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">
<Router>
<main>
<Routes>
<Route path="" view=|| view! { <HomePage/> }/>
<Route path="settings" view=|| view! { <Settings/> }/>
</Routes>
</main>
</Router>
</div>
</div>
</div>
</div>
</div>
/*<Html lang="cz"/>
// injects a stylesheet into the document <head>
// id=leptos means cargo-leptos will hot-reload this stylesheet
<Stylesheet id="leptos" href="/pkg/leptos_start.css"/>
<Stylesheet href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"/>
// sets the document title
<Title text="Welcome to Leptos"/>
<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>*/
}
}
+71
View File
@@ -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)) })
}
}
}
}}
+11 -5
View File
@@ -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(()))
}
+26 -12
View File
@@ -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,
}
pub struct Property {
impl User {
pub fn id(&self) -> i32 {
self.id
}
}
/*pub struct Property {
id: u16,
name: String,
description: String,
@@ -92,4 +106,4 @@ pub struct ReservationSum {
customer: Customer,
price: Decimal,
state: ReservationState,
}
}*/
+18
View File
@@ -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")] {
+102
View File
@@ -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)
}
+140
View File
@@ -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>
}
}
+38
View File
@@ -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" />
}
}
+2
View File
@@ -1,4 +1,6 @@
pub mod modal_box;
pub mod server_err;
pub mod validation_err;
pub mod header;
pub mod admin_portal;
+2 -1
View File
@@ -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 || {
-1
View File
@@ -1,5 +1,4 @@
pub mod app;
pub mod server_fn;
pub mod locales;
pub mod backend;
mod pages;
+1 -2
View File
@@ -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()
+2
View File
@@ -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()
+1 -1
View File
@@ -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;
+16 -12
View File
@@ -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>
-11
View File
@@ -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>
}
}
+74
View File
@@ -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>
}
}
+2
View File
@@ -2,3 +2,5 @@ pub mod home_page;
pub mod settings;
pub mod company_info;
mod company_edit;
pub mod login;
pub mod public;
+8
View File
@@ -0,0 +1,8 @@
use leptos::*;
#[component]
pub fn Public() -> impl IntoView {
view! {
<div>"public"</div>
}
}
-33
View File
@@ -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(())
}
+8 -8
View File
@@ -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()