Add a really basic frontend that can log in and create posts

This commit is contained in:
asonix 2023-04-08 17:03:25 -05:00
parent 3945e89a98
commit d6e81a0df0
15 changed files with 2127 additions and 85 deletions

1227
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,24 +1,6 @@
[package]
name = "streaming-funtimes"
description = "A simple activitypub relay"
version = "0.1.0"
authors = ["asonix <asonix@asonix.dog>"]
license = "AGPL-3.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
actix-web = { version = "4.3.1", default-features = false }
actix-web-lab = "0.19.1"
bcrypt = "0.14.0"
bonsaidb = { git = "https://github.com/KhonsuLabs/bonsaidb", version = "0.4.0", branch = "main", features = ["server", "client"] }
metrics = "0.20.1"
metrics-exporter-prometheus = "0.11.0"
rand = "0.8.5"
serde = { version = "1.0.159", features = ["derive"] }
tokio = { version = "1.27.0", features = ["full"] }
tracing = "0.1.37"
tracing-actix-web = { version = "0.7.3", default-features = false }
tracing-subscriber = { version = "0.3.16", features = ["env-filter"]}
uuid = { version = "1.3.0", features = ["v4", "serde"] }
[workspace]
members = [
"api-types",
"backend",
"streaming-frontends"
]

9
api-types/Cargo.toml Normal file
View file

@ -0,0 +1,9 @@
[package]
name = "api-types"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
serde = { version = "1", features = ["derive"] }

45
api-types/src/lib.rs Normal file
View file

