create auth example

This commit is contained in:
Evan Almloff committed 2023-07-08 10:37:28 -07:00
1 parent 453d087ed6
commit 27b0c2683b
6 files changed
+456 -1

No files matched your search

+1
View File
@@ -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",
@@ -0,0 +1,3 @@
dist
target
static
@@ -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
@@ -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)
}
}
@@ -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
))
}
@@ -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())
}
}