register working based on leptos example

This commit is contained in:
Felix Ableitner 2024-01-12 16:48:24 +01:00
parent 90d92f5391
commit 89a71c7fcd
15 changed files with 246 additions and 82 deletions

View File

@ -1,6 +1,7 @@
use crate::backend::database::user::{DbLocalUser, DbPerson, LocalUserView}; use crate::backend::database::user::{DbLocalUser, DbPerson, LocalUserView};
use crate::backend::database::{MyDataHandle, read_jwt_secret}; use crate::backend::database::{read_jwt_secret, MyDataHandle};
use crate::backend::error::MyResult; use crate::backend::error::MyResult;
use crate::common::{LoginResponse, LoginUserData, RegisterUserData};
use activitypub_federation::config::Data; use activitypub_federation::config::Data;
use anyhow::anyhow; use anyhow::anyhow;
use axum::{Form, Json}; use axum::{Form, Json};
@ -12,7 +13,6 @@ use jsonwebtoken::Validation;
use jsonwebtoken::{decode, get_current_timestamp}; use jsonwebtoken::{decode, get_current_timestamp};
use jsonwebtoken::{encode, EncodingKey, Header}; use jsonwebtoken::{encode, EncodingKey, Header};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::common::{LoginResponse, LoginUserData, RegisterUserData};
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
struct Claims { struct Claims {

View File

@ -96,11 +96,11 @@ pub async fn start(hostname: &str, database_url: &str) -> MyResult<()> {
.nest_service("/assets", ServeDir::new("assets")) .nest_service("/assets", ServeDir::new("assets"))
.nest_service( .nest_service(
"/pkg/ibis.js", "/pkg/ibis.js",
ServeFile::new_with_mime("dist/ibis.js", &"application/javascript".parse()?), ServeFile::new_with_mime("assets/dist/ibis.js", &"application/javascript".parse()?),
) )
.nest_service( .nest_service(
"/pkg/ibis_bg.wasm", "/pkg/ibis_bg.wasm",
ServeFile::new_with_mime("dist/ibis_bg.wasm", &"application/wasm".parse()?), ServeFile::new_with_mime("assets/dist/ibis_bg.wasm", &"application/wasm".parse()?),
) )
.nest("", federation_routes()) .nest("", federation_routes())
.nest("/api/v1", api_routes()) .nest("/api/v1", api_routes())

View File

@ -61,7 +61,7 @@ pub struct RegisterUserData {
pub password: String, pub password: String,
} }
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize, Clone)]
pub struct LoginResponse { pub struct LoginResponse {
pub jwt: String, pub jwt: String,
} }

View File

