Browse Source

create auth example

Evan Almloff 2 years ago
parent
commit
27b0c2683b

+ 1 - 0
Cargo.toml

@@ -29,6 +29,7 @@ members = [
     "packages/fullstack/examples/axum-hello-world",
     "packages/fullstack/examples/axum-router",
     "packages/fullstack/examples/axum-desktop",
+    "packages/fullstack/examples/axum-auth",
     "packages/fullstack/examples/salvo-hello-world",
     "packages/fullstack/examples/warp-hello-world",
     "packages/fullstack/examples/static-hydrated",

+ 3 - 0
packages/fullstack/examples/axum-auth/.gitignore

@@ -0,0 +1,3 @@
+dist
+target
+static

+ 52 - 0
packages/fullstack/examples/axum-auth/Cargo.toml

@@ -0,0 +1,52 @@
+[package]
+name = "axum-auth"
+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 = { workspace = true }
+dioxus-fullstack = { workspace = true }
+axum = { version = "0.6.12", optional = true }
+tokio = { workspace = true, features = ["full"], optional = true }
+serde = "1.0.159"
+execute = "0.2.12"
+tower-http = { version = "0.4.1", features = ["auth"], optional = true }
+simple_logger = { version = "4.2.0", optional = true }
+async-trait = { version = "0.1.71", optional = true }
+sqlx = { version = "0.7.0", features = [
+    "macros",
+    "migrate",
+    "postgres",
+    "sqlite",
+    "_unstable-all-types",
+    "tls-rustls",
+    "runtime-tokio",
+], optional = true }
+anyhow = "1.0.71"
+http = { version = "0.2.9", optional = true }
+
+[dependencies.axum_session]
+version = "0.3.0"
+features = ["sqlite-rustls"]
+optional = true
+
+[dependencies.axum_session_auth]
+version = "0.3.0"
+features = ["sqlite-rustls"]
+optional = true
+
+[features]
+default = []
+ssr = ["axum", "tokio", "dioxus-fullstack/axum", "tower-http", "simple_logger", "async-trait", "sqlx", "axum_session", "axum_session_auth", "http"]
+web = ["dioxus-web"]
+
+[profile.release]
+lto = true
+panic = "abort"
+opt-level = 'z'
+strip = true
+codegen-units = 1

+ 229 - 0
packages/fullstack/examples/axum-auth/src/auth.rs

@@ -0,0 +1,229 @@
+use async_trait::async_trait;
+use axum::{http::Method, routing::get, Router, response::{Response, IntoResponse}};
+use axum_session::{SessionConfig, SessionLayer, SessionSqlitePool, SessionStore};
+use axum_session_auth::*;
+use serde::{Deserialize, Serialize};
+use sqlx::sqlite::{SqliteConnectOptions, SqlitePool, SqlitePoolOptions};
+use std::{collections::HashSet, net::SocketAddr, str::FromStr};
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct User {
+    pub id: i32,
+    pub anonymous: bool,
+    pub username: String,
+    pub permissions: HashSet<String>,
+}
+
+#[derive(sqlx::FromRow, Clone)]
+pub struct SqlPermissionTokens {
+    pub token: String,
+}
+
+impl Default for User {
+    fn default() -> Self {
+        let mut permissions = HashSet::new();
+
+        permissions.insert("Category::View".to_owned());
+
+        Self {
+            id: 1,
+            anonymous: true,
+            username: "Guest".into(),
+            permissions,
+        }
+    }
+}
+
+#[async_trait]
+impl Authentication<User, i64, SqlitePool> for User {
+    async fn load_user(userid: i64, pool: Option<&SqlitePool>) -> Result<User, anyhow::Error> {
+        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> {
+        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 modifing 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(sqluser.into_user(Some(sql_user_perms)))
+    }
+
+    pub async fn create_user_tables(pool: &SqlitePool) {
+        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
+        .unwrap();
+
+        sqlx::query(
+            r#"
+                CREATE TABLE IF NOT EXISTS user_permissions (
+                    "user_id" INTEGER NOT NULL,
+                    "token" VARCHAR(256) NOT NULL
+                )
+        "#,
+        )
+        .execute(pool)
+        .await
+        .unwrap();
+
+        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
+        .unwrap();
+
+        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
+        .unwrap();
+
+        sqlx::query(
+            r#"
+                INSERT INTO user_permissions
+                    (user_id, token) SELECT 2, 'Category::View'
+            "#,
+        )
+        .execute(pool)
+        .await
+        .unwrap();
+    }
+}
+
+#[derive(sqlx::FromRow, Clone)]
+pub struct SqlUser {
+    pub id: i32,
+    pub anonymous: bool,
+    pub username: String,
+}
+
+impl SqlUser {
+    pub fn into_user(self, sql_user_perms: Option<Vec<SqlPermissionTokens>>) -> User {
+        User {
+            id: self.id,
+            anonymous: self.anonymous,
+            username: self.username,
+            permissions: if let Some(user_perms) = sql_user_perms {
+                user_perms
+                    .into_iter()
+                    .map(|x| x.token)
+                    .collect::<HashSet<String>>()
+            } else {
+                HashSet::<String>::new()
+            },
+        }
+    }
+}
+
+pub async fn connect_to_database() -> SqlitePool {
+    let connect_opts = SqliteConnectOptions::from_str("sqlite::memory:").unwrap();
+
+    SqlitePoolOptions::new()
+        .max_connections(5)
+        .connect_with(connect_opts)
+        .await
+        .unwrap()
+}
+
+pub struct Session(pub axum_session_auth::AuthSession<crate::auth::User, i64, axum_session_auth::SessionSqlitePool, sqlx::SqlitePool>);
+
+impl  std::ops::Deref for Session {
+    type Target = axum_session_auth::AuthSession<crate::auth::User, i64, axum_session_auth::SessionSqlitePool, sqlx::SqlitePool>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+impl  std::ops::DerefMut for Session {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.0
+    }
+}
+
+#[derive(Debug)]
+pub struct AuthSessionLayerNotFound;
+
+impl std::fmt::Display for AuthSessionLayerNotFound {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "AuthSessionLayer was not found")
+    }
+}
+
+impl std::error::Error for AuthSessionLayerNotFound {}
+
+impl IntoResponse for AuthSessionLayerNotFound {
+        fn into_response(self) -> Response {
+            (http::status::StatusCode::INTERNAL_SERVER_ERROR, "AuthSessionLayer was not found").into_response()
+        }
+    }
+
+#[async_trait]
+impl<S: std::marker::Sync + std::marker::Send> axum::extract::FromRequestParts<S> for Session {
+    type Rejection = AuthSessionLayerNotFound;
+
+    async fn from_request_parts(parts: &mut http::request::Parts, state: &S) -> Result<Self, Self::Rejection> {
+        axum_session_auth::AuthSession::<crate::auth::User, i64, axum_session_auth::SessionSqlitePool, sqlx::SqlitePool>::from_request_parts(parts, state)
+            .await
+            .map(Session)
+            .map_err(|_| AuthSessionLayerNotFound)
+    }
+}

