mirror of
https://github.com/Nutomic/ibis.git
synced 2024-12-25 21:31:24 +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::{MyDataHandle, read_jwt_secret};
|
||||
use crate::backend::database::{read_jwt_secret, MyDataHandle};
|
||||
use crate::backend::error::MyResult;
|
||||
use crate::common::{LoginResponse, LoginUserData, RegisterUserData};
|
||||
use activitypub_federation::config::Data;
|
||||
use anyhow::anyhow;
|
||||
use axum::{Form, Json};
|
||||
|
@ -12,7 +13,6 @@ use jsonwebtoken::Validation;
|
|||
use jsonwebtoken::{decode, get_current_timestamp};
|
||||
use jsonwebtoken::{encode, EncodingKey, Header};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::common::{LoginResponse, LoginUserData, RegisterUserData};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct Claims {
|
||||
|
|
|
@ -96,11 +96,11 @@ pub async fn start(hostname: &str, database_url: &str) -> MyResult<()> {
|
|||
.nest_service("/assets", ServeDir::new("assets"))
|
||||
.nest_service(
|
||||
"/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(
|
||||
"/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("/api/v1", api_routes())
|
||||
|
|
|
@ -61,7 +61,7 @@ pub struct RegisterUserData {
|
|||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[derive(Deserialize, Serialize, Clone)]
|
||||
pub struct LoginResponse {
|
||||
pub jwt: String,
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
use crate::common::{ArticleView, LoginResponse, LoginUserData, RegisterUserData};
|
||||
use crate::common::GetArticleData;
|
||||
use crate::common::{ArticleView, LoginResponse, LoginUserData, RegisterUserData};
|
||||
use crate::frontend::error::MyResult;
|
||||
use anyhow::anyhow;
|
||||
use once_cell::sync::Lazy;
|
||||
use reqwest::{Client, RequestBuilder};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::frontend::error::MyResult;
|
||||
|
||||
pub static CLIENT: Lazy<Client> = Lazy::new(Client::new);
|
||||
|
||||
|
@ -33,30 +33,20 @@ where
|
|||
let status = res.status();
|
||||
let text = res.text().await?;
|
||||
if status == reqwest::StatusCode::OK {
|
||||
Ok(serde_json::from_str(&text)
|
||||
.map_err(|e| anyhow!("Json error on {text}: {e}"))
|
||||
?)
|
||||
Ok(serde_json::from_str(&text).map_err(|e| anyhow!("Json error on {text}: {e}"))?)
|
||||
} else {
|
||||
Err(anyhow!("API error: {text}").into())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn register(hostname: &str, username: &str, password: &str) -> MyResult<LoginResponse> {
|
||||
let register_form = RegisterUserData {
|
||||
username: username.to_string(),
|
||||
password: password.to_string(),
|
||||
};
|
||||
pub async fn register(hostname: &str, register_form: RegisterUserData) -> MyResult<LoginResponse> {
|
||||
let req = CLIENT
|
||||
.post(format!("http://{}/api/v1/user/register", hostname))
|
||||
.form(®ister_form);
|
||||
handle_json_res(req).await
|
||||
}
|
||||
|
||||
pub async fn login(
|
||||
hostname: &str,
|
||||
username: &str,
|
||||
password: &str,
|
||||
) -> MyResult<LoginResponse> {
|
||||
pub async fn login(hostname: &str, username: &str, password: &str) -> MyResult<LoginResponse> {
|
||||
let login_form = LoginUserData {
|
||||
username: username.to_string(),
|
||||
password: password.to_string(),
|
||||
|
|
|
@ -1,16 +1,33 @@
|
|||
use crate::frontend::article::Article;
|
||||
use crate::frontend::login::Login;
|
||||
use crate::frontend::nav::Nav;
|
||||
use leptos::{component, view, IntoView};
|
||||
use crate::frontend::components::nav::Nav;
|
||||
use crate::frontend::pages::article::Article;
|
||||
use crate::frontend::pages::login::Login;
|
||||
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::*;
|
||||
use leptos_router::Route;
|
||||
use leptos_router::Router;
|
||||
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]
|
||||
pub fn App() -> impl IntoView {
|
||||
provide_meta_context();
|
||||
let backend_hostname = BackendHostname("localhost:8080".to_string());
|
||||
provide_context(backend_hostname);
|
||||
view! {
|
||||
<>
|
||||
<Stylesheet id="simple" href="/assets/simple.css"/>
|
||||
|
@ -19,8 +36,9 @@ pub fn App() -> impl IntoView {
|
|||
<Nav />
|
||||
<main>
|
||||
<Routes>
|
||||
<Route path="/" view=Article/>
|
||||
<Route path="/login" view=Login/>
|
||||
<Route path={Page::Home.path()} view=Article/>
|
||||
<Route path={Page::Login.path()} view=Login/>
|
||||
<Route path={Page::Register.path()} view=Register/>
|
||||
</Routes>
|
||||
</main>
|
||||
</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]
|
||||
pub fn Nav() -> impl IntoView {
|
||||
// TODO: use `<Show when` based on auth token for login/register/logout
|
||||
view! {
|
||||
<nav class="inner">
|
||||
<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 app;
|
||||
pub mod article;
|
||||
mod login;
|
||||
pub mod nav;
|
||||
mod components;
|
||||
mod error;
|
||||
mod pages;
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
#[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::frontend::api;
|
||||
use ibis::frontend::api::get_query;
|
||||
use ibis_lib::frontend::api;
|
||||
use once_cell::sync::Lazy;
|
||||
use reqwest::{Client, StatusCode};
|
||||
use serde::de::Deserialize;
|
||||
|
@ -24,7 +25,6 @@ use std::time::Duration;
|
|||
use tokio::task::JoinHandle;
|
||||
use tracing::log::LevelFilter;
|
||||
use url::Url;
|
||||
use ibis_lib::frontend::api;
|
||||
|
||||
pub static CLIENT: Lazy<Client> = Lazy::new(Client::new);
|
||||
|
||||
|
|
Loading…
Reference in a new issue