@ -1,10 +1,10 @@
use crate::common::{ArticleView, LoginResponse, LoginUserData, RegisterUserData};
use crate::common::GetArticleData; use crate::common::GetArticleData;
use crate::common::{ArticleView, LoginResponse, LoginUserData, RegisterUserData};
use crate::frontend::error::MyResult;
use anyhow::anyhow; use anyhow::anyhow;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use reqwest::{Client, RequestBuilder}; use reqwest::{Client, RequestBuilder};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::frontend::error::MyResult;
pub static CLIENT: Lazy<Client> = Lazy::new(Client::new); pub static CLIENT: Lazy<Client> = Lazy::new(Client::new);
@ -33,30 +33,20 @@ where
let status = res.status(); let status = res.status();
let text = res.text().await?; let text = res.text().await?;
if status == reqwest::StatusCode::OK { if status == reqwest::StatusCode::OK {
Ok(serde_json::from_str(&text) Ok(serde_json::from_str(&text).map_err(|e| anyhow!("Json error on {text}: {e}"))?)
.map_err(|e| anyhow!("Json error on {text}: {e}"))
?)
} else { } else {
Err(anyhow!("API error: {text}").into()) Err(anyhow!("API error: {text}").into())
} }
} }
pub async fn register(hostname: &str, username: &str, password: &str) -> MyResult<LoginResponse> { pub async fn register(hostname: &str, register_form: RegisterUserData) -> MyResult<LoginResponse> {
let register_form = RegisterUserData {
username: username.to_string(),
password: password.to_string(),
};
let req = CLIENT let req = CLIENT
.post(format!("http://{}/api/v1/user/register", hostname)) .post(format!("http://{}/api/v1/user/register", hostname))
.form(&register_form); .form(&register_form);
handle_json_res(req).await handle_json_res(req).await
} }
pub async fn login( pub async fn login(hostname: &str, username: &str, password: &str) -> MyResult<LoginResponse> {
hostname: &str,
username: &str,
password: &str,
) -> MyResult<LoginResponse> {
let login_form = LoginUserData { let login_form = LoginUserData {
username: username.to_string(), username: username.to_string(),
password: password.to_string(), password: password.to_string(),

View File

@ -1,16 +1,33 @@
use crate::frontend::article::Article; use crate::frontend::components::nav::Nav;
use crate::frontend::login::Login; use crate::frontend::pages::article::Article;
use crate::frontend::nav::Nav; use crate::frontend::pages::login::Login;
use leptos::{component, view, IntoView}; use crate::frontend::pages::register::Register;
use crate::frontend::pages::Page;
use leptos::{component, provide_context, use_context, view, IntoView};
use leptos_meta::provide_meta_context; use leptos_meta::provide_meta_context;
use leptos_meta::*; use leptos_meta::*;
use leptos_router::Route; use leptos_router::Route;
use leptos_router::Router; use leptos_router::Router;
use leptos_router::Routes; use leptos_router::Routes;
// TODO: change to GlobalState and also store auth token here
// https://book.leptos.dev/15_global_state.html
#[derive(Clone)]
pub struct BackendHostname(String);
impl BackendHostname {
pub fn read() -> String {
use_context::<BackendHostname>()
.expect("backend hostname is provided")
.0
}
}
#[component] #[component]
pub fn App() -> impl IntoView { pub fn App() -> impl IntoView {
provide_meta_context(); provide_meta_context();
let backend_hostname = BackendHostname("localhost:8080".to_string());
provide_context(backend_hostname);
view! { view! {
<> <>
<Stylesheet id="simple" href="/assets/simple.css"/> <Stylesheet id="simple" href="/assets/simple.css"/>
@ -19,8 +36,9 @@ pub fn App() -> impl IntoView {
<Nav /> <Nav />
<main> <main>
<Routes> <Routes>
<Route path="/" view=Article/> <Route path={Page::Home.path()} view=Article/>
<Route path="/login" view=Login/> <Route path={Page::Login.path()} view=Login/>
<Route path={Page::Register.path()} view=Register/>
</Routes> </Routes>
</main> </main>
</Router> </Router>

View File

@ -0,0 +1,75 @@
use leptos::{ev, *};
#[component]
pub fn CredentialsForm(
title: &'static str,
action_label: &'static str,
action: Action<(String, String), ()>,
error: Signal<Option<String>>,
disabled: Signal<bool>,
) -> impl IntoView {
let (password, set_password) = create_signal(String::new());
let (username, set_username) = create_signal(String::new());
let dispatch_action = move || action.dispatch((username.get(), password.get()));
let button_is_disabled = Signal::derive(move || {
disabled.get() || password.get().is_empty() || username.get().is_empty()
});
view! {
<form on:submit=|ev| ev.prevent_default()>
<p>{title}</p>
{move || {
error
.get()
.map(|err| {
view! { <p style="color:red;">{err}</p> }
})
}}
<input
type="text"
required
placeholder="Username"
prop:disabled=move || disabled.get()
on:keyup=move |ev: ev::KeyboardEvent| {
let val = event_target_value(&ev);
set_username.update(|v| *v = val);
}
on:change=move |ev| {
let val = event_target_value(&ev);
set_username.update(|v| *v = val);
}
/>
<input
type="password"
required
placeholder="Password"
prop:disabled=move || disabled.get()
on:keyup=move |ev: ev::KeyboardEvent| {
match &*ev.key() {
"Enter" => {
dispatch_action();
}
_ => {
let val = event_target_value(&ev);
set_password.update(|p| *p = val);
}
}
}
on:change=move |ev| {
let val = event_target_value(&ev);
set_password.update(|p| *p = val);
}
/>
<div>
<button
prop:disabled=move || button_is_disabled.get()
on:click=move |_| dispatch_action()
>
{action_label}
</button>
</div>
</form>
}
}

View File

@ -0,0 +1,2 @@
pub(crate) mod credentials;
pub mod nav;

View File

@ -3,6 +3,7 @@ use leptos_router::*;
#[component] #[component]
pub fn Nav() -> impl IntoView { pub fn Nav() -> impl IntoView {
// TODO: use `<Show when` based on auth token for login/register/logout
view! { view! {
<nav class="inner"> <nav class="inner">
<li> <li>

View File

@ -1,52 +0,0 @@
use leptos::*;
use leptos::ev::{SubmitEvent};
use log::info;
use crate::frontend::api::login;
// TODO: this seems to be working, but need to implement registration also
// TODO: use leptos_form if possible
// https://github.com/leptos-form/leptos_form/issues/18
fn do_login(ev: SubmitEvent, username: String, password: String) {
ev.prevent_default();
spawn_local(
async move {
let res = login("localhost:8080", &username, &password).await;
info!("{}", res.unwrap().jwt);
});
}
#[component]
pub fn Login() -> impl IntoView {
let name = RwSignal::new(String::new());
let password = RwSignal::new(String::new());
view! {
<form on:submit=move |ev| do_login(ev, name.get(), password.get())>
<div>
<label for="username">Username: </label>
<input
id="username"
type="text"
on:input=move |ev| name.set(event_target_value(&ev))
label="Username"
/>
</div>
<div>
<label for="password">Password: </label>
<input
id="password"
type="password"
on:input=move |ev| password.set(event_target_value(&ev))
/>
</div>
<div>
<button type="submit">
"Login"
</button>
</div>
</form>
}
}

View File

@ -1,9 +1,8 @@
pub mod api; pub mod api;
pub mod app; pub mod app;
pub mod article; mod components;
mod login;
pub mod nav;
mod error; mod error;
mod pages;
#[cfg(feature = "hydrate")] #[cfg(feature = "hydrate")]
#[wasm_bindgen::prelude::wasm_bindgen] #[wasm_bindgen::prelude::wasm_bindgen]

View File

@ -0,0 +1,51 @@
use crate::frontend::api::login;
use leptos::ev::SubmitEvent;
use leptos::*;
use log::info;
// TODO: this seems to be working, but need to implement registration also
// TODO: use leptos_form if possible
// https://github.com/leptos-form/leptos_form/issues/18
fn do_login(ev: SubmitEvent, username: String, password: String) {
ev.prevent_default();
spawn_local(async move {
let res = login("localhost:8080", &username, &password).await;
info!("{}", res.unwrap().jwt);
});
}
#[component]
pub fn Login() -> impl IntoView {
let name = RwSignal::new(String::new());
let password = RwSignal::new(String::new());
view! {
<form on:submit=move |ev| do_login(ev, name.get(), password.get())>
<div>
<label for="username">Username: </label>
<input
id="username"
type="text"
on:input=move |ev| name.set(event_target_value(&ev))
label="Username"
/>
</div>
<div>
<label for="password">Password: </label>
<input
id="password"
type="password"
on:input=move |ev| password.set(event_target_value(&ev))
/>
</div>
<div>
<button type="submit">
"Login"
</button>
</div>
</form>
}
}

21
src/frontend/pages/mod.rs Normal file
View File

@ -0,0 +1,21 @@
pub mod article;
pub mod login;
pub mod register;
#[derive(Debug, Clone, Copy, Default)]
pub enum Page {
#[default]
Home,
Login,
Register,
}
impl Page {
pub fn path(&self) -> &'static str {
match self {
Self::Home => "/",
Self::Login => "/login",
Self::Register => "/register",
}
}
}

View File

@ -0,0 +1,59 @@
use crate::common::{LoginResponse, RegisterUserData};
use crate::frontend::api::register;
use crate::frontend::app::BackendHostname;
use crate::frontend::components::credentials::*;
use crate::frontend::pages::Page;
use leptos::{logging::log, *};
use leptos_router::*;
#[component]
pub fn Register() -> impl IntoView {
let (register_response, set_register_response) = create_signal(None::<LoginResponse>);
let (register_error, set_register_error) = create_signal(None::<String>);
let (wait_for_response, set_wait_for_response) = create_signal(false);
let register_action = create_action(move |(email, password): &(String, String)| {
let username = email.to_string();
let password = password.to_string();
let credentials = RegisterUserData { username, password };
log!("Try to register new account for {}", credentials.username);
async move {
set_wait_for_response.update(|w| *w = true);
let result = register(&BackendHostname::read(), credentials).await;
set_wait_for_response.update(|w| *w = false);
match result {
Ok(res) => {
set_register_response.update(|v| *v = Some(res));
set_register_error.update(|e| *e = None);
}
Err(err) => {
let msg = err.0.to_string();
log::warn!("Unable to register new account: {msg}");
set_register_error.update(|e| *e = Some(msg));
}
}
}
});
let disabled = Signal::derive(move || wait_for_response.get());
view! {
<Show
when=move || register_response.get().is_some()
fallback=move || {
view! {
<CredentialsForm
title="Please enter the desired credentials"
action_label="Register"
action=register_action
error=register_error.into()
disabled
/>
}
}
>
<p>"You have successfully registered."</p>
<p>"You can now " <A href=Page::Login.path()>"login"</A> " with your new account."</p>
</Show>
}
}

View File

@ -11,6 +11,7 @@ use ibis::backend::start;
use ibis::common::ArticleView; use ibis::common::ArticleView;
use ibis::frontend::api; use ibis::frontend::api;
use ibis::frontend::api::get_query; use ibis::frontend::api::get_query;
use ibis_lib::frontend::api;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use reqwest::{Client, StatusCode}; use reqwest::{Client, StatusCode};
use serde::de::Deserialize; use serde::de::Deserialize;
@ -24,7 +25,6 @@ use std::time::Duration;
use tokio::task::JoinHandle; use tokio::task::JoinHandle;
use tracing::log::LevelFilter; use tracing::log::LevelFilter;
use url::Url; use url::Url;
use ibis_lib::frontend::api;
pub static CLIENT: Lazy<Client> = Lazy::new(Client::new); pub static CLIENT: Lazy<Client> = Lazy::new(Client::new);