@ -0,0 +1,45 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize)]
#[serde(transparent)]
pub struct PostQuery {
pub tags: Vec<(String, String)>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct NewPost {
pub title: String,
pub body: String,
pub tags: Vec<String>,
}
#[derive(Debug, Deserialize)]
pub struct NewComment {
pub body: String,
}
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Hash)]
pub struct NewUser {
pub username: String,
pub password: String,
}
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Hash)]
pub struct Auth {
pub username: String,
pub password: String,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct AuthResponse {
pub token: String,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct Post {
pub user_id: String,
pub id: String,
pub title: String,
pub body: String,
pub tags: Vec<String>,
}

25
backend/Cargo.toml Normal file
View file

@ -0,0 +1,25 @@
[package]
name = "streaming-funtimes"
description = "A simple activitypub relay"
version = "0.1.0"
authors = ["asonix <asonix@asonix.dog>"]
license = "AGPL-3.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
actix-web = { version = "4.3.1", default-features = false }
actix-web-lab = "0.19.1"
api-types = { version = "0.1.0", path = "../api-types" }
bcrypt = "0.14.0"
bonsaidb = { git = "https://github.com/KhonsuLabs/bonsaidb", version = "0.4.0", branch = "main", features = ["server", "client"] }
metrics = "0.20.1"
metrics-exporter-prometheus = "0.11.0"
rand = "0.8.5"
serde = { version = "1.0.159", features = ["derive"] }
tokio = { version = "1.27.0", features = ["full"] }
tracing = "0.1.37"
tracing-actix-web = { version = "0.7.3", default-features = false }
tracing-subscriber = { version = "0.3.16", features = ["env-filter"]}
uuid = { version = "1.3.0", features = ["v4", "serde"] }

View file

@ -15,6 +15,7 @@ use actix_web::{
App, FromRequest, HttpRequest, HttpServer,
};
use actix_web_lab::middleware::{from_fn, Next};
use api_types::{Auth, AuthResponse, NewComment, NewPost, NewUser, Post};
use bonsaidb::{
core::{
connection::{AsyncConnection, AsyncStorageConnection},
@ -28,7 +29,7 @@ use schema::{
Comment, CommentsByPost, MySchema, PostId, PostWithTags, PostsWithTagsByMultipleTags, Session,
TagSet, TrendingPosts, TrendingPostsByRank, UserByUsername, UserId,
};
use serde::{Deserialize, Serialize};
use serde::Deserialize;
use tracing_actix_web::TracingLogger;
use tracing_subscriber::{
fmt::format::FmtSpan, prelude::__tracing_subscriber_SubscriberExt, util::SubscriberInitExt,
@ -47,13 +48,14 @@ async fn index() -> String {
#[derive(Debug, Deserialize)]
#[serde(transparent)]
struct PostQuery {
tags: Vec<(String, String)>,
inner: api_types::PostQuery,
}
impl PostQuery {
// ?tags=hi&tags=hello&tags=howdy&something=anotherthing
fn tags(&self) -> TagSet {
let tags = self
.inner
.tags
.iter()
.filter_map(|(field_name, field_value)| {
@ -67,35 +69,10 @@ impl PostQuery {
TagSet::new(tags)
}
}
#[derive(Debug, Deserialize)]
struct NewPost {
title: String,
body: String,
tags: Vec<String>,
}
#[derive(Debug, Deserialize)]
struct NewComment {
body: String,
}
#[derive(Debug, Deserialize)]
struct NewUser {
username: String,
password: String,
}
#[derive(Debug, Deserialize)]
struct Auth {
username: String,
password: String,
}
#[derive(Debug, Serialize)]
struct AuthResponse {
token: String,
fn is_empty(&self) -> bool {
self.inner.tags.is_empty()
}
}
struct CurrentUser(pub UserId);
@ -232,7 +209,7 @@ async fn create_post(
Ok("Created!".to_string())
}
async fn get_post(repo: Data<Repo>, path: Path<PostId>) -> actix_web::Result<Json<PostWithTags>> {
async fn get_post(repo: Data<Repo>, path: Path<PostId>) -> actix_web::Result<Json<Post>> {
let post_id = path.into_inner();
let option = repo
.database
@ -242,9 +219,14 @@ async fn get_post(repo: Data<Repo>, path: Path<PostId>) -> actix_web::Result<Jso
.map_err(ErrorInternalServerError)?;
if let Some(document) = option {
Ok(Json(
PostWithTags::document_contents(&document).map_err(ErrorInternalServerError)?,
))
let pwt = PostWithTags::document_contents(&document).map_err(ErrorInternalServerError)?;
Ok(Json(Post {
user_id: pwt.user_id.to_string(),
id: pwt.id.to_string(),
title: pwt.title,
body: pwt.body,
tags: pwt.tags,
}))
} else {
Err(ErrorNotFound("Post does not exist").into())
}
@ -253,8 +235,8 @@ async fn get_post(repo: Data<Repo>, path: Path<PostId>) -> actix_web::Result<Jso
async fn get_all_posts(
repo: Data<Repo>,
query: Query<PostQuery>,
) -> actix_web::Result<Json<Vec<PostWithTags>>> {
let posts = if !query.tags.is_empty() {
) -> actix_web::Result<Json<Vec<Post>>> {
let posts = if !query.is_empty() {
repo.database
.view::<PostsWithTagsByMultipleTags>()
.with_key(&query.tags())
@ -278,7 +260,18 @@ async fn get_all_posts(
.map_err(ErrorInternalServerError)?
};
Ok(Json(posts))
Ok(Json(
posts
.into_iter()
.map(|pwt| Post {
user_id: pwt.user_id.to_string(),
id: pwt.id.to_string(),
title: pwt.title,
body: pwt.body,
tags: pwt.tags,
})
.collect(),
))
}
async fn create_comment(
@ -360,6 +353,10 @@ struct Repo {
}
fn initialize_tracing() {
if std::env::var("RUST_LOG").is_err() {
std::env::set_var("RUST_LOG", "info");
}
tracing_subscriber::registry()
.with(
tracing_subscriber::fmt::layer()

View file

@ -130,6 +130,12 @@ impl Default for PostId {
}
}
impl std::fmt::Display for PostId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.id.fmt(f)
}
}
impl<'k> KeyEncoding<'k> for PostId {
type Error = uuid::Error;
const LENGTH: Option<usize> = Some(16);
@ -170,6 +176,12 @@ impl Default for UserId {
}
}
impl std::fmt::Display for UserId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.id.fmt(f)
}
}
impl<'k> KeyEncoding<'k> for UserId {
type Error = uuid::Error;
const LENGTH: Option<usize> = Some(16);

View file

@ -0,0 +1,21 @@
[package]
name = "streaming-frontends"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
api-types = { version = "0.1.0", path = "../api-types" }
axum = "0.6.1"
dioxus = "0.3.2"
dioxus-liveview = { version = "0.3.0", features = ["axum"] }
dioxus-router = "0.3.0"
metrics = "0.20.1"
metrics-exporter-prometheus = "0.11.0"
reqwest = { version = "0.11.16", features = ["json"], default-features = false }
tokio = { version = "1", features = ["full"] }
tower = "0.4.13"
tower-http = { version = "0.4.0", features = ["trace"] }
tracing = "0.1.37"
tracing-subscriber = { version = "0.3.16", features = ["env-filter"]}

View file

@ -0,0 +1,79 @@
use std::sync::Arc;
use api_types::NewPost;
use reqwest::Client;
#[derive(Debug)]
pub(crate) enum CreatePostError {
Request(reqwest::Error),
ServerError,
}
impl From<reqwest::Error> for CreatePostError {
fn from(value: reqwest::Error) -> Self {
Self::Request(value)
}
}
impl std::fmt::Display for CreatePostError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Request(_) => write!(f, "Error making request"),
Self::ServerError => write!(f, "Server errored"),
}
}
}
impl std::error::Error for CreatePostError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Request(e) => Some(e),
_ => None,
}
}
}
#[derive(Clone)]
pub(crate) struct AuthenticatedClient {
client: Client,
session_token: Arc<String>,
}
impl AuthenticatedClient {
pub(crate) fn new(client: Client, session_token: String) -> Self {
Self {
client,
session_token: Arc::new(session_token),
}
}
pub(crate) async fn create_post(&self, new_post: &NewPost) -> Result<(), CreatePostError> {
let response = self
.client
.post("http://localhost:8006/posts")
.header("Authorization", format!("Bearer {}", self.session_token))
.json(new_post)
.send()
.await?;
if !response.status().is_success() {
return Err(CreatePostError::ServerError);
}
Ok(())
}
}
impl PartialEq for AuthenticatedClient {
fn eq(&self, other: &Self) -> bool {
self.session_token.eq(&other.session_token)
}
}
impl<'a> PartialEq<&'a AuthenticatedClient> for AuthenticatedClient {
fn eq(&self, other: &&'a AuthenticatedClient) -> bool {
self.session_token.eq(&other.session_token)
}
}
impl Eq for AuthenticatedClient {}

View file

@ -0,0 +1,142 @@
use crate::AuthenticatedClient;
use std::collections::BTreeMap;
use api_types::NewPost;
use dioxus::prelude::*;
use dioxus_router::use_router;
#[derive(Props, PartialEq, Eq)]
pub(crate) struct CreatePostProps {
pub(crate) client: AuthenticatedClient,
}
#[derive(Default)]
struct FormState {
title: String,
body: String,
tags: BTreeMap<usize, String>,
tag_id: usize,
new_tag: String,
}
impl FormState {
fn push_tag(&mut self) {
let tag = std::mem::take(&mut self.new_tag);
let id = self.tag_id;
self.tag_id += 1;
self.tags.insert(id, tag);
}
}
pub(crate) fn CreatePostForm(cx: Scope<CreatePostProps>) -> Element {
let form_state = use_ref(cx, || FormState::default());
let client = cx.props.client.clone();
let router = use_router(cx).clone();
let submit = move |_| {
let new_post = form_state.with(|form| NewPost {
title: form.title.clone(),
body: form.body.clone(),
tags: form.tags.values().cloned().collect(),
});
let client = client.clone();
let router = router.clone();
cx.spawn(async move {
match client.create_post(&new_post).await {
Ok(_) => router.push_route("/posts", None, None),
Err(e) => tracing::warn!("{e}"),
}
});
};
cx.render(rsx! {
form {
onsubmit: submit,
div {
label {
r#for: "title",
"Title:",
},
input {
oninput: move |evt| form_state.with_mut(|form| form.title = evt.value.clone()),
r#type: "text",
name: "title",
},
},
div {
label {
r#for: "body",
"Post:",
},
input {
oninput: move |evt| form_state.with_mut(|form| { form.body = evt.value.clone(); }),
r#type: "text",
name: "body",
},
},
div {
label {
r#for: "tags",
"Tags:",
},
form_state.with(|form| {
form.tags.iter().map(|(index, tag)| {
let index = *index;
let tag = tag.clone();
rsx! {
div {
input {
oninput: move |evt| form_state.with_mut(|form| {
if let Some(tag) = form.tags.get_mut(&index) {
*tag = evt.value.clone();
}
}),
value: "{tag}",
r#type: "text",
name: "tags",
}
input {
r#type: "button",
onclick: move |_| form_state.with_mut(|form| {
form.tags.remove(&index);
}),
value: "Remove",
}
}
}
}).collect::<Vec<_>>()
}).into_iter(),
div {
input {
oninput: move |evt| form_state.with_mut(|form| {
form.new_tag = evt.value.clone();
}),
r#type: "text",
value: "{form_state.with(|form| form.new_tag.clone())}",
name: "tags",
},
input {
r#type: "button",
onclick: move |_| form_state.with_mut(|form| {
form.push_tag();
}),
value: "Add",
},
},
},
div {
input {
r#type: "submit",
value: "Create Post",
},
},
},
})
}

View file

@ -0,0 +1,111 @@
use api_types::Post;
use dioxus::prelude::*;
use reqwest::Client;
#[derive(Props)]
pub(crate) struct HomeProps<'a> {
pub(crate) client: &'a Client,
}
#[derive(Debug)]
enum HomeError {
Request(reqwest::Error),
ServerError,
}
impl From<reqwest::Error> for HomeError {
fn from(value: reqwest::Error) -> Self {
Self::Request(value)
}
}
impl std::fmt::Display for HomeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Request(_) => write!(f, "Error in request"),
Self::ServerError => write!(f, "Server had a problem"),
}
}
}
impl std::error::Error for HomeError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Request(e) => Some(e),
_ => None,
}
}
}
pub(crate) fn Home<'a>(cx: Scope<'a, HomeProps<'a>>) -> Element<'a> {
let client = cx.props.client.clone();
let posts = use_future(cx, (), |()| async move {
let response = client.get("http://localhost:8006/posts").send().await?;
if !response.status().is_success() {
return Err(HomeError::ServerError);
}
let posts: Vec<Post> = response.json().await?;
Ok(posts)
});
cx.render(match posts.value() {
None => {
rsx! { "Loading posts..." }
}
Some(Err(e)) => rsx! {
div {
"Error: ",
e.to_string(),
ol {
{
let mut report = Vec::new();
let e = e as &dyn std::error::Error;
let mut err = e.source();
while let Some(error) = err {
report.push(rsx! {
li { error.to_string() }
});
err = error.source();
}
rsx! {
report.into_iter()
}
}
}
}
},
Some(Ok(posts)) if posts.is_empty() => {
rsx! { "No posts found" }
}
Some(Ok(posts)) => {
let rendered = posts.iter().map(|post| {
rsx! {
div {
h3 {
post.title.clone()
}
p {
post.body.clone()
}
if post.tags.is_empty() {
rsx! {"No tags"}
} else {
rsx! {
post.tags.iter().map(|tag| {
rsx! { span { tag.clone() } }
})
}
}
}
}
});
rsx! { rendered }
}
})
}