+ 170 - 0
packages/fullstack/examples/axum-auth/src/main.rs

@@ -0,0 +1,170 @@
+//! Run with:
+//!
+//! ```sh
+//! dioxus build --features web
+//! cargo run --features ssr
+//! ```
+
+#![allow(non_snake_case, unused)]
+
+#[cfg(feature = "ssr")]
+mod auth;
+
+use dioxus::prelude::*;
+use dioxus_fullstack::prelude::*;
+use serde::{Deserialize, Serialize};
+
+fn main() {
+    #[cfg(feature = "web")]
+    // Hydrate the application on the client
+    dioxus_web::launch_cfg(app, dioxus_web::Config::new().hydrate(true));
+
+    #[cfg(feature = "ssr")]
+    {
+        use crate::auth::*;
+        use axum::routing::*;
+        use axum_session::SessionConfig;
+        use axum_session::SessionStore;
+        use axum_session_auth::AuthConfig;
+        use axum_session_auth::SessionSqlitePool;
+        simple_logger::SimpleLogger::new().init().unwrap();
+        tokio::runtime::Runtime::new()
+            .unwrap()
+            .block_on(async move {
+                let pool = connect_to_database().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();
+
+                //Create the Database table for storing our Session Data.
+                session_store.initiate().await.unwrap();
+                User::create_user_tables(&pool).await;
+
+                // build our application with some routes
+                let app = Router::new()
+                    // Server side render the application, serve static assets, and register server functions
+                    .serve_dioxus_application("", ServeConfigBuilder::new(app, ()))
+                    .layer(
+                        axum_session_auth::AuthSessionLayer::<
+                            crate::auth::User,
+                            i64,
+                            axum_session_auth::SessionSqlitePool,
+                            sqlx::SqlitePool,
+                        >::new(Some(pool))
+                        .with_config(auth_config),
+                    )
+                    .layer(axum_session::SessionLayer::new(session_store))
+                    ;
+
+                // run it
+                let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 3000));
+
+                axum::Server::bind(&addr)
+                    .serve(app.into_make_service())
+                    .await
+                    .unwrap();
+            });
+    }
+}
+
+fn app(cx: Scope) -> Element {
+    let user_name = use_state(cx, || "?".to_string());
+    let permissions = use_state(cx, || "?".to_string());
+
+    cx.render(rsx! {
+        div {
+            button {
+                onclick: move |_| {
+                    async move {
+                        login().await.unwrap();
+                    }
+                },
+                "Login Test User"
+            }
+        }
+        div {
+            button {
+                onclick: move |_| {
+                    to_owned![user_name];
+                    async move {
+                        if let Ok(data) = get_user_name().await {
+                            user_name.set(data);
+                        }
+                    }
+                },
+                "Get User Name"
+            }
+            "User name: {user_name}"
+        }
+        div {
+            button {
+                onclick: move |_| {
+                    to_owned![permissions];
+                    async move {
+                        if let Ok(data) = get_permissions().await {
+                            permissions.set(data);
+                        }
+                    }
+                },
+                "Get Permissions"
+            }
+            "Permissions: {permissions}"
+        }
+    })
+}
+
+#[server(GetUserName)]
+pub async fn get_user_name(
+    #[extract]
+    session : crate::auth::Session
+) -> Result<String, ServerFnError> {
+    Ok(session.0.current_user.unwrap().username
+    .to_string())
+}
+
+#[server(Login)]
+pub async fn login(
+    #[extract]
+    auth: crate::auth::Session
+) -> Result<(), ServerFnError> {
+    auth.login_user(2);
+    Ok(())
+}
+
+#[server(Permissions)]
+pub async fn get_permissions(
+    #[extract]
+    method: axum::http::Method,
+    #[extract]
+    auth: crate::auth::Session,
+) -> Result<String, ServerFnError> {
+    let current_user = auth.current_user.clone().unwrap_or_default();
+
+    // lets check permissions only and not worry about if they are anon or not
+    if !axum_session_auth::Auth::<crate::auth::User, i64, sqlx::SqlitePool>::build([axum::http::Method::GET], false)
+        .requires(axum_session_auth::Rights::any([
+            axum_session_auth::Rights::permission("Category::View"),
+            axum_session_auth::Rights::permission("Admin::View"),
+        ]))
+        .validate(&current_user, &method, None)
+        .await
+    {
+        return Ok(format!(
+            "User {}, Does not have permissions needed to view this page please login",
+            current_user.username
+        ));
+    }
+
+    Ok(format!(
+        "User has Permissions needed. Here are the Users permissions: {:?}",
+        current_user.permissions
+    ))
+}

+ 1 - 1
packages/fullstack/src/props_html/deserialize_props.rs

@@ -26,6 +26,6 @@ pub fn get_root_props_from_document<T: DeserializeOwned>() -> Option<T> {
             .get_element_by_id("dioxus-storage")?
             .get_attribute("data-serialized")?;
 
-        serde_from_string(&attribute)
+        serde_from_bytes(attribute.as_bytes())
     }
 }