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

+61
View File
@@ -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"]
+144
View File
@@ -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(())
}
}
+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
))
}