View file

@ -0,0 +1,128 @@
use api_types::{Auth, AuthResponse};
use dioxus::prelude::*;
use dioxus_router::use_router;
use reqwest::Client;
use tracing::Instrument;
#[derive(Debug)]
enum LoginError {
Request(reqwest::Error),
InvalidForm,
LoginFailed,
}
impl std::fmt::Display for LoginError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Request(_) => write!(f, "Failed to request upstream"),
Self::InvalidForm => write!(f, "Login form is invalid"),
Self::LoginFailed => write!(f, "Login failed"),
}
}
}
impl std::error::Error for LoginError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Request(e) => Some(e),
_ => None,
}
}
}
impl From<reqwest::Error> for LoginError {
fn from(value: reqwest::Error) -> Self {
Self::Request(value)
}
}
#[derive(Props)]
pub(crate) struct LoginProps<'a> {
pub(crate) session_token: &'a UseState<Option<String>>,
pub(crate) client: &'a Client,
}
pub(crate) fn LoginForm<'a>(cx: Scope<'a, LoginProps<'a>>) -> Element<'a> {
let auth = use_state(cx, || Auth {
username: String::new(),
password: String::new(),
});
let router = use_router(cx).clone();
let client: Client = cx.props.client.clone();
let session_token = cx.props.session_token;
let _future = use_future(cx, (auth, session_token), move |(auth, session_token)| {
async move {
if session_token.get().is_some() {
return Ok(());
}
let auth = auth.get();
if auth.username.is_empty() || auth.password.is_empty() {
return Err(LoginError::InvalidForm);
}
tracing::info!("Authenticating user");
let response = client
.post("http://localhost:8006/sessions")
.json(auth)
.send()
.await?;
if response.status().is_success() {
let json = response.json::<AuthResponse>().await?;
session_token.set(Some(json.token));
router.push_route("/home", None, None);
return Ok(());
}
Err(LoginError::LoginFailed)
}
.instrument(tracing::info_span!("Running login future"))
});
cx.render(rsx! {
form {
onsubmit: move |event| {
let Some(username) = event.data.values.get("username") else {
return;
};
let Some(password) = event.data.values.get("password") else {
return;
};
auth.set(Auth {
username: username.to_string(),
password: password.to_string(),
});
},
div {
label {
r#for: "username",
"Username"
},
input {
r#type: "text",
name: "username",
},
},
div {
label {
r#for: "password",
"Password"
},
input {
r#type: "password",
name: "password",
},
}
input {
r#type: "submit",
"Login"
},
}
})
}

