Remove duplicated auth example
This commit is contained in:
1 parent
05f3b8d20f
commit
fada28f7b1
8 files changed
+143
-499
No files matched your search
Generated
-21
@@ -7408,27 +7408,6 @@ dependencies = [
|
|||||||
"tower-http",
|
"tower-http",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "fullstack-auth-example-with-fullstack"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"anyhow",
|
|
||||||
"async-trait",
|
|
||||||
"axum 0.8.4",
|
|
||||||
"axum_session",
|
|
||||||
"axum_session_auth",
|
|
||||||
"axum_session_sqlx",
|
|
||||||
"dioxus",
|
|
||||||
"dioxus-web",
|
|
||||||
"execute",
|
|
||||||
"http 1.3.1",
|
|
||||||
"serde",
|
|
||||||
"sqlx",
|
|
||||||
"tokio",
|
|
||||||
"tower 0.5.2",
|
|
||||||
"tower-http",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fullstack-desktop-example"
|
name = "fullstack-desktop-example"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|||||||
@@ -110,7 +110,6 @@ members = [
|
|||||||
"examples/07-fullstack/router",
|
"examples/07-fullstack/router",
|
||||||
"examples/07-fullstack/desktop",
|
"examples/07-fullstack/desktop",
|
||||||
"examples/07-fullstack/auth",
|
"examples/07-fullstack/auth",
|
||||||
"examples/07-fullstack/auth-with-fullstack",
|
|
||||||
"examples/07-fullstack/ssr-only",
|
"examples/07-fullstack/ssr-only",
|
||||||
|
|
||||||
# Integrations
|
# Integrations
|
||||||
|
|||||||
@@ -1,61 +0,0 @@
|
|||||||
[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"]
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
//! 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,158 +0,0 @@
|
|||||||
//! 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,7 +10,7 @@ publish = false
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
dioxus-web = { workspace = true, features = ["hydrate"], optional = true }
|
dioxus-web = { workspace = true, features = ["hydrate"], optional = true }
|
||||||
dioxus = { features = ["fullstack"], workspace = true }
|
dioxus = { features = ["fullstack"], workspace = true }
|
||||||
axum = { workspace = true, optional = true }
|
axum = { workspace = true, optional = true, features = ["macros"]}
|
||||||
tokio = { workspace = true, features = ["full"], optional = true }
|
tokio = { workspace = true, features = ["full"], optional = true }
|
||||||
tower-http = { workspace = true, features = ["auth"], optional = true }
|
tower-http = { workspace = true, features = ["auth"], optional = true }
|
||||||
async-trait = { version = "0.1.88", optional = true }
|
async-trait = { version = "0.1.88", optional = true }
|
||||||
@@ -44,7 +44,7 @@ features = ["sqlite"]
|
|||||||
optional = true
|
optional = true
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = ["web", "server"]
|
||||||
server = [
|
server = [
|
||||||
"dioxus/server",
|
"dioxus/server",
|
||||||
"dep:axum",
|
"dep:axum",
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
//! 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 async_trait::async_trait;
|
||||||
use axum_session_auth::*;
|
use axum_session_auth::*;
|
||||||
use axum_session_sqlx::SessionSqlitePool;
|
use axum_session_sqlx::SessionSqlitePool;
|
||||||
@@ -5,8 +8,9 @@ use serde::{Deserialize, Serialize};
|
|||||||
use sqlx::sqlite::SqlitePool;
|
use sqlx::sqlite::SqlitePool;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
pub(crate) type Session =
|
pub(crate) type Session = axum_session_auth::AuthSession<User, i64, SessionSqlitePool, SqlitePool>;
|
||||||
axum_session_auth::AuthSession<User, i64, SessionSqlitePool, sqlx::SqlitePool>;
|
pub(crate) type AuthLayer =
|
||||||
|
axum_session_auth::AuthSessionLayer<User, i64, SessionSqlitePool, SqlitePool>;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub(crate) struct User {
|
pub(crate) struct User {
|
||||||
@@ -24,11 +28,36 @@ pub(crate) struct SqlPermissionTokens {
|
|||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Authentication<User, i64, SqlitePool> for User {
|
impl Authentication<User, i64, SqlitePool> for User {
|
||||||
async fn load_user(userid: i64, pool: Option<&SqlitePool>) -> Result<User, anyhow::Error> {
|
async fn load_user(userid: i64, pool: Option<&SqlitePool>) -> Result<User, anyhow::Error> {
|
||||||
let pool = pool.unwrap();
|
let db = pool.unwrap();
|
||||||
|
|
||||||
User::get_user(userid, pool)
|
#[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
|
.await
|
||||||
.ok_or_else(|| anyhow::anyhow!("Could not load user"))
|
.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 {
|
fn is_authenticated(&self) -> bool {
|
||||||
@@ -50,95 +79,3 @@ impl HasPermission<SqlitePool> for User {
|
|||||||
self.permissions.contains(perm)
|
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(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,129 +1,158 @@
|
|||||||
#[cfg(feature = "server")]
|
//! This example showcases how to use the `axum-session-auth` crate with Dioxus fullstack.
|
||||||
mod auth;
|
//! 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::*;
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
#[cfg(feature = "server")]
|
||||||
|
mod auth;
|
||||||
|
|
||||||
fn main() {
|
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")]
|
#[cfg(feature = "server")]
|
||||||
dioxus::serve(|| async {
|
dioxus::serve(|| async {
|
||||||
use crate::auth::*;
|
use crate::auth::*;
|
||||||
use axum_session::{SessionConfig, SessionLayer, SessionStore};
|
use axum_session::{SessionConfig, SessionLayer, SessionStore};
|
||||||
use axum_session_auth::AuthConfig;
|
use axum_session_auth::AuthConfig;
|
||||||
use axum_session_sqlx::SessionSqlitePool;
|
use axum_session_sqlx::SessionSqlitePool;
|
||||||
use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
|
use sqlx::{sqlite::SqlitePoolOptions, Executor};
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
let pool = SqlitePoolOptions::new()
|
// Create an in-memory SQLite database and set up our tables
|
||||||
.max_connections(5)
|
let db = SqlitePoolOptions::new()
|
||||||
.connect_with(SqliteConnectOptions::from_str("sqlite::memory:").unwrap())
|
.max_connections(20)
|
||||||
|
.connect_with("sqlite::memory:".parse()?)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// This Defaults as normal Cookies.
|
// Create the tables (sessions, users)
|
||||||
// To enable Private cookies for integrity, and authenticity please check the next Example.
|
db.execute(r#"CREATE TABLE IF NOT EXISTS users ( "id" INTEGER PRIMARY KEY, "anonymous" BOOLEAN NOT NULL, "username" VARCHAR(256) NOT NULL )"#,)
|
||||||
let session_config = SessionConfig::default().with_table_name("test_table");
|
.await?;
|
||||||
let auth_config = AuthConfig::<i64>::default().with_anonymous_user_id(Some(1));
|
db.execute(r#"CREATE TABLE IF NOT EXISTS user_permissions ( "user_id" INTEGER NOT NULL, "token" VARCHAR(256) NOT NULL)"#,)
|
||||||
let session_store =
|
.await?;
|
||||||
SessionStore::<SessionSqlitePool>::new(Some(pool.clone().into()), session_config)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
User::create_user_tables(&pool).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?;
|
||||||
|
|
||||||
// build our application with some routes
|
// Make sure our test user has the ability to view categories
|
||||||
Ok(
|
db.execute(r#"INSERT INTO user_permissions (user_id, token) SELECT 2, 'Category::View'"#)
|
||||||
dioxus::server::router(app)
|
.await?;
|
||||||
.layer(
|
|
||||||
axum_session_auth::AuthSessionLayer::<
|
// Create an axum router that dioxus will attach the app to
|
||||||
User,
|
Ok(dioxus::server::router(app)
|
||||||
i64,
|
.layer(
|
||||||
SessionSqlitePool,
|
AuthLayer::new(Some(db.clone()))
|
||||||
sqlx::SqlitePool,
|
.with_config(AuthConfig::<i64>::default().with_anonymous_user_id(Some(1))),
|
||||||
>::new(Some(pool))
|
)
|
||||||
.with_config(auth_config),
|
.layer(SessionLayer::new(
|
||||||
|
SessionStore::<SessionSqlitePool>::new(
|
||||||
|
Some(db.into()),
|
||||||
|
SessionConfig::default().with_table_name("test_table"),
|
||||||
)
|
)
|
||||||
.layer(SessionLayer::new(session_store)),
|
.await?,
|
||||||
)
|
)))
|
||||||
});
|
});
|
||||||
|
|
||||||
dioxus::launch(app);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The UI for our app - is just a few buttons to call our server functions and display the results.
|
||||||
fn app() -> Element {
|
fn app() -> Element {
|
||||||
|
let mut login = use_action(login);
|
||||||
let mut user_name = use_action(get_user_name);
|
let mut user_name = use_action(get_user_name);
|
||||||
let mut permissions = use_action(get_permissions);
|
let mut permissions = use_action(get_permissions);
|
||||||
let mut login = use_action(login);
|
let mut logout = use_action(logout);
|
||||||
|
|
||||||
|
let fetch_new = move |_| async move {
|
||||||
|
user_name.call().await;
|
||||||
|
permissions.call().await;
|
||||||
|
};
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
div {
|
div {
|
||||||
button {
|
button {
|
||||||
onclick: move |_| {
|
onclick: move |_| async move {
|
||||||
login.call();
|
login.call().await;
|
||||||
},
|
},
|
||||||
"Login Test User"
|
"Login Test User"
|
||||||
}
|
}
|
||||||
}
|
|
||||||
div {
|
|
||||||
button {
|
button {
|
||||||
onclick: move |_| {
|
onclick: move |_| async move {
|
||||||
user_name.call();
|
logout.call().await;
|
||||||
},
|
},
|
||||||
"Get User Name"
|
"Logout"
|
||||||
}
|
}
|
||||||
"User name: {user_name.value().unwrap_or_default()}"
|
|
||||||
}
|
|
||||||
div {
|
|
||||||
button {
|
button {
|
||||||
onclick: move |_| {
|
onclick: fetch_new,
|
||||||
permissions.call();
|
"Fetch User Info"
|
||||||
},
|
|
||||||
"Get Permissions"
|
|
||||||
}
|
}
|
||||||
"Permissions: {permissions.value().unwrap_or_default()}"
|
|
||||||
|
pre { "Logged in: {login.result():?}" }
|
||||||
|
pre { "User name: {user_name.result():?}" }
|
||||||
|
pre { "Permissions: {permissions.result():?}" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/api/user/name", auth: auth::Session)]
|
/// We use the `auth::Session` extractor to get access to the current user session.
|
||||||
pub async fn get_user_name() -> Result<String> {
|
/// This lets us modify the user session, log in/out, and access the current user.
|
||||||
Ok(auth
|
|
||||||
.current_user
|
|
||||||
.context("UNAUTHORIZED")?
|
|
||||||
.username
|
|
||||||
.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/api/user/login", auth: auth::Session)]
|
#[post("/api/user/login", auth: auth::Session)]
|
||||||
pub async fn login() -> Result<()> {
|
pub async fn login() -> Result<()> {
|
||||||
auth.login_user(2);
|
auth.login_user(2);
|
||||||
Ok(())
|
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)]
|
#[get("/api/user/permissions", auth: auth::Session)]
|
||||||
pub async fn get_permissions() -> Result<String> {
|
pub async fn get_permissions() -> Result<HashSet<String>> {
|
||||||
use crate::auth::User;
|
use crate::auth::User;
|
||||||
use axum_session_auth::{Auth, Rights};
|
use axum_session_auth::{Auth, Rights};
|
||||||
|
|
||||||
let user = auth.current_user.as_ref().context("UNAUTHORIZED")?;
|
let user = auth.current_user.unwrap();
|
||||||
|
|
||||||
// lets check permissions only and not worry about if they are anon or not
|
Auth::<User, i64, sqlx::SqlitePool>::build([axum::http::Method::GET], false)
|
||||||
let has_permissions =
|
.requires(Rights::any([
|
||||||
Auth::<User, i64, sqlx::SqlitePool>::build([axum::http::Method::POST], false)
|
Rights::permission("Category::View"),
|
||||||
.requires(Rights::any([
|
Rights::permission("Admin::View"),
|
||||||
Rights::permission("Category::View"),
|
]))
|
||||||
Rights::permission("Admin::View"),
|
.validate(&user, &axum::http::Method::GET, None)
|
||||||
]))
|
.await
|
||||||
.validate(user, &axum::http::Method::GET, None)
|
.or_unauthorized("You do not have permission to view categories")?;
|
||||||
.await;
|
|
||||||
|
|
||||||
if !has_permissions {
|
Ok(user.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
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user