Add a really basic frontend that can log in and create posts
This commit is contained in:
parent
3945e89a98
commit
d6e81a0df0
1227
Cargo.lock
generated
1227
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
30
Cargo.toml
30
Cargo.toml
|
@ -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
9
api-types/Cargo.toml
Normal 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
45
api-types/src/lib.rs
Normal 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
25
backend/Cargo.toml
Normal 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"] }
|
|
@ -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()
|
|
@ -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);
|
21
streaming-frontends/Cargo.toml
Normal file
21
streaming-frontends/Cargo.toml
Normal 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"]}
|
79
streaming-frontends/src/authenticated_client.rs
Normal file
79
streaming-frontends/src/authenticated_client.rs
Normal 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 {}
|
142
streaming-frontends/src/create_post.rs
Normal file
142
streaming-frontends/src/create_post.rs
Normal 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",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
111
streaming-frontends/src/home.rs
Normal file
111
streaming-frontends/src/home.rs
Normal 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 }
|
||||
}
|
||||
})
|
||||
}
|
128
streaming-frontends/src/login.rs
Normal file
128
streaming-frontends/src/login.rs
Normal 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"
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
167
streaming-frontends/src/main.rs
Normal file
167
streaming-frontends/src/main.rs
Normal 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(())
|
||||
}
|
145
streaming-frontends/src/registration.rs
Normal file
145
streaming-frontends/src/registration.rs
Normal 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"
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
Loading…
Reference in a new issue