View file

@ -0,0 +1,167 @@
use std::{collections::BTreeMap, sync::Arc};
use api_types::Post;
use axum::{extract::WebSocketUpgrade, response::Html, routing::get, Extension, Router};
use dioxus::prelude::*;
use dioxus_router::{Link, Redirect, Route, Router};
use reqwest::Client;
use tower::ServiceBuilder;
use tower_http::trace::{DefaultMakeSpan, TraceLayer};
use tracing::Level;
use tracing_subscriber::{
fmt::format::FmtSpan, prelude::__tracing_subscriber_SubscriberExt, util::SubscriberInitExt,
EnvFilter, Layer,
};
mod authenticated_client;
mod create_post;
mod home;
mod login;
mod registration;
use authenticated_client::AuthenticatedClient;
use create_post::CreatePostForm;
use home::Home;
use login::LoginForm;
use registration::RegistrationForm;
fn application(cx: Scope<Arc<ApplicationState>>) -> Element {
let session_token: &UseState<Option<String>> = use_state(cx, || None);
let client = &cx.props.client;
cx.render(rsx! {
Router {
nav {
div {
Link { to: "/posts", "Home" },
},
if session_token.get().is_some() {
rsx! {
div {
Link { to: "/posts/new", "Create Post!" }
}
}
} else {
rsx! {
div {
Link { to: "/login", "Login!" },
},
div {
Link { to: "/register", "Register!" },
}
}
}
},
Route { to: "/posts", Home { client: client } },
if let Some(session_token) = session_token.get() {
rsx! {
Route {
to: "/posts/new",
CreatePostForm {
client: AuthenticatedClient::new(client.clone(), session_token.clone()),
}
},
}
}
Route { to: "/login", LoginForm { session_token: session_token, client: client } },
Route { to: "/register", RegistrationForm { session_token: session_token, client: client } },
Redirect { from: "", to: "/posts" },
}
})
}
fn initialize_tracing() {
if std::env::var("RUST_LOG").is_err() {
std::env::set_var("RUST_LOG", "info");
}
tracing_subscriber::registry()
.with(
tracing_subscriber::fmt::layer()
.pretty()
.with_span_events(FmtSpan::NEW | FmtSpan::CLOSE)
.with_filter(EnvFilter::from_default_env()),
)
.init();
}
fn initialize_metrics() {
// PrometheusBuilder::new()
// .install()
// .expect("Installed prometheus recorder");
}
struct ApplicationState {
client: Client,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
initialize_tracing();
initialize_metrics();
let client = reqwest::ClientBuilder::new()
.user_agent("Our cool UI")
.build()?;
let addr: std::net::SocketAddr = ([127, 0, 0, 1], 3030).into();
let view = dioxus_liveview::LiveViewPool::new();
let app =
Router::new()
// The root route contains the glue code to connect to the WebSocket
.route(
"/",
get(move || async move {
Html(format!(
r#"
<!DOCTYPE html>
<html>
<head> <title>Dioxus LiveView with Axum</title> </head>
<body> <div id="main"></div> </body>
{glue}
</html>
"#,
// Create the glue code to connect to the WebSocket on the "/ws" route
glue = dioxus_liveview::interpreter_glue(&format!("ws://{addr}/ws"))
))
}),
)
// The WebSocket route is what Dioxus uses to communicate with the browser
.route(
"/ws",
get(
move |ws: WebSocketUpgrade,
Extension(state): Extension<Arc<ApplicationState>>| async move {
ws.on_upgrade(move |socket| async move {
// When the WebSocket is upgraded, launch the LiveView with the app component
_ = view
.launch_with_props(
dioxus_liveview::axum_socket(socket),
application,
state.clone(),
)
.await;
})
},
),
)
.layer(
ServiceBuilder::new()
.layer(
TraceLayer::new_for_http()
.make_span_with(DefaultMakeSpan::new().level(Level::INFO)),
)
.layer(Extension(Arc::new(ApplicationState { client }))),
);
tracing::info!("Listening on http://{addr}");
axum::Server::bind(&addr.to_string().parse()?)
.serve(app.into_make_service())
.await?;
Ok(())
}

View file

@ -0,0 +1,145 @@
use api_types::{AuthResponse, NewUser};
use dioxus::prelude::*;
use dioxus_router::use_router;
use reqwest::Client;
use tracing::Instrument;
#[derive(Debug)]
enum RegistrationError {
Request(reqwest::Error),
InvalidForm,
RegistrationFailed,
LoginFailed,
}
impl std::fmt::Display for RegistrationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Request(_) => write!(f, "Failed to request upstream"),
Self::InvalidForm => write!(f, "Registration form is invalid"),
Self::RegistrationFailed => write!(f, "Invalid credentials"),
Self::LoginFailed => write!(f, "Login failed"),
}
}
}
impl std::error::Error for RegistrationError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Request(e) => Some(e),
_ => None,
}
}
}
impl From<reqwest::Error> for RegistrationError {
fn from(value: reqwest::Error) -> Self {
Self::Request(value)
}
}
#[derive(Props)]
pub(crate) struct RegistrationProps<'a> {
pub(crate) session_token: &'a UseState<Option<String>>,
pub(crate) client: &'a Client,
}
pub(crate) fn RegistrationForm<'a>(cx: Scope<'a, RegistrationProps<'a>>) -> Element<'a> {
let new_user = use_state(cx, || NewUser {
username: String::new(),
password: String::new(),
});
let router = use_router(cx).clone();
let client: Client = cx.props.client.clone();
let session_token = cx.props.session_token;
let _future = use_future(
cx,
(new_user, session_token),
move |(new_user, session_token)| {
async move {
if session_token.get().is_some() {
return Ok(());
}
let new_user = new_user.get();
if new_user.username.is_empty() || new_user.password.is_empty() {
return Err(RegistrationError::InvalidForm);
}
tracing::info!("Creating user");
let response = client
.post("http://localhost:8006/users")
.json(new_user)
.send()
.await?;
if !response.status().is_success() {
return Err(RegistrationError::RegistrationFailed);
}
tracing::info!("Authenticating user");
let response = client
.post("http://localhost:8006/sessions")
.json(new_user)
.send()
.await?;
if response.status().is_success() {
let json = response.json::<AuthResponse>().await?;
session_token.set(Some(json.token));
router.push_route("/home", None, None);
return Ok(());
}
Err(RegistrationError::LoginFailed)
}
.instrument(tracing::info_span!("Running create user future"))
},
);
cx.render(rsx! {
form {
onsubmit: move |event| {
let Some(username) = event.data.values.get("username") else {
return;
};
let Some(password) = event.data.values.get("password") else {
return;
};
new_user.set(NewUser {
username: username.to_string(),
password: password.to_string(),
});
},
div {
label {
r#for: "username",
"Username"
},
input {
r#type: "text",
name: "username",
},
},
div {
label {
r#for: "password",
"Password"
},
input {
r#type: "password",
name: "password",
},
}
input {
r#type: "submit",
"Create Account"
},
}
})
}