codify "platform" into resolution logic, server-fn/fullstack overhaul (#4596)

overhaul dioxus fullstack using a new implementation of server_fn

This PR is a complete rewrite of dioxus fullstack designed to improve compatibility with axum and to fix a number of compilation errors based on feature flag unification plumbing through macros.
This commit is contained in:
Jonathan Kelley authored and GitHub committed 2025-10-03 18:26:19 -07:00
1 parent 8421c3aef0
commit f2b1c6d97f
424 files changed
+17284 -6624

No files matched your search

@@ -1,7 +1,7 @@
[package]
name = "bluetooth-scanner"
version = "0.1.1"
edition = "2018"
edition = "2021"
publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -0,0 +1,90 @@
use dioxus::prelude::*;
fn main() {
dioxus::launch(app)
}
fn app() -> Element {
let mut scan = use_action(|| async {
use btleplug::api::{Central, Manager as _, Peripheral, ScanFilter};
let manager = btleplug::platform::Manager::new().await?;
// get the first bluetooth adapter
let adapters = manager.adapters().await?;
let central = adapters
.into_iter()
.next()
.context("No Bluetooth adapter found")?;
// start scanning for devices
central.start_scan(ScanFilter::default()).await?;
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
// Return the list of peripherals after scanning
let mut devices = vec![];
for p in central.peripherals().await? {
if let Some(p) = p.properties().await? {
devices.push(p);
}
}
// Sort them by RSSI (signal strength)
devices.sort_by_key(|p| p.rssi.unwrap_or(-100));
dioxus::Ok(devices)
});
rsx! {
Stylesheet { href: asset!("/assets/tailwind.css") }
div {
div { class: "py-8 px-6",
div { class: "container px-4 mx-auto",
h2 { class: "text-2xl font-bold", "Scan for Bluetooth Devices" }
button {
class: "inline-block w-full md:w-auto px-6 py-3 font-medium text-white bg-indigo-500 hover:bg-indigo-600 rounded transition duration-200",
disabled: scan.is_pending(),
onclick: move |_| {
scan.call();
},
if scan.is_pending() { "Scanning" } else { "Scan" }
}
}
}
section { class: "py-8",
div { class: "container px-4 mx-auto",
div { class: "p-4 mb-6 bg-white shadow rounded overflow-x-auto",
table { class: "table-auto w-full",
thead {
tr { class: "text-xs text-gray-500 text-left",
th { class: "pl-6 pb-3 font-medium", "Strength" }
th { class: "pb-3 font-medium", "Network" }
th { class: "pb-3 font-medium", "Channel" }
th { class: "pb-3 px-2 font-medium", "Security" }
}
}
match scan.result() {
None if scan.is_pending() => rsx! { "Scanning..." },
None => rsx! { "Press Scan to start scanning" },
Some(Err(_err)) => rsx! { "Failed to scan" },
Some(Ok(peripherals)) => rsx! {
tbody {
for peripheral in peripherals.read().iter().rev() {
tr { class: "text-xs bg-gray-50",
td { class: "py-5 px-6 font-medium", "{peripheral.rssi.unwrap_or(-100)}" }
td { class: "flex py-3 font-medium", "{peripheral.local_name.clone().unwrap_or_default()}" }
td { span { class: "inline-block py-1 px-2 text-white bg-green-500 rounded-full", "{peripheral.address}" } }
td { span { class: "inline-block py-1 px-2 text-purple-500 bg-purple-50 rounded-full", "{peripheral.tx_power_level.unwrap_or_default()}" } }
}
}
}
}
}
}
}
}
}
}
}
}
@@ -54,7 +54,7 @@ fn app() -> Element {
};
rsx! {
document::Link { rel: "stylesheet", href: STYLE }
Stylesheet { href: STYLE }
div { id: "wrapper",
div { class: "app",
div { class: "calculator", tabindex: "0", onkeydown: handle_key_down_event,
@@ -8,8 +8,8 @@
use dioxus::desktop::tao::dpi::LogicalSize;
use dioxus::desktop::{Config, WindowBuilder};
use dioxus::html::input_data::keyboard_types::Key;
use dioxus::html::MouseEvent;
use dioxus::html::input_data::keyboard_types::Key;
use dioxus::prelude::*;
fn main() {
@@ -29,10 +29,7 @@ fn app() -> Element {
let mut state = use_signal(Calculator::new);
rsx! {
document::Link {
rel: "stylesheet",
href: asset!("/examples/assets/calculator.css"),
}
Stylesheet { href: asset!("/examples/assets/calculator.css") }
div { id: "wrapper",
div { class: "app",
div {
@@ -16,7 +16,7 @@ fn app() -> Element {
let sum = use_memo(move || counters.read().iter().copied().sum::<i32>());
rsx! {
document::Link { rel: "stylesheet", href: STYLE }
Stylesheet { href: STYLE }
div { id: "controls",
button { onclick: move |_| counters.write().push(0), "Add counter" }
@@ -20,20 +20,12 @@ fn main() {
}))
.launch(|| {
rsx! {
document::Link {
rel: "stylesheet",
Stylesheet {
href: "https://unpkg.com/purecss@2.0.6/build/pure-min.css",
integrity: "sha384-Uu6IeWbM+gzNVXJcM9XV3SohHtmWE+3VGi496jvgX1jyvDTXfdK+rfZc8C1Aehk5",
crossorigin: "anonymous",
}
document::Link {
rel: "stylesheet",
href: asset!("/examples/assets/crm.css"),
}
document::Link {
rel: "stylesheet",
href: asset!("/examples/assets/crm.css"),
}
Stylesheet { href: asset!("/examples/assets/crm.css") }
h1 { "Dioxus CRM Example" }
Router::<Route> {}
}
+70
View File
@@ -0,0 +1,70 @@
//! This example demonstrates a simple app that fetches a list of dog breeds and displays a random dog.
//!
//! This app combines `use_loader` and `use_action` to fetch data from the Dog API.
//! - `use_loader` automatically fetches the list of dog breeds when the component mounts.
//! - `use_action` fetches a random dog image whenever the `.dispatch` method is called.
use dioxus::prelude::*;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
fn main() {
dioxus::launch(app);
}
fn app() -> Element {
// Fetch the list of breeds from the Dog API, using the `?` syntax to suspend or throw errors
let breed_list = use_loader(move || async move {
#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)]
struct ListBreeds {
message: HashMap<String, Vec<String>>,
}
reqwest::get("https://dog.ceo/api/breeds/list/all")
.await?
.json::<ListBreeds>()
.await
})?;
// Whenever this action is called, it will re-run the future and return the result.
let mut breed = use_action(move |breed| async move {
#[derive(Deserialize, Serialize, Debug, PartialEq)]
struct DogApi {
message: String,
}
reqwest::get(format!("https://dog.ceo/api/breed/{breed}/images/random"))
.await
.unwrap()
.json::<DogApi>()
.await
});
rsx! {
h1 { "Doggo selector" }
div { width: "400px",
for cur_breed in breed_list.read().message.keys().take(20).cloned() {
button {
onclick: move |_| {
breed.call(cur_breed.clone());
},
"{cur_breed}"
}
}
}
div {
match breed.result() {
None => rsx! { div { "Click the button to fetch a dog!" } },
Some(Err(_e)) => rsx! { div { "Failed to fetch a dog, please try again." } },
Some(Ok(res)) => rsx! {
img {
max_width: "500px",
max_height: "500px",
src: "{res.read().message}"
}
},
}
}
}
}
@@ -1,7 +1,7 @@
[package]
name = "ecommerce-site"
version = "0.1.1"
edition = "2018"
edition = "2021"
publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -1,9 +1,27 @@
#![allow(unused)]
use chrono::{DateTime, Utc};
use dioxus::prelude::Result;
use serde::{Deserialize, Serialize};
use std::fmt::Display;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
// Cache up to 100 requests, invalidating them after 60 seconds
pub(crate) async fn fetch_product(product_id: usize) -> Result<Product> {
Ok(
reqwest::get(format!("https://fakestoreapi.com/products/{product_id}"))
.await?
.json()
.await?,
)
}
// Cache up to 100 requests, invalidating them after 60 seconds
pub(crate) async fn fetch_products(count: usize, sort: Sort) -> Result<Vec<Product>> {
Ok(reqwest::get(format!(
"https://fakestoreapi.com/products/?sort={sort}&limit={count}"
))
.await?
.json()
.await?)
}
#[derive(Serialize, Deserialize, PartialEq, Clone, Debug, Default)]
pub(crate) struct Product {
@@ -54,48 +72,8 @@ impl Display for Sort {
}
}
// Cache up to 100 requests, invalidating them after 60 seconds
pub(crate) async fn fetch_user_carts(user_id: usize) -> Result<Vec<Cart>, reqwest::Error> {
reqwest::get(format!(
"https://fakestoreapi.com/carts/user/{user_id}?startdate=2019-12-10&enddate=2023-01-01"
))
.await?
.json()
.await
}
// Cache up to 100 requests, invalidating them after 60 seconds
pub(crate) async fn fetch_user(user_id: usize) -> dioxus::Result<Product> {
Ok(
reqwest::get(format!("https://fakestoreapi.com/users/{user_id}"))
.await?
.json()
.await?,
)
}
// Cache up to 100 requests, invalidating them after 60 seconds
pub(crate) async fn fetch_product(product_id: usize) -> dioxus::Result<Product> {
Ok(
reqwest::get(format!("https://fakestoreapi.com/products/{product_id}"))
.await?
.json()
.await?,
)
}
// Cache up to 100 requests, invalidating them after 60 seconds
pub(crate) async fn fetch_products(count: usize, sort: Sort) -> dioxus::Result<Vec<Product>> {
Ok(reqwest::get(format!(
"https://fakestoreapi.com/products/?sort={sort}&limit={count}"
))
.await?
.json()
.await?)
}
#[derive(Serialize, Deserialize)]
pub(crate) struct User {
struct User {
id: usize,
email: String,
username: String,
@@ -104,14 +82,6 @@ pub(crate) struct User {
phone: String,
}
impl User {
async fn fetch_most_recent_cart(&self) -> Result<Option<Cart>, reqwest::Error> {
let all_carts = fetch_user_carts(self.id).await?;
Ok(all_carts.into_iter().max_by_key(|cart| cart.date))
}
}
#[derive(Serialize, Deserialize)]
struct FullName {
firstname: String,
@@ -119,7 +89,7 @@ struct FullName {
}
#[derive(Serialize, Deserialize, Clone)]
pub(crate) struct Cart {
struct Cart {
id: usize,
#[serde(rename = "userId")]
user_id: usize,
@@ -128,29 +98,9 @@ pub(crate) struct Cart {
date: DateTime<Utc>,
}
impl Cart {
async fn update_database(&mut self) -> Result<(), reqwest::Error> {
let id = self.id;
let client = reqwest::Client::new();
*self = client
.put(format!("https://fakestoreapi.com/carts/{id}"))
.send()
.await?
.json()
.await?;
Ok(())
}
}
#[derive(Serialize, Deserialize, Clone)]
pub(crate) struct ProductInCart {
struct ProductInCart {
#[serde(rename = "productId")]
product_id: usize,
quantity: usize,
}
impl ProductInCart {
pub async fn fetch_product(&self) -> Result<Product, dioxus::CapturedError> {
fetch_product(self.product_id).await
}
}
@@ -0,0 +1,23 @@
// The homepage is statically rendered, so we don't need to a persistent websocket connection.
use crate::{
api::{fetch_products, Sort},
components::nav::Nav,
components::product_item::ProductItem,
};
use dioxus::prelude::*;
pub(crate) fn Home() -> Element {
let products = use_loader(|| fetch_products(10, Sort::Ascending))?;
rsx! {
Nav {}
section { class: "p-10",
for product in products.iter() {
ProductItem {
product: product.clone()
}
}
}
}
}
@@ -0,0 +1,12 @@
use dioxus::prelude::*;
#[component]
pub(crate) fn ChildrenOrLoading(children: Element) -> Element {
rsx! {
Stylesheet { href: asset!("/public/loading.css") }
SuspenseBoundary {
fallback: |_| rsx! { div { class: "spinner", } },
{children}
}
}
}
@@ -1,6 +1,7 @@
use dioxus::prelude::*;
pub fn nav() -> Element {
#[component]
pub fn Nav() -> Element {
rsx! {
section { class: "relative",
nav { class: "flex justify-between border-b",
@@ -3,7 +3,7 @@ use dioxus::prelude::*;
use crate::api::Product;
#[component]
pub(crate) fn product_item(product: Product) -> Element {
pub(crate) fn ProductItem(product: Product) -> Element {
let Product {
id,
title,
@@ -3,44 +3,11 @@ use std::{fmt::Display, str::FromStr};
use crate::api::{fetch_product, Product};
use dioxus::prelude::*;
#[derive(Default)]
enum Size {
Small,
#[default]
Medium,
Large,
}
impl Display for Size {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Size::Small => "small".fmt(f),
Size::Medium => "medium".fmt(f),
Size::Large => "large".fmt(f),
}
}
}
impl FromStr for Size {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
use Size::*;
match s.to_lowercase().as_str() {
"small" => Ok(Small),
"medium" => Ok(Medium),
"large" => Ok(Large),
_ => Err(()),
}
}
}
#[component]
pub fn product_page(product_id: ReadSignal<usize>) -> Element {
pub fn ProductPage(product_id: ReadSignal<usize>) -> Element {
let mut quantity = use_signal(|| 1);
let mut size = use_signal(Size::default);
let product = use_server_future(move || fetch_product(product_id()))?;
let product = use_loader(move || fetch_product(product_id()))?;
let Product {
title,
@@ -50,7 +17,7 @@ pub fn product_page(product_id: ReadSignal<usize>) -> Element {
image,
rating,
..
} = product().unwrap()?;
} = product();
rsx! {
section { class: "py-20",
@@ -216,6 +183,38 @@ pub fn product_page(product_id: ReadSignal<usize>) -> Element {
}
}
#[derive(Default)]
enum Size {
Small,
#[default]
Medium,
Large,
}
impl Display for Size {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Size::Small => "small".fmt(f),
Size::Medium => "medium".fmt(f),
Size::Large => "large".fmt(f),
}
}
}
impl FromStr for Size {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
use Size::*;
match s.to_lowercase().as_str() {
"small" => Ok(Small),
"medium" => Ok(Medium),
"large" => Ok(Large),
_ => Err(()),
}
}
}
mod icons {
use super::*;
@@ -43,8 +43,8 @@ enum Route {
fn Details(product_id: usize) -> Element {
rsx! {
div {
components::nav::nav {}
components::product_page::product_page {
components::nav::Nav {}
components::product_page::ProductPage {
product_id
}
}
@@ -8,25 +8,19 @@
use std::env::current_dir;
use std::path::PathBuf;
use dioxus::desktop::{Config, WindowBuilder};
use dioxus::prelude::*;
fn main() {
dioxus::LaunchBuilder::desktop()
.with_cfg(Config::new().with_window(WindowBuilder::new().with_resizable(true)))
.launch(app)
dioxus::launch(app);
}
fn app() -> Element {
let mut files = use_signal(Files::new);
rsx! {
document::Link {
rel: "stylesheet",
href: asset!("/assets/fileexplorer.css")
}
Stylesheet { href: asset!("/assets/fileexplorer.css") }
Stylesheet { href: "https://fonts.googleapis.com/icon?family=Material+Icons" }
div {
document::Link { href: "https://fonts.googleapis.com/icon?family=Material+Icons", rel: "stylesheet" }
header {
i { class: "material-icons icon-menu", "menu" }
h1 { "Files: " {files.read().current()} }
@@ -13,45 +13,37 @@ use svg_attributes::to;
fn main() {
LaunchBuilder::new()
.with_cfg(server_only! {
dioxus::fullstack::ServeConfig::builder().enable_out_of_order_streaming()
dioxus::server::ServeConfig::builder().enable_out_of_order_streaming()
})
.launch(|| rsx! { Router::<Route> {} });
.launch(|| {
rsx! {
Stylesheet { href: asset!("/assets/hackernews.css") }
Router::<Route> {}
}
});
}
#[derive(Clone, Routable)]
enum Route {
#[redirect("/", || Route::Homepage { story: PreviewState { active_story: None } })]
#[route("/:story")]
Homepage { story: PreviewState },
}
pub fn App() -> Element {
rsx! {
Router::<Route> {}
}
#[route("/story&:story")]
StoryPreview { story: Option<i64> },
}
#[component]
fn Homepage(story: ReadSignal<PreviewState>) -> Element {
fn StoryPreview(story: ReadSignal<Option<i64>>) -> Element {
rsx! {
document::Link { rel: "stylesheet", href: asset!("/assets/hackernews.css") }
div { display: "flex", flex_direction: "row", width: "100%",
div {
width: "50%",
SuspenseBoundary {
fallback: |context: SuspenseContext| rsx! {
"Loading..."
},
div { width: "50%",
SuspenseBoundary { fallback: |context| rsx! { "Loading..." },
Stories {}
}
}
div { width: "50%",
SuspenseBoundary {
fallback: |context: SuspenseContext| rsx! {
"Loading preview..."
},
Preview {
story
SuspenseBoundary { fallback: |context| rsx! { "Loading preview..." },
if let Some(story) = story() {
Preview { story_id: story }
} else {
div { padding: "0.5rem", "Select a story to preview" }
}
}
}
@@ -61,32 +53,31 @@ fn Homepage(story: ReadSignal<PreviewState>) -> Element {
#[component]
fn Stories() -> Element {
let stories: Resource<dioxus::Result<Vec<i64>>> = use_server_future(move || async move {
let url = format!("{}topstories.json", BASE_API_URL);
let mut stories_ids = reqwest::get(&url).await?.json::<Vec<i64>>().await?;
stories_ids.truncate(30);
Ok(stories_ids)
let stories = use_loader(move || async move {
let stories_ids = reqwest::get(&format!("{}topstories.json", BASE_API_URL))
.await?
.json::<Vec<i64>>()
.await?
.into_iter()
.take(30)
.collect::<Vec<i64>>();
dioxus::Ok(stories_ids)
})?;
match stories().unwrap() {
Ok(list) => rsx! {
div {
for story in list {
ChildrenOrLoading {
key: "{story}",
StoryListing { story }
}
rsx! {
div {
for story in stories() {
ChildrenOrLoading { key: "{story}",
StoryListing { story }
}
}
},
Err(err) => rsx! {"An error occurred while fetching stories {err}"},
}
}
}
#[component]
fn StoryListing(story: ReadSignal<i64>) -> Element {
let story = use_server_future(move || get_story(story()))?;
let story = use_loader(move || get_story(story()))?;
let StoryItem {
title,
url,
@@ -96,7 +87,7 @@ fn StoryListing(story: ReadSignal<i64>) -> Element {
kids,
id,
..
} = story().unwrap()?.item;
} = story().item;
let url = url.as_deref().unwrap_or_default();
let hostname = url
@@ -121,7 +112,7 @@ fn StoryListing(story: ReadSignal<i64>) -> Element {
position: "relative",
div { font_size: "1.5rem",
Link {
to: Route::Homepage { story: PreviewState { active_story: Some(id) } },
to: Route::StoryPreview { story: Some(id) },
"{title}"
}
a {
@@ -141,44 +132,9 @@ fn StoryListing(story: ReadSignal<i64>) -> Element {
}
}
#[derive(Clone, Debug, Default)]
struct PreviewState {
active_story: Option<i64>,
}
impl FromStr for PreviewState {
type Err = ParseIntError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let state = i64::from_str(s)?;
Ok(PreviewState {
active_story: Some(state),
})
}
}
impl Display for PreviewState {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
if let Some(id) = &self.active_story {
write!(f, "{id}")?;
}
Ok(())
}
}
#[component]
fn Preview(story: ReadSignal<PreviewState>) -> Element {
let PreviewState {
active_story: Some(id),
} = story()
else {
return rsx! {"Click on a story to preview it here"};
};
let story = use_server_future(use_reactive!(|id| get_story(id)))?;
let story = story().unwrap()?;
fn Preview(story_id: ReadSignal<i64>) -> Element {
let story = use_loader(move || get_story(story_id()))?.cloned();
rsx! {
div { padding: "0.5rem",
div { font_size: "1.5rem", a { href: story.item.url, "{story.item.title}" } }
@@ -194,13 +150,14 @@ fn Preview(story: ReadSignal<PreviewState>) -> Element {
}
#[component]
fn Comment(comment: i64) -> Element {
let comment: Resource<dioxus::Result<CommentData>> =
use_server_future(use_reactive!(|comment| async move {
let url = format!("{}{}{}.json", BASE_API_URL, ITEM_API, comment);
let mut comment = reqwest::get(&url).await?.json::<CommentData>().await?;
Ok(comment)
}))?;
fn Comment(comment: ReadSignal<i64>) -> Element {
let comment = use_loader(move || async move {
let mut comment = reqwest::get(&format!("{}{}{}.json", BASE_API_URL, ITEM_API, comment))
.await?
.json::<CommentData>()
.await?;
dioxus::Ok(comment)
})?;
let CommentData {
by,
@@ -209,7 +166,7 @@ fn Comment(comment: i64) -> Element {
id,
kids,
..
} = comment().unwrap()?;
} = comment();
rsx! {
div { padding: "0.5rem",
@@ -272,33 +229,21 @@ pub struct StoryItem {
pub r#type: String,
}
pub async fn get_story(id: i64) -> dioxus::Result<StoryPageData> {
let url = format!("{}{}{}.json", BASE_API_URL, ITEM_API, id);
Ok(reqwest::get(&url).await?.json::<StoryPageData>().await?)
pub async fn get_story(id: i64) -> Result<StoryPageData> {
Ok(
reqwest::get(&format!("{}{}{}.json", BASE_API_URL, ITEM_API, id))
.await?
.json::<StoryPageData>()
.await?,
)
}
#[component]
fn ChildrenOrLoading(children: Element) -> Element {
rsx! {
SuspenseBoundary {
fallback: |context: SuspenseContext| {
rsx! {
if let Some(placeholder) = context.suspense_placeholder() {
{placeholder}
} else {
LoadingIndicator {}
}
}
},
fallback: |_| rsx! { div { class: "spinner", } },
children
}
}
}
fn LoadingIndicator() -> Element {
rsx! {
div {
class: "spinner",
}
}
}
File renamed without changes.
@@ -11,9 +11,10 @@ reqwest = { workspace = true, features = ["json"] }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
rusqlite = { version = "0.32.0", optional = true }
anyhow = { workspace = true }
[features]
default = ["web"]
default = ["web", "server"]
web = ["dioxus/web"]
desktop = ["dioxus/desktop"]
mobile = ["dioxus/mobile"]
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,41 @@
use anyhow::Result;
use dioxus::prelude::*;
#[cfg(feature = "server")]
thread_local! {
static DB: std::sync::LazyLock<rusqlite::Connection> = std::sync::LazyLock::new(|| {
let conn = rusqlite::Connection::open("hotdogdb/hotdog.db").expect("Failed to open database");
conn.execute_batch(
"CREATE TABLE IF NOT EXISTS dogs (
id INTEGER PRIMARY KEY,
url TEXT NOT NULL
);",
)
.unwrap();
conn
});
}
#[get("/api/dogs")]
pub async fn list_dogs() -> Result<Vec<(usize, String)>> {
DB.with(|db| {
Ok(db
.prepare("SELECT id, url FROM dogs ORDER BY id DESC LIMIT 10")?
.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?
.collect::<Result<Vec<(usize, String)>, rusqlite::Error>>()?)
})
}
#[delete("/api/dogs/{id}")]
pub async fn remove_dog(id: usize) -> Result<()> {
DB.with(|db| db.execute("DELETE FROM dogs WHERE id = ?1", [id]))?;
Ok(())
}
#[post("/api/dogs")]
pub async fn save_dog(image: String) -> Result<()> {
DB.with(|db| db.execute("INSERT INTO dogs (url) VALUES (?1)", [&image]))?;
Ok(())
}
@@ -0,0 +1,71 @@
use dioxus::prelude::*;
use crate::{
backend::{list_dogs, remove_dog, save_dog},
Route,
};
#[component]
pub fn Favorites() -> Element {
let mut favorites = use_loader(list_dogs)?;
rsx! {
div { id: "favorites",
for (id , url) in favorites.cloned() {
div { class: "favorite-dog", key: "{id}",
img { src: "{url}" }
button {
onclick: move |_| async move {
_ = remove_dog(id).await;
favorites.restart();
},
"❌"
}
}
}
}
}
}
#[component]
pub fn NavBar() -> Element {
rsx! {
div { id: "title",
span {}
Link { to: Route::DogView, h1 { "🌭 HotDog! " } }
Link { to: Route::Favorites, id: "heart", "♥️" }
}
Outlet::<Route> {}
}
}
#[component]
pub fn DogView() -> Element {
let mut img_src = use_loader(|| async move {
anyhow::Ok(
reqwest::get("https://dog.ceo/api/breeds/image/random")
.await?
.json::<serde_json::Value>()
.await?["message"]
.to_string(),
)
})?;
rsx! {
div { id: "dogview",
img { id: "dogimg", src: "{img_src}" }
}
div { id: "buttons",
button {
id: "skip",
onclick: move |_| img_src.restart(),
"skip"
}
button {
id: "save",
onclick: move |_| async move { _ = save_dog(img_src()).await },
"save!"
}
}
}
}
@@ -1,8 +1,8 @@
mod backend;
mod components;
mod frontend;
use components::{DogView, Favorites, NavBar};
use dioxus::prelude::*;
use frontend::*;
#[derive(Routable, PartialEq, Clone)]
enum Route {
@@ -16,14 +16,14 @@ enum Route {
fn main() {
#[cfg(not(feature = "server"))]
server_fn::client::set_server_url("https://hot-dog.fly.dev");
dioxus::fullstack::set_server_url("https://hot-dog.fly.dev");
dioxus::launch(app);
}
fn app() -> Element {
rsx! {
document::Stylesheet { href: asset!("/assets/main.css") }
Stylesheet { href: asset!("/assets/main.css") }
Router::<Route> {}
}
}
@@ -0,0 +1,97 @@
use dioxus::prelude::*;
fn main() {
dioxus::launch(app)
}
fn app() -> Element {
let mut api_key = use_signal(|| "".to_string());
let mut prompt = use_signal(|| "".to_string());
let mut num_images = use_signal(|| 1.to_string());
let mut image = use_action(move || async move {
#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Props, Clone, Default)]
struct ImageResponse {
created: i32,
data: Vec<UrlImage>,
}
#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Props, Clone)]
struct UrlImage {
url: String,
}
if api_key.peek().is_empty() || prompt.peek().is_empty() || num_images.peek().is_empty() {
return dioxus::Ok(ImageResponse::default());
}
let res = reqwest::Client::new()
.post("https://api.openai.com/v1/images/generations")
.json(&serde_json::json!({
"prompt": prompt.cloned(),
"n": num_images.cloned().parse::<i32>().unwrap_or(1),
"size":"1024x1024",
}))
.bearer_auth(api_key)
.send()
.await?
.json::<ImageResponse>()
.await?;
Ok(res)
});
rsx! {
Stylesheet { href: "https://unpkg.com/bulma@0.9.0/css/bulma.min.css" }
div { class: "container",
div { class: "columns",
div { class: "column",
input { class: "input is-primary mt-4",
value: "{api_key}",
r#type: "text",
placeholder: "Your OpenAI API Key",
oninput: move |evt| api_key.set(evt.value()),
}
input { class: "input is-primary mt-4",
placeholder: "MAX 1000 Dgts",
r#type: "text",
value:"{prompt}",
oninput: move |evt| prompt.set(evt.value())
}
input { class: "input is-primary mt-4",
r#type: "number",
min:"1",
max:"10",
value:"{num_images}",
oninput: move |evt| num_images.set(evt.value()),
}
}
}
button {
class: "button is-primary",
class: if image.is_pending() { "is-loading" },
onclick: move |_| {
image.call();
},
"Generate image"
}
if let Some(image) = image.value() {
for image in image.read().data.as_slice() {
section { class: "is-flex",
div { class: "container is-fluid",
div { class: "container has-text-centered",
div { class: "is-justify-content-center",
div { class: "level",
div { class: "level-item",
figure { class: "image", img { alt: "", src: "{image.url}", } }
}
}
}
}
}
}
}
}
}
}
}
File renamed without changes.
@@ -63,7 +63,7 @@ fn app() -> Element {
};
rsx! {
document::Link { rel: "stylesheet", href: STYLE }
Stylesheet { href: STYLE }
section { class: "todoapp",
TodoHeader { todos }
section { class: "main",
@@ -124,7 +124,7 @@ fn app() -> Element {
};
rsx! {
document::Link { rel: "stylesheet", href: STYLE }
Stylesheet { href: STYLE }
section { class: "todoapp",
TodoHeader { todos }
section { class: "main",
@@ -1,7 +1,8 @@
#![allow(non_snake_case)]
use dioxus::prelude::*;
use dioxus::{fullstack::Loading, prelude::*};
use serde::{Deserialize, Serialize};
use std::fmt::Display;
fn main() {
dioxus::launch(app);
@@ -16,10 +17,10 @@ fn app() -> Element {
id: 2950159,
});
let current_weather = use_resource(move || async move { get_weather(&country()).await });
let current_weather = use_loader(move || get_weather(country()));
rsx! {
document::Link { rel: "stylesheet", href: "https://unpkg.com/tailwindcss@^2.0/dist/tailwind.min.css" }
Stylesheet { href: "https://unpkg.com/tailwindcss@^2.0/dist/tailwind.min.css" }
div { class: "mx-auto p-4 bg-gray-100 h-screen flex justify-center",
div { class: "flex items-center justify-center flex-row",
div { class: "flex items-start justify-center flex-row",
@@ -27,14 +28,20 @@ fn app() -> Element {
div { class: "flex flex-wrap w-full px-2",
div { class: "bg-gray-900 text-white relative min-w-0 break-words rounded-lg overflow-hidden shadow-sm mb-4 w-full dark:bg-gray-600",
div { class: "px-6 py-6 relative",
if let Some(Ok(weather)) = current_weather.read().as_ref() {
CountryData {
country: country.read().clone(),
weather: weather.clone(),
match current_weather {
Ok(weather) => rsx! {
CountryData {
country: country.read().clone(),
weather: weather.cloned(),
}
Forecast { weather: weather.cloned() }
},
Err(Loading::Pending(_)) => rsx! {
div { "Loading weather data..." }
},
Err(Loading::Failed(_)) => rsx! {
div { "Failed to load weather data." }
}
Forecast { weather: weather.clone() }
} else {
p { "Loading.." }
}
}
}
@@ -110,8 +117,7 @@ fn Forecast(weather: WeatherResponse) -> Element {
#[component]
fn SearchBox(mut country: WriteSignal<WeatherLocation>) -> Element {
let mut input = use_signal(|| "".to_string());
let locations = use_resource(move || async move { get_locations(&input()).await });
let locations = use_resource(move || get_locations(input()));
rsx! {
div {
@@ -197,7 +203,7 @@ struct SearchResponse {
results: WeatherLocations,
}
async fn get_locations(input: &str) -> reqwest::Result<WeatherLocations> {
async fn get_locations(input: impl Display) -> reqwest::Result<WeatherLocations> {
let res = reqwest::get(&format!(
"https://geocoding-api.open-meteo.com/v1/search?name={input}"
))
@@ -226,13 +232,9 @@ struct DailyWeather {
temperature_2m_max: Vec<f32>,
}
async fn get_weather(location: &WeatherLocation) -> reqwest::Result<WeatherResponse> {
let res = reqwest::get(&format!("https://api.open-meteo.com/v1/forecast?latitude={}&longitude={}&hourly=temperature_2m&daily=temperature_2m_max,temperature_2m_min,apparent_temperature_max,apparent_temperature_min&timezone=GMT", location.latitude, location.longitude))
.await
?
async fn get_weather(location: WeatherLocation) -> reqwest::Result<WeatherResponse> {
reqwest::get(&format!("https://api.open-meteo.com/v1/forecast?latitude={}&longitude={}&hourly=temperature_2m&daily=temperature_2m_max,temperature_2m_min,apparent_temperature_max,apparent_temperature_min&timezone=GMT", location.latitude, location.longitude))
.await?
.json::<WeatherResponse>()
.await
?;
Ok(res)
}
+162
View File
@@ -0,0 +1,162 @@
//! A websocket chat demo using Dioxus' built-in websocket support.
//!
//! We setup an endpoint at `/api/chat` that accepts a `name` and `user_id` query parameter.
//! Each client connects to that endpoint, and we use a `tokio::broadcast` channel
//! to send messages to all connected clients.
//!
//! In practice, you'd use a distributed messaging system (Redis PubSub / Kafka / etc) to coordinate
//! between multiple server instances and an additional database to persist chat history.
use dioxus::fullstack::{WebSocketOptions, Websocket, use_websocket};
use dioxus::prelude::*;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
fn main() {
println!("IP and PORT: {:#?}", std::env::var("IP"));
println!("IP and PORT: {:#?}", std::env::var("PORT"));
dioxus::launch(app);
}
fn app() -> Element {
// store the user's current input
let mut input = use_signal(|| "".to_string());
// Select a unique id for the user, and then use that entropy to pick a random name
let user_id = use_signal(uuid::Uuid::new_v4);
let user_name = use_signal(|| {
match user_id.read().as_bytes()[0] % 7 {
0 => "Alice",
1 => "Bob",
2 => "Eve",
3 => "Mallory",
4 => "Trent",
5 => "Peggy",
6 => "Victor",
_ => "Charlie",
}
.to_string()
});
// Store the messages we've received from the server
let mut message_list = use_signal(Vec::<ChatMessage>::new);
// Connect to the websocket endpoint
let mut socket =
use_websocket(move || uppercase_ws(user_name(), user_id(), Default::default()));
use_future(move || async move {
while let Ok(msg) = socket.recv().await {
match msg {
ServerEvent::ReceiveMessage(message) => message_list.write().push(message),
ServerEvent::Connected { messages } => message_list.set(messages),
}
}
});
rsx! {
h1 { "WebSocket Chat" }
p { "Connection status: {socket.status():?} as {user_name}" }
input {
placeholder: "Type a message",
value: "{input}",
oninput: move |e| async move { input.set(e.value()) },
onkeydown: move |e| async move {
if e.key() == Key::Enter {
_ = socket.send(ClientEvent::SendMessage(input.read().clone())).await;
input.set("".to_string());
}
}
}
div {
for message in message_list.read().iter().rev() {
pre { "{message.name}: {message.message}" }
}
}
}
}
/// The events that the client can send to the server
#[derive(Serialize, Deserialize, Debug)]
enum ClientEvent {
SendMessage(String),
}
/// The events that the server can send to the client
#[derive(Serialize, Deserialize, Debug)]
enum ServerEvent {
Connected { messages: Vec<ChatMessage> },
ReceiveMessage(ChatMessage),
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
struct ChatMessage {
user_id: Uuid,
name: String,
message: String,
}
#[get("/api/chat?name&user_id")]
async fn uppercase_ws(
name: String,
user_id: Uuid,
options: WebSocketOptions,
) -> Result<Websocket<ClientEvent, ServerEvent>> {
use std::sync::LazyLock;
use tokio::sync::{
Mutex,
broadcast::{self, Sender},
};
// Every chat app needs a chat room! For this demo, we just use a tokio broadcast channel and a mutex-protected
// list of messages to store chat history.
//
// We place these types in the body of this serverfn since they're not used on the client, only the server.
static MESSAGES: LazyLock<Mutex<Vec<ChatMessage>>> = LazyLock::new(|| Mutex::new(Vec::new()));
static BROADCAST: LazyLock<Sender<ChatMessage>> = LazyLock::new(|| broadcast::channel(100).0);
Ok(options.on_upgrade(move |mut socket| async move {
// Send back all the messages from the room to the new client
let messages = MESSAGES.lock().await.clone();
_ = socket.send(ServerEvent::Connected { messages }).await;
// Subscriber to the broadcast channel
let sender = BROADCAST.clone();
let mut broadcast = sender.subscribe();
// Announce that we've joined
let _ = sender.send(ChatMessage {
message: format!("{name} has connected."),
user_id,
name: "[CONSOLE]".to_string(),
});
// Loop poll the broadcast receiver and the websocket for new messages
// If we receive a message from the broadcast channel, send it to the client
// If we receive a message from the client, broadcast it to all other clients and save it to the message list
loop {
tokio::select! {
Ok(msg) = broadcast.recv() => {
let _ = socket.send(ServerEvent::ReceiveMessage(msg)).await;
}
Ok(ClientEvent::SendMessage(message)) = socket.recv() => {
let chat_message = ChatMessage {
user_id,
name: name.clone(),
message,
};
let _ = sender.send(chat_message.clone());
MESSAGES.lock().await.push(chat_message.clone());
},
else => break,
}
}
_ = sender.send(ChatMessage {
name: "[CONSOLE]".to_string(),
message: format!("{name} has disconnected."),
user_id,
});
}))
}
@@ -12,8 +12,9 @@ fn app() -> Element {
let mut disabled = use_signal(|| false);
rsx! {
div { style: "text-align: center; margin: 20px; display: flex; flex-direction: column; align-items: center;",
button { onclick: move |_| disabled.toggle(),
div { text_align: "center", margin: "20px", display: "flex", flex_direction: "column", align_items: "center",
button {
onclick: move |_| disabled.toggle(),
"click to "
if disabled() { "enable" } else { "disable" }
" the lower button"
File renamed without changes.
@@ -7,7 +7,7 @@
//! If you `go-to-definition` on the `svg` element, you'll see its custom namespace.
use dioxus::prelude::*;
use rand::{rng, Rng};
use rand::{Rng, rng};
fn main() {
dioxus::launch(|| {
@@ -45,7 +45,7 @@ fn Dice() -> Element {
value.set(rng().random_range(1..=6))
},
rect { x: -1000, y: -1000, width: 2000, height: 2000, rx: 200, fill: "#aaa" }
for ((x, y), _) in DOTS.iter().zip(active_dots.read().iter()).filter(|(_, &active)| active) {
for ((x, y), _) in DOTS.iter().zip(active_dots.read().iter()).filter(|(_, active)| **active) {
circle {
cx: *x * 600,
cy: *y * 600,
File renamed without changes.
@@ -20,11 +20,11 @@ fn app() -> Element {
return;
}
response.respond(Response::new(include_bytes!("./assets/logo.png").to_vec()));
response.respond(Response::new(include_bytes!("../assets/logo.png").to_vec()));
});
rsx! {
document::Link { rel: "stylesheet", href: STYLE }
Stylesheet { href: STYLE }
h1 { "Dynamic Assets" }
img { src: "/logos/logo.png" }
}
File renamed without changes.
@@ -0,0 +1,20 @@
//! This example shows how to add metadata to the page with the Meta component
use dioxus::prelude::*;
fn main() {
dioxus::launch(app);
}
fn app() -> Element {
rsx! {
// You can use the Meta component to render a meta tag into the head of the page
// Meta tags are useful to provide information about the page to search engines and social media sites
// This example sets up meta tags for the open graph protocol for social media previews
Meta { property: "og:title", content: "My Site" }
Meta { property: "og:type", content: "website" }
Meta { property: "og:url", content: "https://www.example.com" }
Meta { property: "og:image", content: "https://example.com/image.jpg" }
Meta { name: "description", content: "My Site is a site" }
}
}
@@ -21,7 +21,7 @@ fn app() -> Element {
use_context_provider(|| Signal::new(Theme::Light));
rsx! {
document::Link { rel: "stylesheet", href: STYLE }
Stylesheet { href: STYLE }
main {
class: "main-container",
@@ -54,9 +54,20 @@ fn Show() -> Element {
ErrorBoundary {
handle_error: |errors: ErrorContext| {
rsx! {
for error in errors.errors() {
if let Some(error) = error.show() {
{error}
for error in errors.error() {
// You can downcast the error to see if it's a specific type and render something specific for it
if let Some(_error) = error.downcast_ref::<std::num::ParseIntError>() {
div {
background_color: "red",
border: "black",
border_width: "2px",
border_radius: "5px",
p { "Failed to parse data" }
Link {
to: Route::Home {},
"Go back to the homepage"
}
}
} else {
pre {
color: "red",
@@ -79,24 +90,8 @@ fn ParseNumberWithShow() -> Element {
button {
onclick: move |_| {
let request_data = "0.5";
let data: i32 = request_data.parse()
// You can attach rsx to results that can be displayed in the Error Boundary
.show(|_| rsx! {
div {
background_color: "red",
border: "black",
border_width: "2px",
border_radius: "5px",
p { "Failed to parse data" }
Link {
to: Route::Home {},
"Go back to the homepage"
}
}
})?;
let data: i32 = request_data.parse()?;
println!("parsed {data}");
Ok(())
},
"Click to throw an error"
@@ -18,7 +18,7 @@ fn main() {
fn app() -> Element {
rsx! {
document::Link { rel: "stylesheet", href: STYLE }
Stylesheet { href: STYLE }
Increment {}
Decrement {}
Reset {}
File renamed without changes.
@@ -17,7 +17,7 @@ fn app() -> Element {
let mut state = use_signal(|| PlayerState { is_playing: false });
rsx!(
document::Link { rel: "stylesheet", href: STYLE }
Stylesheet { href: STYLE }
h1 {"Select an option"}
// Add some cute animations if the radio is playing!
@@ -68,7 +68,7 @@ fn app() -> Element {
}
// We can also use the signal value as a slice
if let [ref first, .., ref last] = saved_values.read().as_slice() {
if let [first, .., last] = saved_values.read().as_slice() {
li { "First and last: {first}, {last}" }
} else {
"No saved values"
@@ -12,7 +12,7 @@ fn app() -> Element {
let mut millis = use_signal(|| 0);
use_future(move || async move {
// Save our initial timea
// Save our initial time
let start = Instant::now();
loop {
File renamed without changes.
@@ -2,7 +2,7 @@
use async_std::task::sleep;
use dioxus::prelude::*;
use futures_util::{future, stream, Stream, StreamExt};
use futures_util::{Stream, StreamExt, future, stream};
fn main() {
dioxus::launch(app);
+62
View File
@@ -0,0 +1,62 @@
//! Suspense in Dioxus
//!
//! Suspense allows components to bubble up loading states to parent components, simplifying data fetching.
use dioxus::prelude::*;
fn main() {
dioxus::launch(app)
}
fn app() -> Element {
rsx! {
div {
h1 { "Dogs are very important" }
p {
"The dog or domestic dog (Canis familiaris[4][5] or Canis lupus familiaris[5])"
"is a domesticated descendant of the wolf which is characterized by an upturning tail."
"The dog derived from an ancient, extinct wolf,[6][7] and the modern grey wolf is the"
"dog's nearest living relative.[8] The dog was the first species to be domesticated,[9][8]"
"by hunter–gatherers over 15,000 years ago,[7] before the development of agriculture.[1]"
}
h3 { "Illustrious Dog Photo" }
ErrorBoundary { handle_error: |_| rsx! { p { "Error loading doggos" } },
SuspenseBoundary { fallback: move |_| rsx! { "Loading doggos..." },
Doggo {}
}
}
}
}
}
#[component]
fn Doggo() -> Element {
// `use_loader` returns a Result<Loader<T>, Loading>. Loading can either be "Pending" or "Failed".
// When we use the `?` operator, the pending/error state will be thrown to the nearest Suspense or Error boundary.
//
// During SSR, `use_loader` will serialize the contents of the fetch, and during hydration, the client will
// use the pre-fetched data instead of re-fetching to render.
let mut dog = use_loader(move || async move {
#[derive(serde::Deserialize, serde::Serialize, PartialEq)]
struct DogApi {
message: String,
}
reqwest::get("https://dog.ceo/api/breeds/image/random/")
.await
.unwrap()
.json::<DogApi>()
.await
})?;
rsx! {
button { onclick: move |_| dog.restart(), "Click to fetch another doggo" }
div {
img {
max_width: "500px",
max_height: "500px",
src: "{dog.read().message}"
}
}
}
}
@@ -14,7 +14,7 @@ const STYLE: Asset = asset!("/examples/assets/flat_router.css");
fn main() {
dioxus::launch(|| {
rsx! {
document::Link { rel: "stylesheet", href: STYLE }
Stylesheet { href: STYLE }
Router::<Route> {}
}
})
File renamed without changes.
@@ -16,7 +16,7 @@ fn main() {
fn app() -> Element {
rsx! (
document::Link { rel: "stylesheet", href: STYLE }
Stylesheet { href: STYLE }
Router::<Route> {}
)
}
File renamed without changes.
@@ -13,7 +13,7 @@ const STYLE: Asset = asset!("/examples/assets/router.css");
fn main() {
dioxus::launch(|| {
rsx! {
document::Link { rel: "stylesheet", href: STYLE }
Stylesheet { href: STYLE }
Router::<Route> {}
}
});
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,61 @@
[package]
name = "fullstack-auth-example-with-fullstack"
version = "0.1.0"
edition = "2021"
publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dioxus-web = { workspace = true, features = ["hydrate"], optional = true }
dioxus = { features = ["fullstack"], workspace = true }
axum = { workspace = true, optional = true, features = ["macros"]}
tokio = { workspace = true, features = ["full"], optional = true }
tower-http = { workspace = true, features = ["auth"], optional = true }
async-trait = { version = "0.1.88", optional = true }
sqlx = { version = "0.8.6", features = [
"macros",
"migrate",
"postgres",
"sqlite",
"_unstable-all-types",
"tls-native-tls",
"runtime-tokio",
], optional = true }
http = { workspace = true, optional = true }
tower = { workspace = true, optional = true }
execute = "0.2.13"
serde = { workspace = true }
anyhow = { workspace = true }
[dependencies.axum_session]
workspace = true
optional = true
[dependencies.axum_session_auth]
workspace = true
optional = true
[dependencies.axum_session_sqlx]
workspace = true
features = ["sqlite"]
optional = true
[features]
default = ["web", "server"]
server = [
"dioxus/server",
"dep:axum",
"dep:tokio",
"dep:tower-http",
"dep:async-trait",
"dep:sqlx",
"dep:axum_session",
"dep:axum_session_auth",
"dep:axum_session_sqlx",
"dep:http",
"dep:tower",
]
web = ["dioxus/web", "dep:dioxus-web"]
@@ -0,0 +1,81 @@
//! The code here is pulled from the `axum-session-auth` crate examples, requiring little to no
//! modification to work with dioxus fullstack.
use async_trait::async_trait;
use axum_session_auth::*;
use axum_session_sqlx::SessionSqlitePool;
use serde::{Deserialize, Serialize};
use sqlx::sqlite::SqlitePool;
use std::collections::HashSet;
pub(crate) type Session = axum_session_auth::AuthSession<User, i64, SessionSqlitePool, SqlitePool>;
pub(crate) type AuthLayer =
axum_session_auth::AuthSessionLayer<User, i64, SessionSqlitePool, SqlitePool>;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct User {
pub id: i32,
pub anonymous: bool,
pub username: String,
pub permissions: HashSet<String>,
}
#[derive(sqlx::FromRow, Clone)]
pub(crate) struct SqlPermissionTokens {
pub token: String,
}
#[async_trait]
impl Authentication<User, i64, SqlitePool> for User {
async fn load_user(userid: i64, pool: Option<&SqlitePool>) -> Result<User, anyhow::Error> {
let db = pool.unwrap();
#[derive(sqlx::FromRow, Clone)]
struct SqlUser {
id: i32,
anonymous: bool,
username: String,
}
let sqluser = sqlx::query_as::<_, SqlUser>("SELECT * FROM users WHERE id = $1")
.bind(userid)
.fetch_one(db)
.await
.unwrap();
//lets just get all the tokens the user can use, we will only use the full permissions if modifying them.
let sql_user_perms = sqlx::query_as::<_, SqlPermissionTokens>(
"SELECT token FROM user_permissions WHERE user_id = $1;",
)
.bind(userid)
.fetch_all(db)
.await
.unwrap();
Ok(User {
id: sqluser.id,
anonymous: sqluser.anonymous,
username: sqluser.username,
permissions: sql_user_perms.into_iter().map(|x| x.token).collect(),
})
}
fn is_authenticated(&self) -> bool {
!self.anonymous
}
fn is_active(&self) -> bool {
!self.anonymous
}
fn is_anonymous(&self) -> bool {
self.anonymous
}
}
#[async_trait]
impl HasPermission<SqlitePool> for User {
async fn has(&self, perm: &str, _pool: &Option<&SqlitePool>) -> bool {
self.permissions.contains(perm)
}
}
@@ -0,0 +1,158 @@
//! This example showcases how to use the `axum-session-auth` crate with Dioxus fullstack.
//! We add the `auth::Session` extractor to our server functions to get access to the current user session.
//!
//! To initialize the axum router, we use `dioxus::serve` to spawn a custom axum server that creates
//! our database, session store, and authentication layer.
//!
//! The `.serve_dioxus_application` method is used to mount our Dioxus app as a fallback service to
//! handle HTML rendering and static assets.
//!
//! We easily share the "permissions" between the server and client by using a `HashSet<String>`
//! which is serialized to/from JSON automatically by the server function system.
use std::collections::HashSet;
use dioxus::prelude::*;
#[cfg(feature = "server")]
mod auth;
fn main() {
// On the client, we simply launch the app as normal, taking over the main thread
#[cfg(not(feature = "server"))]
dioxus::launch(app);
// On the server, we can use `dioxus::serve` to create a server that serves our app.
//
// The `serve` function takes a closure that returns a `Future` which resolves to an `axum::Router`.
//
// We return a `Router` such that dioxus sets up logging, hot-reloading, devtools, and wires up the
// IP and PORT environment variables to our server.
#[cfg(feature = "server")]
dioxus::serve(|| async {
use crate::auth::*;
use axum_session::{SessionConfig, SessionLayer, SessionStore};
use axum_session_auth::AuthConfig;
use axum_session_sqlx::SessionSqlitePool;
use sqlx::{sqlite::SqlitePoolOptions, Executor};
// Create an in-memory SQLite database and set up our tables
let db = SqlitePoolOptions::new()
.max_connections(20)
.connect_with("sqlite::memory:".parse()?)
.await?;
// Create the tables (sessions, users)
db.execute(r#"CREATE TABLE IF NOT EXISTS users ( "id" INTEGER PRIMARY KEY, "anonymous" BOOLEAN NOT NULL, "username" VARCHAR(256) NOT NULL )"#,)
.await?;
db.execute(r#"CREATE TABLE IF NOT EXISTS user_permissions ( "user_id" INTEGER NOT NULL, "token" VARCHAR(256) NOT NULL)"#,)
.await?;
// Insert in some test data for two users (one anonymous, one normal)
db.execute(r#"INSERT INTO users (id, anonymous, username) SELECT 1, true, 'Guest' ON CONFLICT(id) DO UPDATE SET anonymous = EXCLUDED.anonymous, username = EXCLUDED.username"#,)
.await?;
db.execute(r#"INSERT INTO users (id, anonymous, username) SELECT 2, false, 'Test' ON CONFLICT(id) DO UPDATE SET anonymous = EXCLUDED.anonymous, username = EXCLUDED.username"#,)
.await?;
// Make sure our test user has the ability to view categories
db.execute(r#"INSERT INTO user_permissions (user_id, token) SELECT 2, 'Category::View'"#)
.await?;
// Create an axum router that dioxus will attach the app to
Ok(dioxus::server::router(app)
.layer(
AuthLayer::new(Some(db.clone()))
.with_config(AuthConfig::<i64>::default().with_anonymous_user_id(Some(1))),
)
.layer(SessionLayer::new(
SessionStore::<SessionSqlitePool>::new(
Some(db.into()),
SessionConfig::default().with_table_name("test_table"),
)
.await?,
)))
});
}
/// The UI for our app - is just a few buttons to call our server functions and display the results.
fn app() -> Element {
let mut login = use_action(login);
let mut user_name = use_action(get_user_name);
let mut permissions = use_action(get_permissions);
let mut logout = use_action(logout);
let fetch_new = move |_| async move {
user_name.call().await;
permissions.call().await;
};
rsx! {
div {
button {
onclick: move |_| async move {
login.call().await;
},
"Login Test User"
}
button {
onclick: move |_| async move {
logout.call().await;
},
"Logout"
}
button {
onclick: fetch_new,
"Fetch User Info"
}
pre { "Logged in: {login.result():?}" }
pre { "User name: {user_name.result():?}" }
pre { "Permissions: {permissions.result():?}" }
}
}
}
/// We use the `auth::Session` extractor to get access to the current user session.
/// This lets us modify the user session, log in/out, and access the current user.
#[post("/api/user/login", auth: auth::Session)]
pub async fn login() -> Result<()> {
auth.login_user(2);
Ok(())
}
/// Just like `login`, but this time we log out the user.
#[post("/api/user/logout", auth: auth::Session)]
pub async fn logout() -> Result<()> {
auth.logout_user();
Ok(())
}
/// We can access the current user via `auth.current_user`.
/// We can have both anonymous user (id 1) and a logged in user (id 2).
///
/// Logged-in users will have more permissions which we can modify.
#[post("/api/user/name", auth: auth::Session)]
pub async fn get_user_name() -> Result<String> {
Ok(auth.current_user.unwrap().username)
}
/// Get the current user's permissions, guarding the endpoint with the `Auth` validator.
/// If this returns false, we use the `or_unauthorized` extension to return a 401 error.
#[get("/api/user/permissions", auth: auth::Session)]
pub async fn get_permissions() -> Result<HashSet<String>> {
use crate::auth::User;
use axum_session_auth::{Auth, Rights};
let user = auth.current_user.unwrap();
Auth::<User, i64, sqlx::SqlitePool>::build([axum::http::Method::GET], false)
.requires(Rights::any([
Rights::permission("Category::View"),
Rights::permission("Admin::View"),
]))
.validate(&user, &axum::http::Method::GET, None)
.await
.or_unauthorized("You do not have permission to view categories")?;
Ok(user.permissions)
}
@@ -10,8 +10,6 @@ publish = false
[dependencies]
dioxus-web = { workspace = true, features = ["hydrate"], optional = true }
dioxus = { features = ["fullstack"], workspace = true }
dioxus-fullstack = { workspace = true }
dioxus-cli-config = { workspace = true, optional = true }
axum = { workspace = true, optional = true }
tokio = { workspace = true, features = ["full"], optional = true }
tower-http = { workspace = true, features = ["auth"], optional = true }
@@ -49,7 +47,6 @@ optional = true
default = []
server = [
"dioxus/server",
"dep:dioxus-cli-config",
"dep:axum",
"dep:tokio",
"dep:tower-http",
@@ -1,23 +1,15 @@
use async_trait::async_trait;
use axum::{
http::Method,
response::{IntoResponse, Response},
routing::get,
Router,
};
use axum_session::{SessionConfig, SessionLayer, SessionStore};
use axum_session_auth::*;
use axum_session_sqlx::SessionSqlitePool;
use core::pin::Pin;
use dioxus::prelude::*;
use serde::{Deserialize, Serialize};
use sqlx::sqlite::{SqliteConnectOptions, SqlitePool, SqlitePoolOptions};
use std::error::Error;
use std::future::Future;
use std::{collections::HashSet, net::SocketAddr, str::FromStr};
use sqlx::sqlite::SqlitePool;
use std::collections::HashSet;
pub(crate) type Session =
axum_session_auth::AuthSession<User, i64, SessionSqlitePool, sqlx::SqlitePool>;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
pub(crate) struct User {
pub id: i32,
pub anonymous: bool,
pub username: String,
@@ -25,25 +17,10 @@ pub struct User {
}
#[derive(sqlx::FromRow, Clone)]
pub struct SqlPermissionTokens {
pub(crate) struct SqlPermissionTokens {
pub token: String,
}
impl Default for User {
fn default() -> Self {
let mut permissions = HashSet::new();
permissions.insert("Category::View".to_owned());
Self {
id: 1,
anonymous: true,
username: "Guest".into(),
permissions,
}
}
}
#[async_trait]
impl Authentication<User, i64, SqlitePool> for User {
async fn load_user(userid: i64, pool: Option<&SqlitePool>) -> Result<User, anyhow::Error> {
@@ -76,6 +53,13 @@ impl HasPermission<SqlitePool> for User {
impl User {
pub async fn get_user(id: i64, pool: &SqlitePool) -> Option<Self> {
#[derive(sqlx::FromRow, Clone)]
struct SqlUser {
id: i32,
anonymous: bool,
username: String,
}
let sqluser = sqlx::query_as::<_, SqlUser>("SELECT * FROM users WHERE id = $1")
.bind(id)
.fetch_one(pool)
@@ -91,10 +75,15 @@ impl User {
.await
.ok()?;
Some(sqluser.into_user(Some(sql_user_perms)))
Some(User {
id: sqluser.id,
anonymous: sqluser.anonymous,
username: sqluser.username,
permissions: sql_user_perms.into_iter().map(|x| x.token).collect(),
})
}
pub async fn create_user_tables(pool: &SqlitePool) {
pub async fn create_user_tables(pool: &SqlitePool) -> Result<(), sqlx::Error> {
sqlx::query(
r#"
CREATE TABLE IF NOT EXISTS users (
@@ -105,20 +94,17 @@ impl User {
"#,
)
.execute(pool)
.await
.unwrap();
.await?;
sqlx::query(
r#"
CREATE TABLE IF NOT EXISTS user_permissions (
"user_id" INTEGER NOT NULL,
"token" VARCHAR(256) NOT NULL
)
"#,
)"#,
)
.execute(pool)
.await
.unwrap();
.await?;
sqlx::query(
r#"
@@ -130,8 +116,7 @@ impl User {
"#,
)
.execute(pool)
.await
.unwrap();
.await?;
sqlx::query(
r#"
@@ -143,8 +128,7 @@ impl User {
"#,
)
.execute(pool)
.await
.unwrap();
.await?;
sqlx::query(
r#"
@@ -153,51 +137,8 @@ impl User {
"#,
)
.execute(pool)
.await
.unwrap();
.await?;
Ok(())
}
}
#[derive(sqlx::FromRow, Clone)]
pub struct SqlUser {
pub id: i32,
pub anonymous: bool,
pub username: String,
}
impl SqlUser {
pub fn into_user(self, sql_user_perms: Option<Vec<SqlPermissionTokens>>) -> User {
User {
id: self.id,
anonymous: self.anonymous,
username: self.username,
permissions: if let Some(user_perms) = sql_user_perms {
user_perms
.into_iter()
.map(|x| x.token)
.collect::<HashSet<String>>()
} else {
HashSet::<String>::new()
},
}
}
}
pub async fn connect_to_database() -> SqlitePool {
let connect_opts = SqliteConnectOptions::from_str("sqlite::memory:").unwrap();
SqlitePoolOptions::new()
.max_connections(5)
.connect_with(connect_opts)
.await
.unwrap()
}
pub type Session =
axum_session_auth::AuthSession<crate::auth::User, i64, SessionSqlitePool, sqlx::SqlitePool>;
pub async fn get_session() -> ServerFnResult<Session> {
extract::<Session, _>()
.await
.map_err(|_| ServerFnError::new("AuthSessionLayer was not found"))
}
+129
View File
@@ -0,0 +1,129 @@
#[cfg(feature = "server")]
mod auth;
use dioxus::prelude::*;
fn main() {
#[cfg(feature = "server")]
dioxus::serve(|| async {
use crate::auth::*;
use axum_session::{SessionConfig, SessionLayer, SessionStore};
use axum_session_auth::AuthConfig;
use axum_session_sqlx::SessionSqlitePool;
use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
use std::str::FromStr;
let pool = SqlitePoolOptions::new()
.max_connections(5)
.connect_with(SqliteConnectOptions::from_str("sqlite::memory:").unwrap())
.await?;
// This Defaults as normal Cookies.
// To enable Private cookies for integrity, and authenticity please check the next Example.
let session_config = SessionConfig::default().with_table_name("test_table");
let auth_config = AuthConfig::<i64>::default().with_anonymous_user_id(Some(1));
let session_store =
SessionStore::<SessionSqlitePool>::new(Some(pool.clone().into()), session_config)
.await
.unwrap();
User::create_user_tables(&pool).await?;
// build our application with some routes
Ok(
dioxus::server::router(app)
.layer(
axum_session_auth::AuthSessionLayer::<
User,
i64,
SessionSqlitePool,
sqlx::SqlitePool,
>::new(Some(pool))
.with_config(auth_config),
)
.layer(SessionLayer::new(session_store)),
)
});
dioxus::launch(app);
}
fn app() -> Element {
let mut user_name = use_action(get_user_name);
let mut permissions = use_action(get_permissions);
let mut login = use_action(login);
rsx! {
div {
button {
onclick: move |_| {
login.call();
},
"Login Test User"
}
}
div {
button {
onclick: move |_| {
user_name.call();
},
"Get User Name"
}
"User name: {user_name.value().unwrap_or_default()}"
}
div {
button {
onclick: move |_| {
permissions.call();
},
"Get Permissions"
}
"Permissions: {permissions.value().unwrap_or_default()}"
}
}
}
#[get("/api/user/name", auth: auth::Session)]
pub async fn get_user_name() -> Result<String> {
Ok(auth
.current_user
.context("UNAUTHORIZED")?
.username
.to_string())
}
#[post("/api/user/login", auth: auth::Session)]
pub async fn login() -> Result<()> {
auth.login_user(2);
Ok(())
}
#[get("/api/user/permissions", auth: auth::Session)]
pub async fn get_permissions() -> Result<String> {
use crate::auth::User;
use axum_session_auth::{Auth, Rights};
let user = auth.current_user.as_ref().context("UNAUTHORIZED")?;
// lets check permissions only and not worry about if they are anon or not
let has_permissions =
Auth::<User, i64, sqlx::SqlitePool>::build([axum::http::Method::POST], false)
.requires(Rights::any([
Rights::permission("Category::View"),
Rights::permission("Admin::View"),
]))
.validate(user, &axum::http::Method::GET, None)
.await;
if !has_permissions {
dioxus::core::bail!(
"User {}, Does not have permissions needed to view this page please login",
user.username
);
}
Ok(format!(
"User has Permissions needed. {:?}",
user.permissions
))
}
@@ -0,0 +1,46 @@
//! This example demonstrates how to use `dioxus::serve` with a custom Axum router.
//!
//! By default, `dioxus::launch` takes over the main thread and runs the Dioxus application.
//! However, if you want to integrate Dioxus into an existing web server or use a custom router,
//! you can use `dioxus::serve` to create a server that serves your Dioxus application alongside
//! other routes.
//!
//! `dioxus::serve` sets up an async runtime, logging, hot-reloading, crash handling, and more.
//! You can then use the `.serve_dioxus_application` method on your router to serve the Dioxus app.
//!
//! `dioxus::serve` is most useful for customizing the server setup, such as adding middleware,
//! custom routes, or integrating with existing axum backend code.
//!
//! Note that `dioxus::serve` is accepts a Router from `axum`. Dioxus will use the IP and PORT
//! environment variables to determine where to bind the server. To customize the port, use environment
//! variables or a `.env` file.
//!
//! On other platforms (like desktop or mobile), you'll want to use `dioxus::launch` instead and then
//! handle async loading of data through hooks like `use_future` or `use_resource` and give the user
//! a loading state while data is being fetched.
use dioxus::prelude::*;
fn main() {
// On the client we just launch the app as normal.
#[cfg(not(feature = "server"))]
dioxus::launch(app);
// On the server, we can use `dioxus::serve` and `.serve_dioxus_application` to serve our app with routing.
// The `dioxus::server::router` function creates a new axum Router with the necessary routes to serve the Dioxus app.
#[cfg(feature = "server")]
dioxus::serve(|| async move {
use dioxus::server::axum::routing::{get, post};
Ok(dioxus::server::router(app)
.route("/submit", post(|| async { "Form submitted!" }))
.route("/about", get(|| async { "About us" }))
.route("/contact", get(|| async { "Contact us" })))
});
}
fn app() -> Element {
rsx! {
div { "Hello from Dioxus!" }
}
}
@@ -2,9 +2,10 @@
use dioxus::prelude::*;
fn main() {
// Set the url of the server where server functions are hosted.
// Make sure to set the url of the server where server functions are hosted - they aren't always at localhost
#[cfg(not(feature = "server"))]
dioxus::fullstack::set_server_url("http://127.0.0.1:8080");
dioxus::launch(app);
}
@@ -30,14 +31,13 @@ pub fn app() -> Element {
}
}
#[server]
#[post("/api/data")]
async fn post_server_data(data: String) -> ServerFnResult {
println!("Server received: {}", data);
Ok(())
}
#[server]
#[get("/api/data")]
async fn get_server_data() -> ServerFnResult<String> {
Ok("Hello from the server!".to_string())
}
@@ -0,0 +1,48 @@
//! This example showcases a fullstack variant of the "dog app" demo, but with the loader and actions
//! self-hosted instead of using the Dog API.
use dioxus::prelude::*;
fn main() {
dioxus::launch(app);
}
fn app() -> Element {
// Fetch the list of breeds from the Dog API, using the `?` syntax to suspend or throw errors
let breed_list = use_loader(list_breeds)?;
// Whenever this action is called, it will re-run the future and return the result.
let mut breed = use_action(get_random_breed_image);
rsx! {
h1 { "Doggo selector" }
div { width: "400px",
for cur_breed in breed_list.read().iter().take(20).cloned() {
button { onclick: move |_| { breed.call(cur_breed.clone()); }, "{cur_breed}" }
}
}
div {
match breed.result() {
None => rsx! { div { "Click the button to fetch a dog!" } },
Some(Err(_e)) => rsx! { div { "Failed to fetch a dog, please try again." } },
Some(Ok(res)) => rsx! { img { max_width: "500px", max_height: "500px", src: "{res}" } },
}
}
}
}
#[get("/api/breeds/list/all")]
async fn list_breeds() -> Result<Vec<String>> {
Ok(vec!["bulldog".into(), "labrador".into(), "poodle".into()])
}
#[get("/api/breed/{breed}/images/random")]
async fn get_random_breed_image(breed: String) -> Result<String> {
match breed.as_str() {
"bulldog" => Ok("https://images.dog.ceo/breeds/buhund-norwegian/hakon3.jpg".into()),
"labrador" => Ok("https://images.dog.ceo/breeds/labrador/n02099712_2501.jpg".into()),
"poodle" => Ok("https://images.dog.ceo/breeds/poodle-standard/n02113799_5973.jpg".into()),
_ => HttpError::not_found("Breed not found")?,
}
}
@@ -0,0 +1,37 @@
//! This example shows how to get access to the full axum request in a handler.
//!
//! The extra arguments in the `post` macro are passed to the handler function, but not exposed
//! to the client. This means we can still call the endpoint from the client, but have full access
//! to the request on the server.
use dioxus::prelude::*;
fn main() {
dioxus::launch(app);
}
fn app() -> Element {
let mut file_id = use_action(full_request);
rsx! {
div { "Access to full axum request" }
button { onclick: move |_| file_id.call(), "Upload file" }
}
}
/// Example of accessing the full axum request in a handler
///
/// The `request: axum_core::extract::Request` argument is placed in the handler function, but not
/// exposed to the client.
#[post("/api/full_request_access", request: axum_core::extract::Request)]
async fn full_request() -> Result<()> {
let headers = request.headers();
if headers.contains_key("x-api-key") {
println!("API key found");
} else {
println!("No API key found");
}
Ok(())
}
@@ -0,0 +1,24 @@
//! A simple example using Dioxus Fullstack to call a server action.
//!
//! the `get`, `post`, `put`, `delete`, etc macros are used to define server actions that can be
//! called from the client. The action can take arguments and return a value, and the client
//! will automatically serialize and deserialize the data.
use dioxus::prelude::*;
fn main() {
dioxus::launch(|| {
let mut message = use_action(get_message);
rsx! {
h1 { "Server says: "}
pre { "{message:?}"}
button { onclick: move |_| message.call("world".into(), 30), "Click me!" }
}
});
}
#[get("/api/{name}/?age")]
async fn get_message(name: String, age: i32) -> Result<String> {
Ok(format!("Hello {}, you are {} years old!", name, age))
}
+173
View File
@@ -0,0 +1,173 @@
//! An example of handling errors from server functions.
//!
//! This example showcases a few important error handling patterns when using Dioxus Fullstack.
//!
//! Run with:
//!
//! ```sh
//! dx serve --web
//! ```
//!
//! What this example shows:
//! - You can return `anyhow::Result<T>` (i.e. `Result<T>` without an `E`) for
//! untyped errors with rich context (converted to HTTP 500 responses by default).
//! - You can return `Result<T, E>` where `E` is one of:
//! - `HttpError` (convenience for returning HTTP status codes)
//! - `StatusCode` (return raw status codes)
//! - a custom error type that implements `From<ServerFnError>` or
//! is `Serialize`/`Deserialize` so it can be sent to the client.
//! - This file demonstrates external API errors, custom typed errors, explicit
//! HTTP errors, and basic success cases. The UI uses `use_action` to call
//! server functions and shows loading/result states simply.
//!
//! Try running requests against the endpoints directly with `curl` or `postman` to see the actual HTTP responses!
use dioxus::fullstack::{Json, StatusCode};
use dioxus::prelude::*;
use serde::{Deserialize, Serialize};
fn main() {
dioxus::launch(|| {
let mut dog_data = use_action(get_dog_data);
let mut dog_data_err = use_action(get_dog_data_err);
let mut ip_data = use_action(get_ip_data);
let mut custom_data = use_action(move || {
get_custom_encoding(Json(serde_json::json!({
"example": "data",
"number": 123,
"array": [1, 2, 3],
})))
});
let mut error_data = use_action(get_throws_error);
let mut typed_error_data = use_action(get_throws_typed_error);
let mut throws_ok_data = use_action(get_throws_ok);
let mut http_error_data = use_action(throws_http_error);
let mut http_error_context_data = use_action(throws_http_error_context);
rsx! {
button { onclick: move |_| { dog_data.call(); }, "Fetch dog data" }
button { onclick: move |_| { ip_data.call(); }, "Fetch IP data" }
button { onclick: move |_| { custom_data.call(); }, "Fetch custom encoded data" }
button { onclick: move |_| { error_data.call(); }, "Fetch error data" }
button { onclick: move |_| { typed_error_data.call(); }, "Fetch typed error data" }
button { onclick: move |_| { dog_data_err.call(); }, "Fetch dog error data" }
button { onclick: move |_| { throws_ok_data.call(); }, "Fetch throws ok data" }
button { onclick: move |_| { http_error_data.call(); }, "Fetch HTTP 400" }
button { onclick: move |_| { http_error_context_data.call(); }, "Fetch HTTP 400 (context)" }
button {
onclick: move |_| {
ip_data.reset();
dog_data.reset();
custom_data.reset();
error_data.reset();
typed_error_data.reset();
dog_data_err.reset();
throws_ok_data.reset();
http_error_data.reset();
http_error_context_data.reset();
},
"Clear data"
}
div { display: "flex", flex_direction: "column", gap: "8px",
pre { "Dog data: {dog_data.value():#?}" }
pre { "IP data: {ip_data.value():#?}" }
pre { "Custom encoded data: {custom_data.value():#?}" }
pre { "Error data: {error_data.result():#?}" }
pre { "Typed error data: {typed_error_data.result():#?}" }
pre { "HTTP 400 data: {http_error_data.result():#?}" }
pre { "HTTP 400 (context) data: {http_error_context_data.result():#?}" }
pre { "Dog error data: {dog_data_err.result():#?}" }
pre { "Throws ok data: {throws_ok_data.result():#?}" }
}
}
});
}
/// Simple POST endpoint used to show a successful server function that returns `StatusCode`.
#[post("/api/data")]
async fn post_server_data(data: String) -> Result<(), StatusCode> {
println!("Server received: {}", data);
Ok(())
}
/// Fetches IP info from an external service. Demonstrates propagation of external errors.
#[get("/api/ip-data")]
async fn get_ip_data() -> Result<serde_json::Value> {
Ok(reqwest::get("https://httpbin.org/ip").await?.json().await?)
}
/// Fetches a random dog image (successful external API example).
#[get("/api/dog-data")]
async fn get_dog_data() -> Result<serde_json::Value> {
Ok(reqwest::get("https://dog.ceo/api/breeds/image/random")
.await?
.json()
.await?)
}
/// Calls the Dog API with an invalid breed to trigger an external API error (e.g. 404).
#[get("/api/dog-data-err")]
async fn get_dog_data_err() -> Result<serde_json::Value> {
Ok(
reqwest::get("https://dog.ceo/api/breed/NOT_A_REAL_DOG/images")
.await?
.json()
.await?,
)
}
/// Accepts JSON and returns a custom-encoded JSON response.
#[post("/api/custom-encoding")]
async fn get_custom_encoding(takes: Json<serde_json::Value>) -> Result<serde_json::Value> {
Ok(serde_json::json!({
"message": "This response was encoded with a custom encoder!",
"success": true,
"you sent": takes.0,
}))
}
/// Returns an untyped `anyhow` error with context (results in HTTP 500).
#[get("/api/untyped-error")]
async fn get_throws_error() -> Result<()> {
Err(None.context("This is an example error using anyhow::Error")?)
}
/// Demonstrates returning an explicit HTTP error (400 Bad Request) using `HttpError`.
#[get("/api/throws-http-error")]
async fn throws_http_error() -> Result<()> {
HttpError::bad_request("Bad request example")?;
Ok(())
}
/// Convenience example: handles an Option and returns HTTP 400 with a message if None.
#[get("/api/throws-http-error-context")]
async fn throws_http_error_context() -> Result<String> {
let res = None.or_bad_request("Value was None")?;
Ok(res)
}
/// A simple server function that always succeeds.
#[get("/api/throws-ok")]
async fn get_throws_ok() -> Result<()> {
Ok(())
}
#[derive(thiserror::Error, Debug, Serialize, Deserialize)]
enum MyCustomError {
#[error("bad request")]
BadRequest { custom_name: String },
#[error("not found")]
NotFound,
#[error("internal server error: {0}")]
ServerFnError(#[from] ServerFnError),
}
/// Returns a custom typed error (serializable) so clients can handle specific cases.
#[get("/api/typed-error")]
async fn get_throws_typed_error() -> Result<(), MyCustomError> {
Err(MyCustomError::BadRequest {
custom_name: "Invalid input".into(),
})
}
+36
View File
@@ -0,0 +1,36 @@
//! This example shows how you can extract a HeaderMap from requests to read custom headers.
//!
//! The extra arguments in the `#[get(...)]` macro are passed to the underlying axum handler,
//! and only visible on the server. This lets you run normal axum extractors like `HeaderMap`,
//! `TypedHeader`, `Query`, etc.
//!
//! Note that headers returned by the server are not always visible to the client due to CORS.
//! Headers like `Set-Cookie` are hidden by default, and need to be explicitly allowed
//! by the server using the `Access-Control-Expose-Headers` header (which dioxus-fullstack does not
//! currently expose directly).
use dioxus::prelude::*;
fn main() {
dioxus::launch(app);
}
fn app() -> Element {
let mut headers = use_action(get_headers);
rsx! {
h1 { "Header Map Example" }
button { onclick: move |_| headers.call(), "Get Headers" }
if let Some(Ok(headers)) = headers.result() {
p { "Response from server:" }
pre { "{headers}" }
} else {
p { "No headers yet" }
}
}
}
#[get("/api/example", headers: dioxus::fullstack::HeaderMap)]
async fn get_headers() -> Result<String> {
Ok(format!("{:#?}", headers))
}
@@ -9,7 +9,10 @@ publish = false
[dependencies]
dioxus = { workspace = true, features = ["fullstack"]}
serde = { workspace = true }
reqwest = { workspace = true }
reqwest = { workspace = true, features = ["json"] }
serde_json = { workspace = true }
anyhow = { workspace = true }
thiserror = { workspace = true }
[features]
default = []
@@ -0,0 +1,3 @@
h1 {
font-family: monospace;
}
@@ -0,0 +1,59 @@
//! A simple hello world example for Dioxus fullstack
//!
//! Run with:
//!
//! ```sh
//! dx serve --web
//! ```
//!
//! This example demonstrates a simple Dioxus fullstack application with a client-side counter
//! and a server function that returns a greeting message.
//!
//! The `use_action` hook makes it easy to call async work (like server functions) from the client side
//! and handle loading and error states.
use dioxus::prelude::*;
use dioxus_fullstack::get;
fn main() {
dioxus::launch(app);
}
fn app() -> Element {
let mut count = use_signal(|| 0);
let mut message = use_action(get_greeting);
rsx! {
div { style: "padding: 2rem; font-family: Arial, sans-serif;",
h1 { "Hello, Dioxus Fullstack!" }
// Client-side counter - you can use any client functionality in your app!
div { style: "margin: 1rem 0;",
h2 { "Client Counter: {count}" }
button { onclick: move |_| count += 1, "Increment" }
button { onclick: move |_| count -= 1, "Decrement" }
}
// We can handle the action result and display loading state
div { style: "margin: 1rem 0;",
h2 { "Server Greeting" }
button { onclick: move |_| message.call("World".to_string(), 30), "Get Server Greeting" }
if message.is_pending() {
p { "Loading..." }
}
p { "{message:#?}" }
}
}
}
}
/// A simple server function that returns a greeting
///
/// Our server function takes a name as a path and query parameters as inputs and returns a greeting message.
#[get("/api/greeting/{name}/{age}")]
async fn get_greeting(name: String, age: i32) -> Result<String> {
Ok(format!(
"Hello from the server, {}! You are {} years old. 🚀",
name, age
))
}
Loaded 100 of 424 files, more files were not shown because too many files have changed in this diff. Show more