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:
1 parent
8421c3aef0
commit
f2b1c6d97f
424 files changed
+17284
-6624
No files matched your search
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
[package]
|
||||
name = "fullstack-auth-example"
|
||||
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 }
|
||||
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 = []
|
||||
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,144 @@
|
||||
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, sqlx::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 pool = pool.unwrap();
|
||||
|
||||
User::get_user(userid, pool)
|
||||
.await
|
||||
.ok_or_else(|| anyhow::anyhow!("Could not load user"))
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
//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(id)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
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) -> Result<(), sqlx::Error> {
|
||||
sqlx::query(
|
||||
r#"
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
"id" INTEGER PRIMARY KEY,
|
||||
"anonymous" BOOLEAN NOT NULL,
|
||||
"username" VARCHAR(256) NOT NULL
|
||||
)
|
||||
"#,
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
CREATE TABLE IF NOT EXISTS user_permissions (
|
||||
"user_id" INTEGER NOT NULL,
|
||||
"token" VARCHAR(256) NOT NULL
|
||||
)"#,
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO users
|
||||
(id, anonymous, username) SELECT 1, true, 'Guest'
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
anonymous = EXCLUDED.anonymous,
|
||||
username = EXCLUDED.username
|
||||
"#,
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO users
|
||||
(id, anonymous, username) SELECT 2, false, 'Test'
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
anonymous = EXCLUDED.anonymous,
|
||||
username = EXCLUDED.username
|
||||
"#,
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO user_permissions
|
||||
(user_id, token) SELECT 2, 'Category::View'
|
||||
"#,
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -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!" }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "fullstack-desktop-example"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
dioxus = { workspace = true, features = ["launch", "fullstack"] }
|
||||
serde = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
server = ["dioxus/server"]
|
||||
desktop = ["dioxus/desktop"]
|
||||
@@ -0,0 +1,43 @@
|
||||
#![allow(non_snake_case)]
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn main() {
|
||||
// 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);
|
||||
}
|
||||
|
||||
pub fn app() -> Element {
|
||||
let mut count = use_signal(|| 0);
|
||||
let mut text = use_signal(|| "...".to_string());
|
||||
|
||||
rsx! {
|
||||
h1 { "High-Five counter: {count}" }
|
||||
button { onclick: move |_| count += 1, "Up high!" }
|
||||
button { onclick: move |_| count -= 1, "Down low!" }
|
||||
button {
|
||||
onclick: move |_| async move {
|
||||
let data = get_server_data().await?;
|
||||
println!("Client received: {}", data);
|
||||
text.set(data.clone());
|
||||
post_server_data(data).await?;
|
||||
Ok(())
|
||||
},
|
||||
"Run a server function"
|
||||
}
|
||||
"Server said: {text}"
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/api/data")]
|
||||
async fn post_server_data(data: String) -> ServerFnResult {
|
||||
println!("Server received: {}", data);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[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))
|
||||
}
|
||||
@@ -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(),
|
||||
})
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "fullstack-hello-world-example"
|
||||
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 = { workspace = true, features = ["fullstack"]}
|
||||
serde = { workspace = true }
|
||||
reqwest = { workspace = true, features = ["json"] }
|
||||
serde_json = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
server = ["dioxus/server"]
|
||||
web = ["dioxus/web"]
|
||||
@@ -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
|
||||
))
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
//! This example demonstrates how to use types like `Form`, `SetHeader`, and `TypedHeader`
|
||||
//! to create a simple login form that sets a cookie in the browser and uses it for authentication
|
||||
//! on a protected endpoint.
|
||||
//!
|
||||
//! For more information on handling forms in general, see the multipart_form example.
|
||||
//!
|
||||
//! The intent with this example is to show how to use the building blocks like `Form` and `SetHeader`
|
||||
//! to roll a simple authentication system.
|
||||
|
||||
use dioxus::fullstack::{Form, SetCookie, SetHeader};
|
||||
use dioxus::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
use {
|
||||
dioxus::fullstack::{Cookie, TypedHeader},
|
||||
std::sync::LazyLock,
|
||||
uuid::Uuid,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
dioxus::launch(app);
|
||||
}
|
||||
|
||||
fn app() -> Element {
|
||||
let mut fetch_login = use_action(login);
|
||||
let mut fetch_sensitive = use_action(sensitive);
|
||||
|
||||
rsx! {
|
||||
h1 { "Login Form Demo" }
|
||||
button {
|
||||
onclick: move |_| async move {
|
||||
fetch_sensitive.call();
|
||||
},
|
||||
"Get Sensitive Data",
|
||||
}
|
||||
pre { "Response from locked API: {fetch_sensitive.result():?}"}
|
||||
form {
|
||||
onsubmit: move |evt: FormEvent| async move {
|
||||
// Prevent the browser from navigating away.
|
||||
evt.prevent_default();
|
||||
|
||||
// Extract the form values into our `LoginForm` struct. The `.parsed_values` method
|
||||
// is provided by Dioxus and works with any form element that has `name` attributes.
|
||||
let values: LoginForm = evt.parsed_values().unwrap();
|
||||
|
||||
// Call our server function with the form values wrapped in `Form`. The `SetHeader`
|
||||
// response will set a cookie in the browser if the login is successful.
|
||||
fetch_login.call(Form(values)).await;
|
||||
|
||||
// Now that we're logged in, we can call our sensitive endpoint.
|
||||
fetch_sensitive.call().await;
|
||||
},
|
||||
input { r#type: "text", id: "username", name: "username" }
|
||||
label { "Username" }
|
||||
input { r#type: "password", id: "password", name: "password" }
|
||||
label { "Password" }
|
||||
button { "Login" }
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct LoginForm {
|
||||
username: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
/// A static session ID for demonstration purposes. This forces all previous logins to be invalidated
|
||||
/// when the server restarts.
|
||||
#[cfg(feature = "server")]
|
||||
static THIS_SESSION_ID: LazyLock<Uuid> = LazyLock::new(Uuid::new_v4);
|
||||
|
||||
/// In our `login` form, we'll return a `SetCookie` header if the login is successful.
|
||||
///
|
||||
/// This will set a cookie in the user's browser that can be used for subsequent authenticated requests.
|
||||
/// The `SetHeader::new()` method takes anything that can be converted into a `HeaderValue`.
|
||||
///
|
||||
/// We can set multiple headers by returning a tuple of `SetHeader` types, or passing in a tuple
|
||||
/// of headers to `SetHeader::new()`.
|
||||
#[post("/api/login")]
|
||||
async fn login(form: Form<LoginForm>) -> Result<SetHeader<SetCookie>> {
|
||||
// Verify the username and password. In a real application, you'd check these against a database.
|
||||
if form.0.username == "admin" && form.0.password == "password" {
|
||||
return Ok(SetHeader::new(format!("auth-demo={};", &*THIS_SESSION_ID))?);
|
||||
}
|
||||
|
||||
HttpError::unauthorized("Invalid username or password")?
|
||||
}
|
||||
|
||||
/// We'll use the `TypedHeader` extractor on the server to get the cookie from the request.
|
||||
#[get("/api/sensitive", header: TypedHeader<Cookie>)]
|
||||
async fn sensitive() -> Result<String> {
|
||||
// Extract the cookie from the request headers and use `.eq` to verify its value.
|
||||
// The `or_unauthorized` works on boolean values, returning a 401 if the condition is false.
|
||||
header
|
||||
.get("auth-demo")
|
||||
.or_unauthorized("Missing auth-demo cookie")?
|
||||
.eq(THIS_SESSION_ID.to_string().as_str())
|
||||
.or_unauthorized("Invalid auth-demo cookie")?;
|
||||
|
||||
Ok("Sensitive data".to_string())
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
//! This example showcases how to handle multipart form data uploads in Dioxus.
|
||||
//!
|
||||
//! Dioxus provides the `MultipartFormData` type to allow converting from the websys `FormData`
|
||||
//! type directly into a streaming multipart form data handler.
|
||||
|
||||
use dioxus::{fullstack::MultipartFormData, prelude::*};
|
||||
|
||||
fn main() {
|
||||
dioxus::launch(app);
|
||||
}
|
||||
|
||||
fn app() -> Element {
|
||||
// The `MultipartFormData` type can be used to handle multipart form data uploads.
|
||||
// We can convert into it by using `.into()` on the `FormEvent`'s data, or by crafting
|
||||
// a `MultipartFormData` instance manually.
|
||||
let mut upload_as_multipart = use_action(move |event: FormEvent| upload(event.into()));
|
||||
|
||||
rsx! {
|
||||
Stylesheet { href: asset!("/examples/assets/file_upload.css") }
|
||||
img { src: asset!("/examples/assets/logo.png"), width: "200px" }
|
||||
div {
|
||||
h3 { "Upload as Multipart" }
|
||||
p { "Use the built-in multipart form handling" }
|
||||
form {
|
||||
display: "flex",
|
||||
flex_direction: "column",
|
||||
gap: "8px",
|
||||
onsubmit: move |evt| async move {
|
||||
evt.prevent_default();
|
||||
upload_as_multipart.call(evt).await;
|
||||
},
|
||||
label { r#for: "headshot", "Photos" }
|
||||
input { r#type: "file", name: "headshot", multiple: true, accept: ".png,.jpg,.jpeg" }
|
||||
label { r#for: "resume", "Resume" }
|
||||
input { r#type: "file", name: "resume", multiple: false, accept: ".pdf" }
|
||||
label { r#for: "name", "Name" }
|
||||
input { r#type: "text", name: "name", placeholder: "Name" }
|
||||
label { r#for: "age", "Age" }
|
||||
input { r#type: "number", name: "age", placeholder: "Age" }
|
||||
input { r#type: "submit", name: "submit", value: "Submit your resume" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Upload a form as multipart form data.
|
||||
///
|
||||
/// MultipartFormData is typed over the form data structure, allowing us to extract
|
||||
/// both files and other form fields in a type-safe manner.
|
||||
///
|
||||
/// On the server, we have access to axum's `Multipart` extractor
|
||||
#[post("/api/upload-multipart")]
|
||||
async fn upload(mut form: MultipartFormData) -> Result<()> {
|
||||
while let Ok(Some(field)) = form.next_field().await {
|
||||
info!(
|
||||
"Field name: {:?}, filename: {:?}, content_type: {:?}",
|
||||
field.name().to_owned(),
|
||||
field.file_name(),
|
||||
field.content_type(),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "fullstack-router-example"
|
||||
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 = { workspace = true, features = ["fullstack", "router"] }
|
||||
axum = { workspace = true, optional = true }
|
||||
tokio = { workspace = true, features = ["full"], optional = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
server = ["axum", "dioxus/server"]
|
||||
web = ["dioxus/web"]
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
//! Run with:
|
||||
//!
|
||||
//! ```sh
|
||||
//! dx serve --platform web
|
||||
//! ```
|
||||
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn main() {
|
||||
dioxus::LaunchBuilder::new()
|
||||
.with_cfg(server_only!(ServeConfig::builder().incremental(
|
||||
dioxus::server::IncrementalRendererConfig::default()
|
||||
.invalidate_after(std::time::Duration::from_secs(120)),
|
||||
)))
|
||||
.launch(app);
|
||||
}
|
||||
|
||||
fn app() -> Element {
|
||||
rsx! { Router::<Route> {} }
|
||||
}
|
||||
|
||||
#[derive(Clone, Routable, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
enum Route {
|
||||
#[route("/")]
|
||||
Home {},
|
||||
|
||||
#[route("/blog/:id/")]
|
||||
Blog { id: i32 },
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Blog(id: i32) -> Element {
|
||||
rsx! {
|
||||
Link { to: Route::Home {}, "Go to counter" }
|
||||
table {
|
||||
tbody {
|
||||
for _ in 0..id {
|
||||
tr {
|
||||
for _ in 0..id {
|
||||
td { "hello world!" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Home() -> Element {
|
||||
let mut count = use_signal(|| 0);
|
||||
let mut text = use_signal(|| "...".to_string());
|
||||
|
||||
rsx! {
|
||||
Link { to: Route::Blog { id: count() }, "Go to blog" }
|
||||
div {
|
||||
h1 { "High-Five counter: {count}" }
|
||||
button { onclick: move |_| count += 1, "Up high!" }
|
||||
button { onclick: move |_| count -= 1, "Down low!" }
|
||||
button {
|
||||
onclick: move |_| async move {
|
||||
let data = get_server_data().await?;
|
||||
println!("Client received: {}", data);
|
||||
text.set(data.clone());
|
||||
post_server_data(data).await?;
|
||||
Ok(())
|
||||
},
|
||||
"Run server function!"
|
||||
}
|
||||
"Server said: {text}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/api/data")]
|
||||
async fn post_server_data(data: String) -> ServerFnResult {
|
||||
println!("Server received: {}", data);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[get("/api/data")]
|
||||
async fn get_server_data() -> ServerFnResult<String> {
|
||||
Ok("Hello from the server!".to_string())
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
//! This example demonstrates server-sent events (SSE) using Dioxus Fullstack.
|
||||
//!
|
||||
//! Server-sent events allow the server to push updates to the client over a single HTTP connection.
|
||||
//! This is useful for real-time updates, notifications, or any scenario where the server needs to
|
||||
//! send data to the client without the client explicitly requesting it.
|
||||
//!
|
||||
//! SSE is a simpler alternative to WebSockets, not requiring a full-duplex, stateful connection with
|
||||
//! the server. Instead, it uses a single long-lived HTTP connection to stream events from the server to the client.
|
||||
//!
|
||||
//! This means that SSE messages are stringly encoded, and thus binary data must be base64 encoded.
|
||||
//! If you need to send binary data, consider using the `Streaming<T>` type instead, which lets
|
||||
//! you send raw bytes over a streaming HTTP response with a custom encoding. You'd reach for SSE
|
||||
//! when dealing with clients that might not support custom streaming protocols.
|
||||
//!
|
||||
//! Calling an SSE endpoint is as simple as calling any other server function. The return type of an
|
||||
//! SSE endpoint is a `ServerEvents<T>` where `T` is the type of event you want to send to the client.
|
||||
//!
|
||||
//! On the client, the `ServerEvents<T>` type implements `Stream<Item = Result<T, ServerFnError>>`
|
||||
//! so you can use it with async streams to get new events as they arrive.
|
||||
//!
|
||||
//! `T` must be serializable and deserializable, so anything that implements `Serialize` and `Deserialize`
|
||||
//! can be used as an event type. Calls to `.recv()` will wait for the next event to arrive and
|
||||
//! deserialize it into the correct type.
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_fullstack::ServerEvents;
|
||||
|
||||
fn main() {
|
||||
dioxus::launch(app);
|
||||
}
|
||||
|
||||
fn app() -> Element {
|
||||
let mut events = use_signal(Vec::new);
|
||||
|
||||
use_future(move || async move {
|
||||
// Call the SSE endpoint to get a stream of events
|
||||
let mut stream = listen_for_changes().await?;
|
||||
|
||||
// And then poll it for new events, adding them to our signal
|
||||
while let Some(Ok(event)) = stream.recv().await {
|
||||
events.write().push(event);
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
});
|
||||
|
||||
rsx! {
|
||||
h1 { "Events from server: " }
|
||||
for msg in events.read().iter().rev() {
|
||||
pre { "{msg:?}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// We can send anything that's serializable as a server event - strings, numbers, structs, enums, etc.
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
|
||||
enum MyServerEvent {
|
||||
Yay { message: String },
|
||||
Nay { error: String },
|
||||
}
|
||||
|
||||
/// Our SSE endpoint, when called, will return the ServerEvents handle which streams events to the client.
|
||||
/// On the client, we can interact with this stream object to get new events as they arrive.
|
||||
#[get("/api/sse")]
|
||||
async fn listen_for_changes() -> Result<ServerEvents<MyServerEvent>> {
|
||||
use std::time::Duration;
|
||||
|
||||
Ok(ServerEvents::new(|mut tx| async move {
|
||||
let mut count = 1;
|
||||
|
||||
loop {
|
||||
// Create our serializable message
|
||||
let msg = if count % 5 == 0 {
|
||||
MyServerEvent::Nay {
|
||||
error: "An error occurred".into(),
|
||||
}
|
||||
} else {
|
||||
MyServerEvent::Yay {
|
||||
message: format!("Hello number {count}"),
|
||||
}
|
||||
};
|
||||
|
||||
// Send the message to the client. If it errors, the client has disconnected
|
||||
if tx.send(msg).await.is_err() {
|
||||
// client disconnected, do some cleanup
|
||||
break;
|
||||
}
|
||||
|
||||
count += 1;
|
||||
|
||||
// Poll some data source here, subscribe to changes, maybe call an LLM?
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
}
|
||||
}))
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
//! This example shows how to use global state to maintain state between server functions.
|
||||
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
use {
|
||||
dioxus::fullstack::Lazy,
|
||||
dioxus::fullstack::axum,
|
||||
futures::lock::Mutex,
|
||||
sqlx::{Executor, Row},
|
||||
std::sync::LazyLock,
|
||||
};
|
||||
|
||||
/*
|
||||
Option 1:
|
||||
|
||||
For simple, synchronous, thread-safe data, we can use statics with atomic types or mutexes.
|
||||
The `LazyLock` type from the standard library is a great choice for simple, synchronous data
|
||||
*/
|
||||
static MESSAGES: LazyLock<Mutex<Vec<String>>> = LazyLock::new(|| Mutex::new(Vec::new()));
|
||||
|
||||
#[post("/api/messages")]
|
||||
async fn add_message() -> Result<()> {
|
||||
MESSAGES.lock().await.push("New message".to_string());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[get("/api/messages")]
|
||||
async fn read_messages() -> Result<Vec<String>> {
|
||||
Ok(MESSAGES.lock().await.clone())
|
||||
}
|
||||
|
||||
/*
|
||||
Option 2:
|
||||
|
||||
For complex async data, we can use the `Lazy` type from Dioxus Fullstack. The `Lazy` type provides
|
||||
an interface like `once_cell::Lazy` but supports async initialization. When reading the value from
|
||||
a `Lazy<T>`, the value will be initialized synchronously, blocking the current task until the value is ready.
|
||||
|
||||
Alternatively, you can create a `Lazy<T>` with `Lazy::lazy` and then initialize it later with
|
||||
`Lazy::initialize`.
|
||||
*/
|
||||
#[cfg(feature = "server")]
|
||||
static DATABASE: Lazy<sqlx::SqlitePool> = Lazy::new(|| async move {
|
||||
use sqlx::sqlite::SqlitePoolOptions;
|
||||
dioxus::Ok(
|
||||
SqlitePoolOptions::new()
|
||||
.max_connections(5)
|
||||
.connect_with("sqlite::memory:".parse().unwrap())
|
||||
.await?,
|
||||
)
|
||||
});
|
||||
|
||||
/// When using the `Lazy<T>` type, it implements `Deref<Target = T>`, so you can use it like a normal reference.
|
||||
#[get("/api/users")]
|
||||
async fn get_users() -> Result<Vec<String>> {
|
||||
let users = DATABASE
|
||||
.fetch_all(sqlx::query("SELECT name FROM users"))
|
||||
.await?
|
||||
.iter()
|
||||
.map(|row| row.get::<String, _>("name"))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(users)
|
||||
}
|
||||
|
||||
/*
|
||||
Option 3:
|
||||
|
||||
For data that needs to be provided per-request, we can use axum's `Extension` type to provide
|
||||
data to our app. This is useful for things like request-scoped data or data that needs to be
|
||||
initialized per-requestz
|
||||
*/
|
||||
#[cfg(feature = "server")]
|
||||
type BroadcastExtension = axum::Extension<tokio::sync::broadcast::Sender<String>>;
|
||||
|
||||
#[post("/api/broadcast", ext: BroadcastExtension)]
|
||||
async fn broadcast_message() -> Result<()> {
|
||||
ext.send("New broadcast message".to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
#[cfg(not(feature = "server"))]
|
||||
dioxus::launch(app);
|
||||
|
||||
// When using `Lazy` items, or axum `Extension`s, we need to initialize them in `dioxus::serve`
|
||||
// before launching our app.
|
||||
#[cfg(feature = "server")]
|
||||
dioxus::serve(|| async move {
|
||||
use dioxus::server::axum::Extension;
|
||||
|
||||
// For axum `Extension`s, we can use the `layer` method to add them to our router.
|
||||
let router = dioxus::server::router(app)
|
||||
.layer(Extension(tokio::sync::broadcast::channel::<String>(16).0));
|
||||
|
||||
Ok(router)
|
||||
});
|
||||
}
|
||||
|
||||
fn app() -> Element {
|
||||
let mut users = use_action(get_users);
|
||||
let mut messages = use_action(read_messages);
|
||||
let mut broadcast = use_action(broadcast_message);
|
||||
let mut add = use_action(add_message);
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
button { onclick: move |_| users.call(), "Get Users" }
|
||||
pre { "{users.result():?}" }
|
||||
button { onclick: move |_| messages.call(), "Get Messages" }
|
||||
pre { "{messages.result():?}" }
|
||||
button { onclick: move |_| broadcast.call(), "Broadcast Message" }
|
||||
pre { "{broadcast.result():?}" }
|
||||
button { onclick: move |_| add.call(), "Add Message" }
|
||||
pre { "{add.result():?}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "ssr-only"
|
||||
edition = "2024"
|
||||
version.workspace = true
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
dioxus = { workspace = true, features = ["fullstack", "router"] }
|
||||
|
||||
[features]
|
||||
default = ["server"]
|
||||
server = ["dioxus/server"]
|
||||
@@ -0,0 +1,59 @@
|
||||
//! This example showcases how to use Fullstack in a server-side rendering only context.
|
||||
//!
|
||||
//! This means we have no client-side bundle at all, and *everything* is rendered on the server.
|
||||
//! You can still use signals, resources, etc, but they won't be reactive on the client.
|
||||
//!
|
||||
//! This is useful for static site generation, or if you want to use Dioxus Fullstack as a server-side
|
||||
//! framework without the `rsx! {}` markup.
|
||||
//!
|
||||
//! To run this example, simply run `cargo run --package ssr-only` and navigate to `http://localhost:8080`.
|
||||
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn main() {
|
||||
dioxus::launch(|| rsx! { Router::<Route> { } });
|
||||
}
|
||||
|
||||
#[derive(Routable, Clone, Debug, PartialEq)]
|
||||
enum Route {
|
||||
#[route("/")]
|
||||
Home,
|
||||
|
||||
#[route("/post/:id")]
|
||||
Post { id: u32 },
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Home() -> Element {
|
||||
rsx! {
|
||||
h1 { "home" }
|
||||
ul {
|
||||
li { a { href: "/post/1", "Post 1" } }
|
||||
li { a { href: "/post/2", "Post 2" } }
|
||||
li { a { href: "/post/3", "Post 3 (404)" } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Post(id: u32) -> Element {
|
||||
// You can return `HttpError` to return a specific HTTP status code and message.
|
||||
// `404 Not Found` will cause the server to return a 404 status code.
|
||||
//
|
||||
// `use_loader` will suspend the server-side rendering until the future resolves.
|
||||
let post_data = use_loader(move || get_post(id))?;
|
||||
|
||||
rsx! {
|
||||
h1 { "Post {id}" }
|
||||
p { "{post_data}" }
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/api/post/{id}")]
|
||||
async fn get_post(id: u32) -> Result<String, HttpError> {
|
||||
match id {
|
||||
1 => Ok("first post".to_string()),
|
||||
2 => Ok("second post".to_string()),
|
||||
_ => HttpError::not_found("Post not found")?,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
//! This example shows how to use the `Streaming<T, E>` type to send streaming responses from the
|
||||
//! server to the client (and the client to the server!).
|
||||
//!
|
||||
//! The `Streaming<T, E>` type automatically coordinates sending and receiving streaming data over HTTP.
|
||||
//! The `T` type parameter is the type of data being sent, and the `E` type parameter is the encoding
|
||||
//! used to serialize and deserialize the data.
|
||||
//!
|
||||
//! Dioxus Fullstack provides several built-in encodings:
|
||||
//! - JsonEncoding: the default, uses JSON for serialization
|
||||
//! - CborEncoding: uses CBOR for binary serialization
|
||||
//! - PostcardEncoding: uses Postcard for binary serialization
|
||||
//! - MsgPackEncoding: uses MessagePack for binary serialization
|
||||
//! - RkyvEncoding: uses Rkyv for zero-copy binary serialization
|
||||
//!
|
||||
//! The default encoding is `JsonEncoding`, which works well for most use cases and can be used by
|
||||
//! most clients. If you need a more efficient binary encoding, consider using one of the
|
||||
//! binary encodings.
|
||||
|
||||
use bytes::Bytes;
|
||||
use dioxus::{
|
||||
fullstack::{JsonEncoding, Streaming, TextStream},
|
||||
prelude::*,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
dioxus::launch(app)
|
||||
}
|
||||
|
||||
fn app() -> Element {
|
||||
let mut text_responses = use_signal(String::new);
|
||||
let mut json_responses = use_signal(Vec::new);
|
||||
|
||||
let mut start_text_stream = use_action(move || async move {
|
||||
text_responses.write().clear();
|
||||
let mut stream = text_stream(Some(100)).await?;
|
||||
|
||||
while let Some(Ok(text)) = stream.next().await {
|
||||
text_responses.write().push_str(&text);
|
||||
text_responses.write().push('\n');
|
||||
}
|
||||
|
||||
dioxus::Ok(())
|
||||
});
|
||||
|
||||
let mut start_json_stream = use_action(move || async move {
|
||||
json_responses.write().clear();
|
||||
let mut stream = json_stream().await?;
|
||||
|
||||
while let Some(Ok(dog)) = stream.next().await {
|
||||
json_responses.write().push(dog);
|
||||
}
|
||||
|
||||
dioxus::Ok(())
|
||||
});
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
button { onclick: move |_| start_text_stream.call(), "Start text stream" }
|
||||
button { onclick: move |_| start_text_stream.cancel(), "Stop text stream" }
|
||||
pre { "{text_responses}" }
|
||||
}
|
||||
div {
|
||||
button { onclick: move |_| start_json_stream.call(), "Start JSON stream" }
|
||||
button { onclick: move |_| start_json_stream.cancel(), "Stop JSON stream" }
|
||||
for dog in json_responses.read().iter() {
|
||||
pre { "{dog:?}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The `TextStream` type is an alias for `Streaming<String>` with a text/plain encoding.
|
||||
///
|
||||
/// The `TextStream::new()` method takes anything that implements `Stream<Item = String>`, so
|
||||
/// we can use a channel to send strings from a background task.
|
||||
#[get("/api/test_stream?start")]
|
||||
async fn text_stream(start: Option<i32>) -> Result<TextStream> {
|
||||
let (tx, rx) = futures::channel::mpsc::unbounded();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let mut count = start.unwrap_or(0);
|
||||
loop {
|
||||
let message = format!("Hello, world! {}", count);
|
||||
if tx.unbounded_send(message).is_err() {
|
||||
break;
|
||||
}
|
||||
|
||||
count += 1;
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
||||
}
|
||||
});
|
||||
|
||||
Ok(Streaming::new(rx))
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug)]
|
||||
struct Dog {
|
||||
name: String,
|
||||
age: u8,
|
||||
}
|
||||
|
||||
/// A custom `Streaming<T, E>` endpoint that streams JSON-encoded `Dog` structs to the client.
|
||||
///
|
||||
/// Dioxus provides the `JsonEncoding` type which can be used to encode and decode JSON data.
|
||||
#[get("/api/json_stream")]
|
||||
async fn json_stream() -> Result<Streaming<Dog, JsonEncoding>> {
|
||||
let (tx, rx) = futures::channel::mpsc::unbounded();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let mut count = 0;
|
||||
loop {
|
||||
let dog = Dog {
|
||||
name: format!("Dog {}", count),
|
||||
age: (count % 10) as u8,
|
||||
};
|
||||
if tx.unbounded_send(dog).is_err() {
|
||||
// If the channel is closed, stop sending chunks
|
||||
break;
|
||||
}
|
||||
count += 1;
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
||||
}
|
||||
});
|
||||
|
||||
Ok(Streaming::new(rx))
|
||||
}
|
||||
|
||||
/// An example of streaming raw bytes to the client using `Streaming<Bytes>`.
|
||||
/// This is useful for sending binary data, such as images, files, or zero-copy data.
|
||||
#[get("/api/byte_stream")]
|
||||
async fn byte_stream() -> Result<Streaming<Bytes>> {
|
||||
let (tx, rx) = futures::channel::mpsc::unbounded();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let mut count = 0;
|
||||
loop {
|
||||
let bytes = vec![count; 10];
|
||||
if tx.unbounded_send(bytes.into()).is_err() {
|
||||
break;
|
||||
}
|
||||
count = (count + 1) % 255;
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
||||
}
|
||||
});
|
||||
|
||||
Ok(Streaming::new(rx))
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
//! This example showcases how to upload files from the client to the server.
|
||||
//!
|
||||
//! We can use the `FileStream` type to handle file uploads in a streaming fashion.
|
||||
//! This allows us to handle large files without loading them entirely into memory.
|
||||
//!
|
||||
//! `FileStream` and `FileDownload` are built on multi-part form data and streams, which we
|
||||
//! also showcase here.
|
||||
|
||||
use dioxus::{
|
||||
fullstack::{ByteStream, FileStream},
|
||||
prelude::*,
|
||||
};
|
||||
use dioxus_html::{FileData, HasFileData};
|
||||
use futures::StreamExt;
|
||||
|
||||
fn main() {
|
||||
dioxus::launch(app);
|
||||
}
|
||||
|
||||
fn app() -> Element {
|
||||
// Dioxus provides the `FileStream` type for efficiently uploading files in a streaming fashion.
|
||||
// This approach automatically automatically sets relevant metadata such as headers like
|
||||
// Content-Type, Content-Length, and Content-Disposition.
|
||||
//
|
||||
// The `FileStream` type can be created from a `FileData` instance using `.into()`.
|
||||
// This approach is better suited for public-facing APIs where standard headers are expected.
|
||||
//
|
||||
// `FileStream` uses the platform's native file streaming capabilities when available,
|
||||
// making it more efficient than manually streaming bytes.
|
||||
let mut upload_as_file_upload = use_action(move |files: Vec<FileData>| async move {
|
||||
for file in files {
|
||||
upload_file_as_filestream(file.into()).await?;
|
||||
}
|
||||
dioxus::Ok(())
|
||||
});
|
||||
|
||||
// We can upload files by directly using the `ByteStream` type. With this approach, we need to
|
||||
// specify the file name and size as query parameters since its an opaque stream.
|
||||
//
|
||||
// The `FileData` type has a `byte_stream` method which returns a `Pin<Box<dyn Stream<Item = Bytes> + Send>>`
|
||||
// that we can turn into a `ByteStream` with `.into()`.
|
||||
//
|
||||
// In WASM, this will buffer the entire file in memory, so it's not the most efficient way to upload files.
|
||||
// This approach is best suited for data created by the user in the browser.
|
||||
let mut upload_files_as_bytestream = use_action(move |files: Vec<FileData>| async move {
|
||||
info!("Uploading {} files", files.len());
|
||||
for file in files {
|
||||
upload_as_bytestream(file.name(), file.size(), file.byte_stream().into()).await?;
|
||||
}
|
||||
dioxus::Ok(())
|
||||
});
|
||||
|
||||
let mut download_file = use_action(move || async move {
|
||||
let mut file = download_as_filestream().await?;
|
||||
let mut bytes = vec![];
|
||||
|
||||
info!("Downloaded file: {:?}", file);
|
||||
|
||||
while let Some(Ok(chunk)) = file.next().await {
|
||||
bytes.extend_from_slice(&chunk);
|
||||
}
|
||||
|
||||
dioxus::Ok(String::from_utf8_lossy(&bytes).to_string())
|
||||
});
|
||||
|
||||
rsx! {
|
||||
Stylesheet { href: asset!("/examples/assets/file_upload.css") }
|
||||
div {
|
||||
max_width: "600px",
|
||||
margin: "auto",
|
||||
h1 { "File upload example" }
|
||||
div {
|
||||
h3 { "Upload as FileUpload" }
|
||||
div {
|
||||
class: "drop-zone",
|
||||
ondragover: move |evt| evt.prevent_default(),
|
||||
ondrop: move |evt| async move {
|
||||
evt.prevent_default();
|
||||
upload_as_file_upload.call(evt.files()).await;
|
||||
},
|
||||
"Drop files here"
|
||||
}
|
||||
pre { "{upload_as_file_upload.result():?}" }
|
||||
}
|
||||
|
||||
div {
|
||||
h3 { "Upload as ByteStream" }
|
||||
div {
|
||||
class: "drop-zone",
|
||||
ondragover: move |evt| evt.prevent_default(),
|
||||
ondrop: move |evt| async move {
|
||||
evt.prevent_default();
|
||||
upload_files_as_bytestream.call(evt.files()).await;
|
||||
},
|
||||
"Drop files here"
|
||||
}
|
||||
}
|
||||
|
||||
div {
|
||||
h3 { "Download a file from the server" }
|
||||
button { onclick: move |_| download_file.call(), "Download file" }
|
||||
if let Some(Ok(content)) = &download_file.result() {
|
||||
pre { "{content}" }
|
||||
} else if let Some(Err(e)) = &download_file.result() {
|
||||
pre { "Error downloading file: {e}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Upload a file using the `FileStream` type which automatically sets relevant metadata
|
||||
/// as headers like Content-Type, Content-Length, and Content-Disposition.
|
||||
#[post("/api/upload_as_file_stream")]
|
||||
async fn upload_file_as_filestream(mut upload: FileStream) -> Result<u32> {
|
||||
use futures::StreamExt;
|
||||
use std::env::temp_dir;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
info!("Received file upload: {:?}", upload);
|
||||
|
||||
// Create a temporary file to write the uploaded data to.
|
||||
let upload_file = std::path::absolute(temp_dir().join(upload.file_name()))?;
|
||||
|
||||
// Reject paths that are outside the temp directory for security reasons.
|
||||
if !upload_file.starts_with(temp_dir()) {
|
||||
HttpError::bad_request("Invalid file path")?;
|
||||
}
|
||||
|
||||
info!(
|
||||
"Uploading bytes of {:?} file to {:?}",
|
||||
upload.size(),
|
||||
upload_file
|
||||
);
|
||||
|
||||
// Open the file for writing.
|
||||
tokio::fs::create_dir_all(upload_file.parent().unwrap()).await?;
|
||||
let mut file = tokio::fs::File::create(&upload_file).await?;
|
||||
let expected = upload.size();
|
||||
|
||||
// Stream the data from the request body to the file.
|
||||
let mut uploaded: u64 = 0;
|
||||
let mut errored = false;
|
||||
while let Some(chunk) = upload.next().await {
|
||||
match chunk {
|
||||
Ok(bytes) => {
|
||||
uploaded += bytes.len() as u64;
|
||||
if file.write_all(&bytes).await.is_err() {
|
||||
errored = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// 1GB max file size or attempting to upload more than expected.
|
||||
if uploaded > expected.unwrap_or(1024 * 1024 * 1024) {
|
||||
errored = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
errored = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up the file if there was an error during upload.
|
||||
if errored {
|
||||
_ = file.sync_data().await;
|
||||
let _ = tokio::fs::remove_file(&upload_file).await;
|
||||
HttpError::internal_server_error("Failed to upload file")?;
|
||||
}
|
||||
|
||||
Ok(uploaded as u32)
|
||||
}
|
||||
|
||||
/// Upload a file as a raw byte stream. This requires us to specify the file name and size
|
||||
/// as query parameters since the `ByteStream` type is an opaque stream without metadata.
|
||||
///
|
||||
/// We could also use custom headers to pass metadata if we wanted to avoid query parameters.
|
||||
#[post("/api/upload_as_bytestream?name&size")]
|
||||
async fn upload_as_bytestream(name: String, size: u64, mut stream: ByteStream) -> Result<()> {
|
||||
let mut collected = 0;
|
||||
while let Some(chunk) = stream.next().await {
|
||||
let chunk = chunk?;
|
||||
collected += chunk.len() as u64;
|
||||
|
||||
info!("Received {} bytes for file {}", chunk.len(), name);
|
||||
|
||||
if collected > size {
|
||||
HttpError::bad_request("Received more data than expected")?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Download a file from the server as a `FileStream`. This automatically sets relevant
|
||||
/// headers like Content-Type, Content-Length, and Content-Disposition.
|
||||
///
|
||||
/// This endpoint is nice because 3rd-party clients can visit it directly and download the file!
|
||||
/// Try visiting this endpoint directly in your browser.
|
||||
#[get("/api/download_as_filestream")]
|
||||
async fn download_as_filestream() -> Result<FileStream> {
|
||||
Ok(FileStream::from_path(file!()).await?)
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
//! This example demonstrates that dioxus server functions can be be called directly as a Rust
|
||||
//! function or via an HTTP request using reqwest.
|
||||
//!
|
||||
//! Dioxus server functions generated a REST endpoint that can be called using any HTTP client.
|
||||
//! By default, they also support different serialization formats like JSON and CBOR. Try changing
|
||||
//! your `accept` header to see the different formats.
|
||||
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn main() {
|
||||
dioxus::launch(app);
|
||||
}
|
||||
|
||||
fn app() -> Element {
|
||||
let mut user_from_server_fn = use_action(get_user);
|
||||
|
||||
let mut user_from_reqwest = use_action(move |id: i32| async move {
|
||||
let port = dioxus::cli_config::server_port().unwrap_or(8080);
|
||||
reqwest::get(&format!("http://localhost:{}/api/user/{}", port, id))
|
||||
.await?
|
||||
.json::<User>()
|
||||
.await
|
||||
});
|
||||
|
||||
rsx! {
|
||||
button { onclick: move |_| user_from_server_fn.call(123), "Fetch Data" }
|
||||
button { onclick: move |_| user_from_reqwest.call(456), "Fetch From Endpoint" }
|
||||
div { display: "flex", flex_direction: "column",
|
||||
pre { "User from server: {user_from_server_fn.value():?}", }
|
||||
pre { "User from server: {user_from_reqwest.value():?}", }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug)]
|
||||
struct User {
|
||||
id: String,
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[get("/api/user/{id}")]
|
||||
async fn get_user(id: i32) -> Result<User> {
|
||||
Ok(User {
|
||||
id: id.to_string(),
|
||||
name: "John Doe".into(),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
//! This example showcases the built-in websocket functionality in Dioxus Fullstack.
|
||||
//!
|
||||
//! We can create a new websocket endpoint that takes the WebSocketOptions as a body and returns
|
||||
//! a `Websocket` instance that the client uses to communicate with the server.
|
||||
//!
|
||||
//! The `Websocket` type is generic over the message types and the encoding used to serialize the messages.
|
||||
//!
|
||||
//! By default, we use `JsonEncoding`, but in this example, we use `CborEncoding` to demonstrate that
|
||||
//! binary encodings also work.
|
||||
//!
|
||||
//! The `use_websocket` hook wraps the `Websocket` instance and provides a reactive interface to the
|
||||
//! state of the connection, as well as methods to send and receive messages.
|
||||
//!
|
||||
//! Because the websocket is generic over the message types, calls to `.recv()` and `.send()` are
|
||||
//! strongly typed, making it easy to send and receive messages without having to manually
|
||||
//! serialize and deserialize them.
|
||||
|
||||
use dioxus::{fullstack::CborEncoding, prelude::*};
|
||||
use dioxus_fullstack::{WebSocketOptions, Websocket, use_websocket};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
fn main() {
|
||||
dioxus::launch(app);
|
||||
}
|
||||
|
||||
fn app() -> Element {
|
||||
// Track the messages we've received from the server.
|
||||
let mut messages = use_signal(std::vec::Vec::new);
|
||||
|
||||
// The `use_websocket` wraps the `WebSocket` connection and provides a reactive handle to easily
|
||||
// send and receive messages and track the connection state.
|
||||
//
|
||||
// We can customize the websocket connection with the `WebSocketOptions` struct, allowing us to
|
||||
// set things like custom headers, protocols, reconnection strategies, etc.
|
||||
let mut socket = use_websocket(|| uppercase_ws("John Doe".into(), 30, WebSocketOptions::new()));
|
||||
|
||||
// Calling `.recv()` automatically waits for the connection to be established and deserializes
|
||||
// messages as they arrive.
|
||||
use_future(move || async move {
|
||||
while let Ok(msg) = socket.recv().await {
|
||||
messages.push(msg);
|
||||
}
|
||||
});
|
||||
|
||||
rsx! {
|
||||
h1 { "WebSocket Example" }
|
||||
p { "Type a message and see it echoed back in uppercase!" }
|
||||
p { "Connection status: {socket.status():?}" }
|
||||
input {
|
||||
placeholder: "Type a message",
|
||||
oninput: move |e| async move { _ = socket.send(ClientEvent::TextInput(e.value())).await; },
|
||||
}
|
||||
button { onclick: move |_| messages.clear(), "Clear messages" }
|
||||
for message in messages.read().iter().rev() {
|
||||
pre { "{message:?}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
enum ClientEvent {
|
||||
TextInput(String),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
enum ServerEvent {
|
||||
Uppercase(String),
|
||||
}
|
||||
|
||||
#[get("/api/uppercase_ws?name&age")]
|
||||
async fn uppercase_ws(
|
||||
name: String,
|
||||
age: i32,
|
||||
options: WebSocketOptions,
|
||||
) -> Result<Websocket<ClientEvent, ServerEvent, CborEncoding>> {
|
||||
Ok(options.on_upgrade(move |mut socket| async move {
|
||||
// send back a greeting message
|
||||
_ = socket
|
||||
.send(ServerEvent::Uppercase(format!(
|
||||
"Fist message from server: Hello, {}! You are {} years old.",
|
||||
name, age
|
||||
)))
|
||||
.await;
|
||||
|
||||
// Loop and echo back uppercase messages
|
||||
while let Ok(ClientEvent::TextInput(next)) = socket.recv().await {
|
||||
_ = socket.send(ServerEvent::Uppercase(next)).await;
|
||||
}
|
||||
}))
|
||||
}
|
||||
Reference in new issue
Block a user