mirror of
https://github.com/Nutomic/ibis.git
synced 2024-11-25 07:21:08 +00:00
register working based on leptos example
This commit is contained in:
parent
90d92f5391
commit
89a71c7fcd
15 changed files with 246 additions and 82 deletions
|
@ -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 {
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(®ister_form);
|
.form(®ister_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(),
|
||||||
|
|
|
@ -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>
|
||||||
|
|
75
src/frontend/components/credentials.rs
Normal file
75
src/frontend/components/credentials.rs
Normal 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>
|
||||||
|
}
|
||||||
|
}
|
2
src/frontend/components/mod.rs
Normal file
2
src/frontend/components/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
pub(crate) mod credentials;
|
||||||
|
pub mod nav;
|
|
@ -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>
|
|
@ -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>
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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]
|
||||||
|
|
51
src/frontend/pages/login.rs
Normal file
51
src/frontend/pages/login.rs
Normal 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
21
src/frontend/pages/mod.rs
Normal 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",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
59
src/frontend/pages/register.rs
Normal file
59
src/frontend/pages/register.rs
Normal 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>
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue