├── .gitattributes ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── axumsessionauth.code-workspace ├── examples ├── NoPoolType │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── sqlx-example │ ├── Cargo.toml │ └── src │ │ └── main.rs └── surrealdb │ ├── Cargo.toml │ └── src │ └── main.rs └── src ├── auth.rs ├── cache.rs ├── config.rs ├── layer.rs ├── lib.rs ├── service.rs ├── session.rs └── user.rs /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /.vscode 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) 6 | ## Unreleased 7 | 8 | 9 | ## 0.16.0 (17. Janurary, 2025) 10 | ### Changed 11 | - (Breaking) axum_session 0.16" 12 | 13 | ## 0.15.0 (1. Janurary, 2025) 14 | ### Changed 15 | - (Breaking) Axum 0.8.1 and axum_session 0.15" 16 | 17 | ## 0.14.1 (6. September, 2024) 18 | ### Changed 19 | - cache hit/miss traces from warn level to debug level @jhoobergs 20 | 21 | ## 0.14.0 (12. April, 2024) 22 | ### Changed 23 | - (Breaking) Updated to Axum Session 0.14.0 24 | 25 | ## 0.13.0 (11. March, 2024) 26 | ### Changed 27 | - (Breaking) Updated to Axum Session 0.13.0 28 | 29 | ## 0.12.1 (14. February, 2024) 30 | ### Fixed 31 | - Multiple Cache Existing due to Service fn getting called per async thread. In older versions please Disable cache unless you upgrade to this version. 32 | 33 | ## 0.12.0 (1. January, 2024) 34 | ### Changed 35 | - (Breaking) Updated to Axum_session 0.12.0 36 | - added tracing for debugging. 37 | - Fixed the return to not be an unwrap if session doesnt exist. 38 | 39 | ## 0.11.0 (21. December, 2023) 40 | ### Changed 41 | - (Breaking) Updated to Axum_session 0.11.0 for Redis_pool 0.3 42 | 43 | ## 0.10.1 (12. December, 2023) 44 | ### Fixed 45 | - Documents not building. 46 | 47 | ## 0.10.0 (27. November, 2023) 48 | ### Changed 49 | - (Breaking) Updated to axum_session 0.10.0 50 | - (Breaking) Updated to axum 0.7 51 | - (Breaking) merged all surreal features under a single surreal feature 52 | 53 | ## 0.9.0 (13. November, 2023) 54 | ### Changed 55 | - Updated axum_session to 0.9.0. 56 | - Made AuthStatus Publically visiable 57 | 58 | ### Added 59 | - sync_user_id function to Session. 60 | - StaleUser to AuthStatus Enum. 61 | 62 | ### Fixed 63 | - Config session_id is no longer ignored for login and logout. 64 | 65 | ## 0.8.0 (23. October, 2023) 66 | ### Changed 67 | - Updated axum_session to 0.8.0. 68 | 69 | ### Added 70 | - Added `AuthStatus`, `AuthSession::is_logged_in()`, `AuthSession::reload_user` and `AuthSession::update_user_expiration`. 71 | - Features locked behind an advanced feature. 72 | 73 | ## 0.7.0 (4. October, 2023) 74 | ### Changed 75 | - Updated axum_session to 0.7.0. 76 | - Removed uneeded clone. 77 | 78 | ## 0.6.0 (18. September, 2023) 79 | ### Changed 80 | - Updated axum_session to 0.6.0. 81 | 82 | ## 0.5.0 (6. September, 2023) 83 | ### Changed 84 | - Updated axum_session to 0.5.0. 85 | 86 | ## 0.4.0 (3. September, 2023) 87 | ### Changed 88 | - Updated axum_session to 0.4.0. 89 | 90 | ## 0.3.1 (7. August, 2023) 91 | ### Changed 92 | - Updated github link to renamed repo. 93 | 94 | ## 0.3.0 (7. July, 2023) 95 | ### Changed 96 | - Updated to axum_session 0.3.0. 97 | 98 | ## 0.2.1 (5. May, 2023) 99 | ### Fixed 100 | - RUSTSEC-2020-0071 from chrono. (damccull) 101 | 102 | ## 0.2.0 (11. April, 2023) 103 | ### Changed 104 | - Updated to axum_session 0.2.0. 105 | 106 | ## 0.1.1 (31. March, 2023) 107 | ### Added 108 | - Added Surrealdb features. 109 | 110 | ## 0.1.0 (13. March, 2023) 111 | ### Added 112 | - Initial rename and release. 113 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver="2" 3 | members = [ 4 | ".", 5 | "examples/sqlx-example/", 6 | "examples/NoPoolType/", 7 | "examples/surrealdb/", 8 | ] 9 | 10 | [package] 11 | name = "axum_session_auth" 12 | version = "0.16.0" 13 | authors = ["Andrew Wheeler "] 14 | description = "Library to Provide a User Authentication and privilege Token Checks. It requires the Axum_Session library." 15 | edition = "2021" 16 | license = "MIT OR Apache-2.0" 17 | readme = "README.md" 18 | documentation = "https://docs.rs/axum_session_auth" 19 | keywords = ["Axum", "Tower", "SQLx", "Session", "Authentication"] 20 | repository = "https://github.com/AscendingCreations/AxumSessionAuth" 21 | 22 | [features] 23 | key-store = ["axum_session/key-store"] 24 | rest_mode = ["axum_session/rest_mode"] 25 | advanced = ["axum_session/advanced"] 26 | 27 | [dependencies] 28 | axum-core = "0.5.0" 29 | async-trait = "0.1.83" 30 | anyhow = "1.0.95" 31 | async-recursion = "1.1.1" 32 | http = "1.2.0" 33 | tower-layer = "0.3.3" 34 | tower-service = "0.3.3" 35 | futures = "0.3.31" 36 | bytes = "1.9.0" 37 | http-body = "1.0.1" 38 | dashmap = "6.1.0" 39 | chrono = { version = "0.4.38", default-features = false, features = ["clock", "serde", "std"] } 40 | tokio = { version = "1.42.0", features = ["full"] } 41 | serde = "1.0.209" 42 | tracing = "0.1.40" 43 | 44 | [dependencies.axum_session] 45 | #path = "C:/Sources/AxumSession" 46 | #git = "https://github.com/AscendingCreations/AxumSessions.git" 47 | #branch = "axum0.6" 48 | version = "0.16.0" 49 | 50 | [package.metadata.docs.rs] 51 | features = ["advanced"] 52 | rustdoc-args = ["--document-private-items"] 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Ascending Creations 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Axum Session Auth 3 |

4 | 5 | Library to Provide a User Authentication and privilege Token Checks. It requires the Axum_Session library. 6 | This library will help by making it so User ID or authorizations are not stored on the Client side but rather on the Server side. 7 | The Authorization is linked by the Clients Serverside Session ID which is stored on the Client side. Formally known as Axum Sessions Auth 8 | 9 | [![https://crates.io/crates/axum_session_auth](https://img.shields.io/crates/v/axum_session_auth?style=plastic)](https://crates.io/crates/axum_session_auth) 10 | [![Docs](https://docs.rs/axum_session_auth/badge.svg)](https://docs.rs/axum_session_auth) 11 | 12 | - Wraps `axum_session` for data management serverside. 13 | - Right Management API 14 | - Auto load of user Data upon Page loads. 15 | - User Data cache to Avoid Repeated Database calls when not needed. 16 | 17 | # Help 18 | 19 | If you need help with this library or have suggestions please go to our [Discord Group](https://discord.gg/gVXNDwpS3Z) 20 | 21 | ## Install 22 | 23 | Sessions Authentication uses [`tokio`] runtime and ['axum_session']; 24 | 25 | [`tokio`]: https://github.com/tokio-rs/tokio 26 | [`axum_session`]: https://crates.io/crates/axum_session 27 | 28 | ```toml 29 | # Cargo.toml 30 | [dependencies] 31 | # Postgres + rustls 32 | axum_session_auth = { version = "0.16.0" } 33 | axum_session_sqlx = { version = "0.5.0" } 34 | ``` 35 | 36 | #### Cargo Feature Flags 37 | 38 | | Features | Description | 39 | | ----------------------------- | ---------------------------------------------------------------------------------------------- | 40 | | `advanced` | Enable functions allowing more direct control over the sessions. | 41 | | `rest_mode` | Disables Cookie Handlering In place of Header only usage for Rest API Requests and Responses. | 42 | | `key-store` | Enabled the optional key storage. Will increase ram usage based on Fastbloom settings. | 43 | 44 | 45 | | Database Crate | Persistent | Description | 46 | | ----------------------------------------------------------------------------------- | ---------- | ----------------------------------------------------------- | 47 | | [`axum_session_sqlx`](https://crates.io/crates/axum_session_sqlx) | Yes | Sqlx session store | 48 | | [`axum_session_surreal`](https://crates.io/crates/axum_session_surreal) | Yes | Surreal session store | 49 | | [`axum_session_mongo`](https://crates.io/crates/axum_session_mongo) | Yes | Mongo session store | 50 | | [`axum_session_redispool`](https://crates.io/crates/axum_session_redispool) | Yes | RedisPool session store | 51 | 52 | 53 | # Example 54 | 55 | ```rust 56 | use sqlx::{PgPool, ConnectOptions, postgres::{PgPoolOptions, PgConnectOptions}}; 57 | use std::net::SocketAddr; 58 | use axum_session::{Session, SessionConfig, SessionLayer, DatabasePool}; 59 | use axum_session_auth::{AuthSession, AuthSessionLayer, Authentication, AuthConfig, HasPermission}; 60 | use axum_session_sqlx::SessionPgPool; 61 | use axum::{ 62 | Router, 63 | routing::get, 64 | }; 65 | 66 | #[tokio::main] 67 | async fn main() { 68 | # async { 69 | let poll = connect_to_database().await.unwrap(); 70 | 71 | let session_config = SessionConfig::default() 72 | .with_database("test") 73 | .with_table_name("test_table"); 74 | let auth_config = AuthConfig::::default().with_anonymous_user_id(Some(1)); 75 | let session_store = SessionStore::::new(Some(poll.clone().into()), session_config); 76 | 77 | // Build our application with some routes 78 | let app = Router::new() 79 | .route("/greet/:name", get(greet)) 80 | .layer(SessionLayer::new(session_store)) 81 | .layer(AuthSessionLayer::::new(Some(poll)).with_config(auth_config)); 82 | 83 | // Run it 84 | let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); 85 | tracing::debug!("listening on {}", addr); 86 | axum::Server::bind(&addr) 87 | .serve(app.into_make_service()) 88 | .await 89 | .unwrap(); 90 | # }; 91 | } 92 | 93 | // We can obtain the method to compare with the methods we allow, which is useful if this supports multiple methods. 94 | // When called, auth is loaded in the background for you. 95 | async fn greet(method: Method, auth: AuthSession) -> &'static str { 96 | let mut count: usize = auth.session.get("count").unwrap_or(0); 97 | 98 | // We will get the user if not, then a guest which should be our default. 99 | let current_user = auth.current_user.clone().unwrap_or_default(); 100 | count += 1; 101 | 102 | // Session is also included with Auth so there's no need to require it in the function arguments if you're using AuthSession. 103 | auth.session.set("count", count); 104 | 105 | // If, for some reason, you needed to update your user's permissions 106 | // or data that is cached, then you will want to clear the user cache if it is enabled. 107 | // The user Cache is enabled by default. To clear, simply use: 108 | auth.cache_clear_user(1).await; 109 | // To clear all cached user data for a large update: 110 | auth.cache_clear_all().await; 111 | 112 | // This is our Auth Permission Builder and Rights Checker. We build it with methods to check for permissions. 113 | // In this case, if the page is loaded using Method::Get, it will proceed successfully. However, if Method::Post is used, it will fail with the "no Permissions!" error. 114 | // The boolean value "false" is used to determine whether authentication is required. When set to true, it triggers the function is_authenticated(). 115 | if !Auth::::build([Method::Get], false) 116 | // We prepare which rights we accept or deny from guests or other users. 117 | .requires(Rights::none([ 118 | Rights::permission("Token::UseAdmin"), 119 | Rights::permission("Token::ModifyPerms"), 120 | ])) 121 | // We then validate the current user and method. We also pass our database along for database permissions checking if required; otherwise, None. 122 | .validate(¤t_user, &method, None) 123 | .await 124 | { 125 | // We return a "No Permissions" message if validation fails for any reason. 126 | return format!("No Permissions! for {}", current_user.username)[]; 127 | } 128 | 129 | // Since we had the is_authenticated set to false Above we will instead use it to log in our Guest user. 130 | if !auth.is_authenticated() { 131 | // Set the user ID of the User to the Session so it can be Auto Loaded the next load or redirect 132 | auth.login_user(2); 133 | // Set the session to be long term. Good for Remember me type instances. 134 | auth.remember_user(true); 135 | // We don't currently know the username until the next page access. 136 | // so Normally we would Redirect here after login if we did indeed log in. 137 | // But in this case we will just let the user know to reload the page for the example. 138 | "You have Logged in! Please Refresh the page to display the username and counter." 139 | } else { 140 | 141 | // Upon page reload, if the user possesses all necessary permissions, the method is accurate, and they are logged in, 142 | // their username and a count that increments with each page refresh will be displayed. 143 | format!("{}-{}", current_user.username, count)[..] 144 | }; 145 | } 146 | 147 | #[derive(Clone, Debug)] 148 | pub struct User { 149 | pub id: i32, 150 | pub anonymous: bool, 151 | pub username: String, 152 | } 153 | 154 | // This is only used if you want to use Token based Authentication checks 155 | #[async_trait] 156 | impl HasPermission for User { 157 | async fn has(&self, perm: &String, _pool: &Option<&PgPool>) -> bool { 158 | match &perm[..] { 159 | "Token::UseAdmin" => true, 160 | "Token::ModifyUser" => true, 161 | _ => false, 162 | } 163 | } 164 | } 165 | 166 | #[async_trait] 167 | impl Authentication for User { 168 | // This is run when the user has logged in and has not yet been Cached in the system. 169 | // Once ran it will load and cache the user. 170 | async fn load_user(userid: i64, _pool: Option<&PgPool>) -> Result { 171 | Ok(User { 172 | id: userid, 173 | anonymous: true, 174 | username: "Guest".to_string(), 175 | }) 176 | } 177 | 178 | // This function is used internally to determine if they are logged in or not. 179 | fn is_authenticated(&self) -> bool { 180 | !self.anonymous 181 | } 182 | 183 | fn is_active(&self) -> bool { 184 | !self.anonymous 185 | } 186 | 187 | fn is_anonymous(&self) -> bool { 188 | self.anonymous 189 | } 190 | } 191 | 192 | async fn connect_to_database() -> anyhow::Result> { 193 | // ... 194 | # unimplemented!() 195 | } 196 | ``` 197 | -------------------------------------------------------------------------------- /axumsessionauth.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ], 7 | "settings": {} 8 | } -------------------------------------------------------------------------------- /examples/NoPoolType/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nopool" 3 | version = "0.0.1" 4 | authors = ["Andrew Wheeler "] 5 | edition = "2021" 6 | 7 | [dependencies] 8 | axum = {version = "0.8.1"} 9 | tokio = { version = "1.40.0", features = ["full", "tracing"] } 10 | async-trait = "0.1.71" 11 | sqlx = { version = "0.8.1", features = [ 12 | "macros", 13 | "migrate", 14 | "postgres", 15 | "sqlite", 16 | "_unstable-all-types", 17 | "tls-rustls", 18 | "runtime-tokio", 19 | ] } 20 | anyhow = "1.0.71" 21 | serde = "1.0.167" 22 | axum_session_sqlx = {version = "0.5.0", features = ["sqlite"]} 23 | 24 | [dependencies.axum_session] 25 | version = "0.16.0" 26 | 27 | [dependencies.axum_session_auth] 28 | path = "../../" -------------------------------------------------------------------------------- /examples/NoPoolType/src/main.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | use axum::{http::Method, routing::get, Router}; 3 | use axum_session::{SessionConfig, SessionLayer, SessionStore}; 4 | use axum_session_auth::*; 5 | use axum_session_sqlx::SessionSqlitePool; 6 | use serde::{Deserialize, Serialize}; 7 | use sqlx::sqlite::{SqliteConnectOptions, SqlitePool, SqlitePoolOptions}; 8 | use std::sync::Arc; 9 | use std::{collections::HashSet, str::FromStr}; 10 | use tokio::net::TcpListener; 11 | 12 | #[derive(Debug, Clone, Serialize, Deserialize)] 13 | pub struct User { 14 | pub id: i32, 15 | pub anonymous: bool, 16 | pub username: String, 17 | pub permissions: HashSet, 18 | } 19 | 20 | impl Default for User { 21 | fn default() -> Self { 22 | let mut permissions = HashSet::new(); 23 | 24 | permissions.insert("Category::View".to_owned()); 25 | 26 | Self { 27 | id: 1, 28 | anonymous: true, 29 | username: "Guest".into(), 30 | permissions, 31 | } 32 | } 33 | } 34 | 35 | // We place our Type within a Arc<> so we can send it across async threads. 36 | type NullPool = Arc>; 37 | 38 | #[async_trait] 39 | impl Authentication for User { 40 | async fn load_user(userid: i64, _pool: Option<&NullPool>) -> Result { 41 | if userid == 1 { 42 | Ok(User::default()) 43 | } else { 44 | let mut permissions = HashSet::new(); 45 | 46 | permissions.insert("Category::View".to_owned()); 47 | 48 | Ok(User { 49 | id: 2, 50 | anonymous: false, 51 | username: "Test".to_owned(), 52 | permissions, 53 | }) 54 | } 55 | } 56 | 57 | fn is_authenticated(&self) -> bool { 58 | !self.anonymous 59 | } 60 | 61 | fn is_active(&self) -> bool { 62 | !self.anonymous 63 | } 64 | 65 | fn is_anonymous(&self) -> bool { 66 | self.anonymous 67 | } 68 | } 69 | 70 | #[async_trait] 71 | impl HasPermission for User { 72 | async fn has(&self, perm: &str, _pool: &Option<&NullPool>) -> bool { 73 | self.permissions.contains(perm) 74 | } 75 | } 76 | 77 | #[tokio::main] 78 | async fn main() { 79 | let pool = connect_to_database().await; 80 | 81 | //This Defaults as normal Cookies. 82 | //To enable Private cookies for integrity, and authenticity please check the next Example. 83 | let session_config = SessionConfig::default().with_table_name("test_table"); 84 | let auth_config = AuthConfig::::default().with_anonymous_user_id(Some(1)); 85 | 86 | // create SessionStore and initiate the database tables 87 | let session_store = 88 | SessionStore::::new(Some(pool.clone().into()), session_config) 89 | .await 90 | .unwrap(); 91 | 92 | // We create are NullPool here just for Sessions Auth. 93 | let nullpool = Arc::new(Option::None); 94 | 95 | // build our application with some routes 96 | let app = Router::new() 97 | .route("/", get(greet)) 98 | .route("/greet", get(greet)) 99 | .route("/login", get(login)) 100 | .route("/perm", get(perm)) 101 | .layer( 102 | AuthSessionLayer::::new(Some(nullpool)) 103 | .with_config(auth_config), 104 | ) 105 | .layer(SessionLayer::new(session_store)); 106 | 107 | // run it 108 | let listener = TcpListener::bind("0.0.0.0:3000").await.unwrap(); 109 | axum::serve(listener, app).await.unwrap(); 110 | } 111 | 112 | async fn greet(auth: AuthSession) -> String { 113 | format!( 114 | "Hello {}, Try logging in via /login or testing permissions via /perm", 115 | auth.current_user.unwrap().username 116 | ) 117 | } 118 | 119 | async fn login(auth: AuthSession) -> String { 120 | auth.login_user(2); 121 | "You are logged in as a User please try /perm to check permissions".to_owned() 122 | } 123 | 124 | async fn perm(method: Method, auth: AuthSession) -> String { 125 | let current_user = auth.current_user.clone().unwrap_or_default(); 126 | 127 | //lets check permissions only and not worry about if they are anon or not 128 | if !Auth::::build([Method::GET], false) 129 | .requires(Rights::any([ 130 | Rights::permission("Category::View"), 131 | Rights::permission("Admin::View"), 132 | ])) 133 | .validate(¤t_user, &method, None) 134 | .await 135 | { 136 | return format!( 137 | "User {}, Does not have permissions needed to view this page please login", 138 | current_user.username 139 | ); 140 | } 141 | 142 | format!( 143 | "User has Permissions needed. Here are the Users permissions: {:?}", 144 | current_user.permissions 145 | ) 146 | } 147 | 148 | async fn connect_to_database() -> SqlitePool { 149 | let connect_opts = SqliteConnectOptions::from_str("sqlite::memory:").unwrap(); 150 | 151 | SqlitePoolOptions::new() 152 | .max_connections(5) 153 | .connect_with(connect_opts) 154 | .await 155 | .unwrap() 156 | } 157 | -------------------------------------------------------------------------------- /examples/sqlx-example/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sqlx-example" 3 | version = "0.0.1" 4 | authors = ["Andrew Wheeler "] 5 | edition = "2021" 6 | 7 | [dependencies] 8 | axum = {version = "0.8.1"} 9 | tokio = { version = "1.40.0", features = ["full", "tracing"] } 10 | async-trait = "0.1.71" 11 | sqlx = { version = "0.8.1", features = [ 12 | "macros", 13 | "migrate", 14 | "postgres", 15 | "sqlite", 16 | "_unstable-all-types", 17 | "tls-rustls", 18 | "runtime-tokio", 19 | ] } 20 | anyhow = "1.0.71" 21 | serde = "1.0.167" 22 | axum_session_sqlx = {version = "0.5.0", features = ["sqlite"]} 23 | 24 | [dependencies.axum_session] 25 | version = "0.16.0" 26 | 27 | [dependencies.axum_session_auth] 28 | path = "../../" -------------------------------------------------------------------------------- /examples/sqlx-example/src/main.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | use axum::{http::Method, routing::get, Router}; 3 | use axum_session::{SessionConfig, SessionLayer, SessionStore}; 4 | use axum_session_auth::*; 5 | use axum_session_sqlx::SessionSqlitePool; 6 | use serde::{Deserialize, Serialize}; 7 | use sqlx::sqlite::{SqliteConnectOptions, SqlitePool, SqlitePoolOptions}; 8 | use std::{collections::HashSet, str::FromStr}; 9 | use tokio::net::TcpListener; 10 | 11 | #[derive(Debug, Clone, Serialize, Deserialize)] 12 | pub struct User { 13 | pub id: i32, 14 | pub anonymous: bool, 15 | pub username: String, 16 | pub permissions: HashSet, 17 | } 18 | 19 | #[derive(sqlx::FromRow, Clone)] 20 | pub struct SqlPermissionTokens { 21 | pub token: String, 22 | } 23 | 24 | impl Default for User { 25 | fn default() -> Self { 26 | let mut permissions = HashSet::new(); 27 | 28 | permissions.insert("Category::View".to_owned()); 29 | 30 | Self { 31 | id: 1, 32 | anonymous: true, 33 | username: "Guest".into(), 34 | permissions, 35 | } 36 | } 37 | } 38 | 39 | #[async_trait] 40 | impl Authentication for User { 41 | async fn load_user(userid: i64, pool: Option<&SqlitePool>) -> Result { 42 | let pool = pool.unwrap(); 43 | 44 | User::get_user(userid, pool) 45 | .await 46 | .ok_or_else(|| anyhow::anyhow!("Could not load user")) 47 | } 48 | 49 | fn is_authenticated(&self) -> bool { 50 | !self.anonymous 51 | } 52 | 53 | fn is_active(&self) -> bool { 54 | !self.anonymous 55 | } 56 | 57 | fn is_anonymous(&self) -> bool { 58 | self.anonymous 59 | } 60 | } 61 | 62 | #[async_trait] 63 | impl HasPermission for User { 64 | async fn has(&self, perm: &str, _pool: &Option<&SqlitePool>) -> bool { 65 | self.permissions.contains(perm) 66 | } 67 | } 68 | 69 | impl User { 70 | pub async fn get_user(id: i64, pool: &SqlitePool) -> Option { 71 | let sqluser = sqlx::query_as::<_, SqlUser>("SELECT * FROM users WHERE id = $1") 72 | .bind(id) 73 | .fetch_one(pool) 74 | .await 75 | .ok()?; 76 | 77 | //lets just get all the tokens the user can use, we will only use the full permissions if modifing them. 78 | let sql_user_perms = sqlx::query_as::<_, SqlPermissionTokens>( 79 | "SELECT token FROM user_permissions WHERE user_id = $1;", 80 | ) 81 | .bind(id) 82 | .fetch_all(pool) 83 | .await 84 | .ok()?; 85 | 86 | Some(sqluser.into_user(Some(sql_user_perms))) 87 | } 88 | 89 | pub async fn create_user_tables(pool: &SqlitePool) { 90 | sqlx::query( 91 | r#" 92 | CREATE TABLE IF NOT EXISTS users ( 93 | "id" INTEGER PRIMARY KEY, 94 | "anonymous" BOOLEAN NOT NULL, 95 | "username" VARCHAR(256) NOT NULL 96 | ) 97 | "#, 98 | ) 99 | .execute(pool) 100 | .await 101 | .unwrap(); 102 | 103 | sqlx::query( 104 | r#" 105 | CREATE TABLE IF NOT EXISTS user_permissions ( 106 | "user_id" INTEGER NOT NULL, 107 | "token" VARCHAR(256) NOT NULL 108 | ) 109 | "#, 110 | ) 111 | .execute(pool) 112 | .await 113 | .unwrap(); 114 | 115 | sqlx::query( 116 | r#" 117 | INSERT INTO users 118 | (id, anonymous, username) SELECT 1, true, 'Guest' 119 | ON CONFLICT(id) DO UPDATE SET 120 | anonymous = EXCLUDED.anonymous, 121 | username = EXCLUDED.username 122 | "#, 123 | ) 124 | .execute(pool) 125 | .await 126 | .unwrap(); 127 | 128 | sqlx::query( 129 | r#" 130 | INSERT INTO users 131 | (id, anonymous, username) SELECT 2, false, 'Test' 132 | ON CONFLICT(id) DO UPDATE SET 133 | anonymous = EXCLUDED.anonymous, 134 | username = EXCLUDED.username 135 | "#, 136 | ) 137 | .execute(pool) 138 | .await 139 | .unwrap(); 140 | 141 | sqlx::query( 142 | r#" 143 | INSERT INTO user_permissions 144 | (user_id, token) SELECT 2, 'Category::View' 145 | "#, 146 | ) 147 | .execute(pool) 148 | .await 149 | .unwrap(); 150 | } 151 | } 152 | 153 | #[derive(sqlx::FromRow, Clone)] 154 | pub struct SqlUser { 155 | pub id: i32, 156 | pub anonymous: bool, 157 | pub username: String, 158 | } 159 | 160 | impl SqlUser { 161 | pub fn into_user(self, sql_user_perms: Option>) -> User { 162 | User { 163 | id: self.id, 164 | anonymous: self.anonymous, 165 | username: self.username, 166 | permissions: if let Some(user_perms) = sql_user_perms { 167 | user_perms 168 | .into_iter() 169 | .map(|x| x.token) 170 | .collect::>() 171 | } else { 172 | HashSet::::new() 173 | }, 174 | } 175 | } 176 | } 177 | 178 | #[tokio::main] 179 | async fn main() { 180 | let pool = connect_to_database().await; 181 | 182 | //This Defaults as normal Cookies. 183 | //To enable Private cookies for integrity, and authenticity please check the next Example. 184 | let session_config = SessionConfig::default().with_table_name("test_table"); 185 | let auth_config = AuthConfig::::default().with_anonymous_user_id(Some(1)); 186 | 187 | // create SessionStore and initiate the database tables 188 | let session_store = 189 | SessionStore::::new(Some(pool.clone().into()), session_config) 190 | .await 191 | .unwrap(); 192 | 193 | User::create_user_tables(&pool).await; 194 | 195 | // build our application with some routes 196 | let app = Router::new() 197 | .route("/", get(greet)) 198 | .route("/greet", get(greet)) 199 | .route("/login", get(login)) 200 | .route("/perm", get(perm)) 201 | .layer( 202 | AuthSessionLayer::::new(Some(pool)) 203 | .with_config(auth_config), 204 | ) 205 | .layer(SessionLayer::new(session_store)); 206 | 207 | // run it 208 | let listener = TcpListener::bind("0.0.0.0:3000").await.unwrap(); 209 | axum::serve(listener, app).await.unwrap(); 210 | } 211 | 212 | async fn greet(auth: AuthSession) -> String { 213 | format!( 214 | "Hello {}, Try logging in via /login or testing permissions via /perm", 215 | auth.current_user.unwrap().username 216 | ) 217 | } 218 | 219 | async fn login(auth: AuthSession) -> String { 220 | auth.login_user(2); 221 | "You are logged in as a User please try /perm to check permissions".to_owned() 222 | } 223 | 224 | async fn perm( 225 | method: Method, 226 | auth: AuthSession, 227 | ) -> String { 228 | let current_user = auth.current_user.clone().unwrap_or_default(); 229 | 230 | //lets check permissions only and not worry about if they are anon or not 231 | if !Auth::::build([Method::GET], false) 232 | .requires(Rights::any([ 233 | Rights::permission("Category::View"), 234 | Rights::permission("Admin::View"), 235 | ])) 236 | .validate(¤t_user, &method, None) 237 | .await 238 | { 239 | return format!( 240 | "User {}, Does not have permissions needed to view this page please login", 241 | current_user.username 242 | ); 243 | } 244 | 245 | format!( 246 | "User has Permissions needed. Here are the Users permissions: {:?}", 247 | current_user.permissions 248 | ) 249 | } 250 | 251 | async fn connect_to_database() -> SqlitePool { 252 | let connect_opts = SqliteConnectOptions::from_str("sqlite::memory:").unwrap(); 253 | 254 | SqlitePoolOptions::new() 255 | .max_connections(5) 256 | .connect_with(connect_opts) 257 | .await 258 | .unwrap() 259 | } 260 | -------------------------------------------------------------------------------- /examples/surrealdb/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "surreal-example" 3 | version = "0.0.1" 4 | authors = ["Andrew Wheeler "] 5 | edition = "2021" 6 | 7 | [dependencies] 8 | axum = {version = "0.8.1"} 9 | tokio = { version = "1.40.0", features = ["full", "tracing"] } 10 | async-trait = "0.1.71" 11 | surrealdb = { version = "2.1.4", features = ["kv-mem"] } 12 | anyhow = "1.0.71" 13 | serde = "1.0.167" 14 | axum_session_surreal = "0.4.0" 15 | 16 | [dependencies.axum_session] 17 | version = "0.16.0" 18 | 19 | [dependencies.axum_session_auth] 20 | path = "../../" -------------------------------------------------------------------------------- /examples/surrealdb/src/main.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | use axum::{http::Method, routing::get, Router}; 3 | use axum_session::{SessionConfig, SessionLayer, SessionStore}; 4 | use axum_session_auth::*; 5 | use axum_session_surreal::SessionSurrealPool; 6 | use serde::{Deserialize, Serialize}; 7 | use std::collections::HashSet; 8 | use surrealdb::{ 9 | engine::any::{connect, Any}, 10 | opt::auth::Root, 11 | Surreal, 12 | }; 13 | use tokio::net::TcpListener; 14 | 15 | #[derive(Debug, Clone, Serialize, Deserialize)] 16 | pub struct User { 17 | pub user_id: i32, 18 | pub anonymous: bool, 19 | pub username: String, 20 | #[serde(skip)] 21 | pub permissions: HashSet, 22 | } 23 | 24 | #[derive(Serialize, Deserialize, Clone)] 25 | pub struct SqlPermissionTokens { 26 | pub token: String, 27 | } 28 | 29 | impl Default for User { 30 | fn default() -> Self { 31 | let mut permissions = HashSet::new(); 32 | 33 | permissions.insert("Category::View".to_owned()); 34 | 35 | Self { 36 | user_id: 1, 37 | anonymous: true, 38 | username: "Guest".into(), 39 | permissions, 40 | } 41 | } 42 | } 43 | 44 | #[async_trait] 45 | impl Authentication> for User { 46 | async fn load_user(userid: i64, pool: Option<&Surreal>) -> Result { 47 | let pool = pool.unwrap(); 48 | 49 | User::get_user(userid, pool) 50 | .await 51 | .ok_or_else(|| anyhow::anyhow!("Could not load user")) 52 | } 53 | 54 | fn is_authenticated(&self) -> bool { 55 | !self.anonymous 56 | } 57 | 58 | fn is_active(&self) -> bool { 59 | !self.anonymous 60 | } 61 | 62 | fn is_anonymous(&self) -> bool { 63 | self.anonymous 64 | } 65 | } 66 | 67 | #[async_trait] 68 | impl HasPermission> for User { 69 | async fn has(&self, perm: &str, _pool: &Option<&Surreal>) -> bool { 70 | self.permissions.contains(perm) 71 | } 72 | } 73 | 74 | impl User { 75 | pub async fn get_user(id: i64, pool: &Surreal) -> Option { 76 | let sqluser: Option = pool 77 | .query("SELECT username, user_id, anonymous FROM users where user_id = $user_id") 78 | .bind(("user_id", id)) 79 | .await 80 | .unwrap() 81 | .take(0) 82 | .unwrap(); 83 | 84 | //lets just get all the tokens the user can use, we will only use the full permissions if modifing them. 85 | let sql_user_perms: Vec = pool 86 | .query("SELECT token FROM user_permissions where user_id = $user_id") 87 | .bind(("user_id", id)) 88 | .await 89 | .unwrap() 90 | .take(0) 91 | .unwrap(); 92 | 93 | Some(sqluser.unwrap().into_user(Some(sql_user_perms))) 94 | } 95 | 96 | pub async fn create_user_tables(pool: &Surreal) { 97 | pool.query( 98 | " DEFINE TABLE users SCHEMAFULL; 99 | DEFINE FIELD username ON TABLE users TYPE string; 100 | DEFINE FIELD anonymous ON TABLE users TYPE bool; 101 | DEFINE FIELD user_id ON TABLE users TYPE int; 102 | ", 103 | ) 104 | .await 105 | .unwrap(); 106 | 107 | pool.query( 108 | " DEFINE TABLE user_permissions SCHEMAFULL; 109 | DEFINE FIELD token ON TABLE user_permissions TYPE string; 110 | DEFINE FIELD user_id ON TABLE user_permissions TYPE int; 111 | ", 112 | ) 113 | .await 114 | .unwrap(); 115 | 116 | pool.query( 117 | "INSERT INTO users (username, anonymous, user_id) VALUES ('Guest', true, 1), ('Test', false, 2);" 118 | ).await.unwrap(); 119 | 120 | pool.query("INSERT INTO user_permissions (token, user_id) VALUES ('Category::View', 2);") 121 | .await 122 | .unwrap(); 123 | } 124 | } 125 | 126 | #[derive(Serialize, Deserialize, Clone)] 127 | pub struct SqlUser { 128 | pub user_id: i32, 129 | pub anonymous: bool, 130 | pub username: String, 131 | } 132 | 133 | impl SqlUser { 134 | pub fn into_user(self, sql_user_perms: Option>) -> User { 135 | User { 136 | user_id: self.user_id, 137 | anonymous: self.anonymous, 138 | username: self.username, 139 | permissions: if let Some(user_perms) = sql_user_perms { 140 | user_perms 141 | .into_iter() 142 | .map(|x| x.token) 143 | .collect::>() 144 | } else { 145 | HashSet::::new() 146 | }, 147 | } 148 | } 149 | } 150 | 151 | #[tokio::main] 152 | async fn main() { 153 | let db = connect("ws://localhost:8080").await.unwrap(); 154 | 155 | // sign in as our account. 156 | db.signin(Root { 157 | username: "root", 158 | password: "root", 159 | }) 160 | .await 161 | .unwrap(); 162 | 163 | // Set the database and namespace we will function within. 164 | db.use_ns("test").use_db("test").await.unwrap(); 165 | 166 | //This Defaults as normal Cookies. 167 | //To enable Private cookies for integrity, and authenticity please check the next Example. 168 | let session_config = SessionConfig::default().with_table_name("test_table"); 169 | let auth_config = AuthConfig::::default().with_anonymous_user_id(Some(1)); 170 | 171 | // create SessionStore and initiate the database tables 172 | let session_store: SessionStore> = 173 | SessionStore::new(Some(db.clone().into()), session_config) 174 | .await 175 | .unwrap(); 176 | 177 | User::create_user_tables(&db).await; 178 | 179 | // build our application with some routes 180 | let app = Router::new() 181 | .route("/", get(greet)) 182 | .route("/greet", get(greet)) 183 | .route("/login", get(login)) 184 | .route("/perm", get(perm)) 185 | .layer( 186 | AuthSessionLayer::, Surreal>::new(Some(db)) 187 | .with_config(auth_config), 188 | ) 189 | .layer(SessionLayer::new(session_store)); 190 | 191 | // run it 192 | let listener = TcpListener::bind("0.0.0.0:3000").await.unwrap(); 193 | axum::serve(listener, app).await.unwrap(); 194 | } 195 | 196 | async fn greet(auth: AuthSession, Surreal>) -> String { 197 | format!( 198 | "Hello {}, Try logging in via /login or testing permissions via /perm", 199 | auth.current_user.unwrap().username 200 | ) 201 | } 202 | 203 | async fn login(auth: AuthSession, Surreal>) -> String { 204 | auth.login_user(2); 205 | "You are logged in as a User please try /perm to check permissions".to_owned() 206 | } 207 | 208 | async fn perm( 209 | method: Method, 210 | auth: AuthSession, Surreal>, 211 | ) -> String { 212 | let current_user = auth.current_user.clone().unwrap_or_default(); 213 | 214 | //lets check permissions only and not worry about if they are anon or not 215 | if !Auth::>::build([Method::GET], false) 216 | .requires(Rights::any([ 217 | Rights::permission("Category::View"), 218 | Rights::permission("Admin::View"), 219 | ])) 220 | .validate(¤t_user, &method, None) 221 | .await 222 | { 223 | return format!( 224 | "User {}, Does not have permissions needed to view this page please login", 225 | current_user.username 226 | ); 227 | } 228 | 229 | format!( 230 | "User has Permissions needed. Here are the Users permissions: {:?}", 231 | current_user.permissions 232 | ) 233 | } 234 | -------------------------------------------------------------------------------- /src/auth.rs: -------------------------------------------------------------------------------- 1 | use crate::Authentication; 2 | use async_recursion::async_recursion; 3 | use async_trait::async_trait; 4 | use http::Method; 5 | use serde::{de::DeserializeOwned, Serialize}; 6 | use std::{fmt, hash::Hash, marker::PhantomData}; 7 | 8 | /// Trait is used to check their Permissions via Tokens. 9 | /// 10 | /// Uses a optional Database for SQL Token Checks too. 11 | /// 12 | #[async_trait] 13 | pub trait HasPermission 14 | where 15 | Pool: Clone + Send + Sync + fmt::Debug + 'static, 16 | { 17 | async fn has(&self, perm: &str, pool: &Option<&Pool>) -> bool; 18 | } 19 | 20 | /// Rights enumeration used for building Permissions checks against has() . 21 | /// 22 | #[derive(Clone, Default)] 23 | pub enum Rights { 24 | /// All Rights must Exist 25 | All(Box<[Rights]>), 26 | /// Only one Right needs to Exist 27 | Any(Box<[Rights]>), 28 | /// Can not contain Any of these Rights 29 | NoneOf(Box<[Rights]>), 30 | /// Token to Check for. Recrusivly stores within other Rights. 31 | Permission(String), 32 | #[default] 33 | None, 34 | } 35 | 36 | impl Rights { 37 | /// Shortcut Implementation to add Rights check for Rights::All. 38 | /// 39 | pub fn all(rights: impl IntoIterator) -> Rights { 40 | Rights::All(rights.into_iter().collect()) 41 | } 42 | 43 | /// Shortcut Implementation to add Rights check for Rights::Any. 44 | /// 45 | pub fn any(rights: impl IntoIterator) -> Rights { 46 | Rights::Any(rights.into_iter().collect()) 47 | } 48 | 49 | /// Shortcut Implementation to add Rights check for Rights::NoneOf. 50 | /// 51 | pub fn none(rights: impl IntoIterator) -> Rights { 52 | Rights::NoneOf(rights.into_iter().collect()) 53 | } 54 | 55 | /// Shortcut Implementation to add Permission for Rights::Permission. 56 | /// 57 | pub fn permission(permission: impl Into) -> Rights { 58 | Rights::Permission(permission.into()) 59 | } 60 | 61 | /// Evaluates all Rights based on the Rights enumeration patterns. 62 | /// 63 | #[async_recursion()] 64 | pub async fn evaluate( 65 | &self, 66 | user: &(dyn HasPermission + Sync), 67 | db: &Option<&Pool>, 68 | ) -> bool 69 | where 70 | Pool: Clone + Send + Sync + fmt::Debug + 'static, 71 | { 72 | match self { 73 | Self::All(rights) => { 74 | let mut all = true; 75 | for r in rights.iter() { 76 | if !r.evaluate(user, db).await { 77 | all = false; 78 | break; 79 | } 80 | } 81 | 82 | all 83 | } 84 | Self::Any(rights) => { 85 | let mut all = false; 86 | for r in rights.iter() { 87 | if r.evaluate(user, db).await { 88 | all = true; 89 | break; 90 | } 91 | } 92 | 93 | all 94 | } 95 | Self::NoneOf(rights) => { 96 | let mut all = true; 97 | for r in rights.iter() { 98 | if r.evaluate(user, db).await { 99 | all = false; 100 | break; 101 | } 102 | } 103 | 104 | all 105 | } 106 | Self::Permission(perm) => user.has(perm, db).await, 107 | Self::None => false, 108 | } 109 | } 110 | } 111 | 112 | /// Authentication Structure. 113 | /// 114 | /// All Rights, Methods and Authenticated Checks go thru this. 115 | /// 116 | /// # Examples 117 | /// ```rust no_run 118 | /// if !Auth::::build([Method::POST], true) 119 | /// .requires(Rights::all([ 120 | /// Rights::permission("admin:view"), 121 | /// Rights::permission("form:editreports"), 122 | /// ])) 123 | /// .validate(¤t_user, &state.method, None) 124 | /// .await 125 | /// { 126 | /// return handler_404(state).await.into_response(); 127 | /// } 128 | /// ``` 129 | /// 130 | pub struct Auth 131 | where 132 | User: Authentication + HasPermission + Send, 133 | Pool: Clone + Send + Sync + fmt::Debug + 'static, 134 | Type: Eq + Default + Clone + Send + Sync + Hash + Serialize + DeserializeOwned + 'static, 135 | { 136 | pub rights: Rights, 137 | pub auth_required: bool, 138 | pub methods: Vec, 139 | phantom_user: PhantomData, 140 | phantom_pool: PhantomData, 141 | phantom_type: PhantomData, 142 | } 143 | 144 | impl Auth 145 | where 146 | User: Authentication + HasPermission + Sync + Send, 147 | Pool: Clone + Send + Sync + fmt::Debug + 'static, 148 | Type: Eq + Default + Clone + Send + Sync + Hash + Serialize + DeserializeOwned + 'static, 149 | { 150 | /// Authentication Structure Builder. 151 | /// 152 | /// # Examples 153 | /// ```rust no_run 154 | /// if !Auth::::build([Method::POST], true) 155 | /// .requires(Rights::all([ 156 | /// Rights::permission("admin:view"), 157 | /// Rights::permission("form:editreports"), 158 | /// ])) 159 | /// .validate(¤t_user, &state.method, None) 160 | /// .await 161 | /// { 162 | /// return handler_404(state).await.into_response(); 163 | /// } 164 | /// ``` 165 | /// 166 | pub fn build( 167 | methods: impl IntoIterator, 168 | auth_req: bool, 169 | ) -> Auth { 170 | Auth:: { 171 | rights: Rights::None, 172 | auth_required: auth_req, 173 | methods: methods.into_iter().collect(), 174 | phantom_user: Default::default(), 175 | phantom_pool: Default::default(), 176 | phantom_type: Default::default(), 177 | } 178 | } 179 | 180 | /// Adds Rights Requirements for Lookup. 181 | /// 182 | /// # Examples 183 | /// ```rust no_run 184 | /// if !Auth::::build([Method::POST], true) 185 | /// .requires(Rights::all([ 186 | /// Rights::permission("admin:view"), 187 | /// Rights::permission("form:editreports"), 188 | /// ])) 189 | /// .validate(¤t_user, &state.method, None) 190 | /// .await 191 | /// { 192 | /// return handler_404(state).await.into_response(); 193 | /// } 194 | /// ``` 195 | /// 196 | pub fn requires(&mut self, rights: Rights) -> &mut Self { 197 | self.rights = rights; 198 | self 199 | } 200 | 201 | /// Validates if the Methods MAtch, Rights Exist or do not and If the user is Authenticated. 202 | /// 203 | /// Contains an Optional axum_session_database Pool for User auto loading. 204 | /// 205 | /// # Examples 206 | /// ```rust no_run 207 | /// if !Auth::::build([Method::POST], true) 208 | /// .requires(Rights::all([ 209 | /// Rights::permission("admin:view"), 210 | /// Rights::permission("form:editreports"), 211 | /// ])) 212 | /// .validate(¤t_user, &state.method, None) 213 | /// .await 214 | /// { 215 | /// return handler_404(state).await.into_response(); 216 | /// } 217 | /// ``` 218 | /// 219 | pub async fn validate(&self, user: &User, method: &Method, db: Option<&Pool>) -> bool 220 | where 221 | User: HasPermission + Authentication, 222 | { 223 | if self.auth_required && !user.is_authenticated() { 224 | return false; 225 | } 226 | 227 | if self.methods.iter().any(|r| r == method) { 228 | self.rights.evaluate(user, &db).await 229 | } else { 230 | false 231 | } 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /src/cache.rs: -------------------------------------------------------------------------------- 1 | use crate::{AuthUser, Authentication}; 2 | use chrono::{DateTime, Utc}; 3 | use dashmap::DashMap; 4 | use serde::{de::DeserializeOwned, Serialize}; 5 | use std::{fmt, hash::Hash, marker::PhantomData, sync::Arc}; 6 | use tokio::sync::RwLock; 7 | 8 | #[derive(Clone)] 9 | pub struct AuthCache 10 | where 11 | User: Authentication + Send, 12 | Pool: Clone + Send + Sync + fmt::Debug + 'static, 13 | Type: Eq + Default + Clone + Send + Sync + Hash + Serialize + DeserializeOwned + 'static, 14 | { 15 | pub(crate) last_expiry_sweep: Arc>>, 16 | pub(crate) inner: Arc>>, 17 | pub phantom: PhantomData, 18 | } 19 | 20 | impl std::fmt::Debug for AuthCache 21 | where 22 | User: Authentication + Send, 23 | Pool: Clone + Send + Sync + fmt::Debug + 'static, 24 | Type: Eq + Default + Clone + Send + Sync + Hash + Serialize + DeserializeOwned + 'static, 25 | { 26 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 27 | f.debug_struct("AuthCache") 28 | .field("last_expiry_sweep", &self.last_expiry_sweep) 29 | .finish() 30 | } 31 | } 32 | 33 | impl AuthCache 34 | where 35 | User: Authentication + Clone + Send, 36 | Pool: Clone + Send + Sync + fmt::Debug + 'static, 37 | Type: Eq + Default + Clone + Send + Sync + Hash + Serialize + DeserializeOwned + 'static, 38 | { 39 | pub fn new(last_expiry_sweep: DateTime) -> Self { 40 | Self { 41 | last_expiry_sweep: Arc::new(RwLock::new(last_expiry_sweep)), 42 | inner: Arc::new(DashMap::default()), 43 | phantom: Default::default(), 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use chrono::Duration; 2 | use serde::{de::DeserializeOwned, Serialize}; 3 | use std::borrow::Cow; 4 | use std::hash::Hash; 5 | 6 | /// Configuration for how the Auth service is used. 7 | /// 8 | /// # Examples 9 | /// ```rust 10 | /// use axum_session_auth::AuthConfig; 11 | /// 12 | /// let config = AuthConfig::::default(); 13 | /// ``` 14 | /// 15 | #[derive(Clone)] 16 | pub struct AuthConfig 17 | where 18 | Type: Eq + Default + Clone + Send + Sync + Hash + Serialize + DeserializeOwned + 'static, 19 | { 20 | /// Allows caching of Users, Must tell the database to reload user when a change is made when cached. 21 | pub(crate) cache: bool, 22 | /// The anonymous user id for logging unlogged users into a default guest like account. ID 0 is None 23 | pub(crate) anonymous_user_id: Option, 24 | /// Session Id for the User ID storage. 25 | pub(crate) session_id: Cow<'static, str>, 26 | /// Age the cache is allowed to live for if no visits are made. 27 | pub(crate) max_age: Duration, 28 | } 29 | 30 | impl std::fmt::Debug for AuthConfig 31 | where 32 | Type: Eq + Default + Clone + Send + Sync + Hash + Serialize + DeserializeOwned + 'static, 33 | { 34 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 35 | f.debug_struct("AuthConfig") 36 | .field("cache", &self.cache) 37 | .field("session_id", &self.session_id) 38 | .field("max_age", &self.max_age) 39 | .finish() 40 | } 41 | } 42 | 43 | impl AuthConfig 44 | where 45 | Type: Eq + Default + Clone + Send + Sync + Hash + Serialize + DeserializeOwned + 'static, 46 | { 47 | /// Creates [`Default`] configuration of [`AuthConfig`]. 48 | /// This is equivalent to the [`AuthConfig::::default()`]. 49 | #[inline] 50 | pub fn new() -> Self { 51 | Default::default() 52 | } 53 | /// Sets the auth session to cache the users data for faster reload. 54 | /// if set to true and you update a users Data you must also trigger 55 | /// the reload of the user to reload the new changes on their next request. 56 | /// 57 | /// # Examples 58 | /// ```rust 59 | /// use axum_session_auth::AuthConfig; 60 | /// 61 | /// let config = AuthConfig::::default().set_cache(true); 62 | /// ``` 63 | /// 64 | #[must_use] 65 | pub fn set_cache(mut self, cache: bool) -> Self { 66 | self.cache = cache; 67 | self 68 | } 69 | 70 | /// Set the auto logged in user ID if the user making the request is not logged in yet. 71 | /// Do not use ID 0 as this stands for the default Return when no user is loaded. 72 | /// 73 | /// # Examples 74 | /// ```rust 75 | /// use axum_session_auth::AuthConfig; 76 | /// 77 | /// let config = AuthConfig::::default().with_anonymous_user_id(Some(0)); 78 | /// ``` 79 | /// 80 | #[must_use] 81 | pub fn with_anonymous_user_id(mut self, id: Option) -> Self { 82 | self.anonymous_user_id = id; 83 | self 84 | } 85 | 86 | /// Set's the auth session's max_age (expiration time). 87 | /// 88 | /// This is used to deturmine how long a User Account is cached per last request 89 | /// 90 | /// # Examples 91 | /// ```rust 92 | /// use axum_session_auth::AuthConfig; 93 | /// use chrono::Duration; 94 | /// 95 | /// let config = AuthConfig::::default().with_max_age(Some(Duration::days(2))); 96 | /// ``` 97 | /// 98 | #[must_use] 99 | pub fn with_max_age(mut self, time: Duration) -> Self { 100 | self.max_age = time; 101 | self 102 | } 103 | 104 | /// Set's the auth session's token for session storage. 105 | /// 106 | /// # Examples 107 | /// ```rust 108 | /// use axum_session_auth::AuthConfig; 109 | /// 110 | /// let config = AuthConfig::::default().with_session_id("www.helpme.com".to_string()); 111 | /// ``` 112 | /// 113 | #[must_use] 114 | pub fn with_session_id(mut self, session_id: impl Into>) -> Self { 115 | self.session_id = session_id.into(); 116 | self 117 | } 118 | } 119 | 120 | impl Default for AuthConfig 121 | where 122 | Type: Eq + Default + Clone + Send + Sync + Hash + Serialize + DeserializeOwned + 'static, 123 | { 124 | fn default() -> Self { 125 | Self { 126 | // Set to a 6 hour default in Database Session stores unloading. 127 | cache: true, 128 | session_id: "user_auth_session_id".into(), 129 | max_age: Duration::try_hours(6).unwrap_or_default(), 130 | anonymous_user_id: None, 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/layer.rs: -------------------------------------------------------------------------------- 1 | use crate::{AuthCache, AuthConfig, AuthSessionService, Authentication}; 2 | use axum_session::DatabasePool; 3 | use chrono::{Duration, Utc}; 4 | use serde::{de::DeserializeOwned, Serialize}; 5 | use std::{fmt, hash::Hash, marker::PhantomData}; 6 | use tower_layer::Layer; 7 | 8 | /// Layer used to generate an AuthSessionService. 9 | /// 10 | #[derive(Clone, Debug)] 11 | pub struct AuthSessionLayer 12 | where 13 | User: Authentication + Clone + Send, 14 | Pool: Clone + Send + Sync + fmt::Debug + 'static, 15 | Type: Eq + Default + Clone + Send + Sync + Hash + Serialize + DeserializeOwned + 'static, 16 | { 17 | pub(crate) pool: Option, 18 | pub(crate) config: AuthConfig, 19 | pub(crate) cache: AuthCache, 20 | pub phantom_user: PhantomData, 21 | pub phantom_session: PhantomData, 22 | pub phantom_type: PhantomData, 23 | } 24 | 25 | impl AuthSessionLayer 26 | where 27 | User: Authentication + Clone + Send, 28 | Pool: Clone + Send + Sync + fmt::Debug + 'static, 29 | Type: Eq + Default + Clone + Send + Sync + Hash + Serialize + DeserializeOwned + 'static, 30 | Sess: DatabasePool + Clone + Sync + Send + 'static, 31 | { 32 | /// Used to generate an AuthSessionLayer with will call Towers layer() to generate a AuthSessionService. 33 | /// 34 | /// contains an Optional axum_session_database Pool for Sqlx database lookups against Right tokens. 35 | /// 36 | /// # Examples 37 | /// ```rust no_run 38 | /// let layer = AuthSessionLayer::::new(None); 39 | /// ``` 40 | /// 41 | pub fn new(pool: Option) -> Self { 42 | Self { 43 | pool, 44 | config: AuthConfig::default(), 45 | cache: AuthCache::::new( 46 | Utc::now() + Duration::try_hours(1).unwrap_or_default(), 47 | ), 48 | phantom_user: PhantomData, 49 | phantom_session: PhantomData, 50 | phantom_type: PhantomData, 51 | } 52 | } 53 | 54 | #[must_use] 55 | pub fn with_config(mut self, config: AuthConfig) -> Self { 56 | self.config = config; 57 | self 58 | } 59 | } 60 | 61 | impl Layer for AuthSessionLayer 62 | where 63 | User: Authentication + Clone + Send, 64 | Pool: Clone + Send + Sync + fmt::Debug + 'static, 65 | Type: Eq + Default + Clone + Send + Sync + Hash + Serialize + DeserializeOwned + 'static, 66 | Sess: DatabasePool + Clone + fmt::Debug + Sync + Send + 'static, 67 | { 68 | type Service = AuthSessionService; 69 | 70 | fn layer(&self, inner: S) -> Self::Service { 71 | AuthSessionService { 72 | pool: self.pool.clone(), 73 | config: self.config.clone(), 74 | cache: self.cache.clone(), 75 | inner, 76 | phantom_session: PhantomData, 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | #![forbid(unsafe_code)] 3 | ///This Library Requires that DatabaseSessions is used as an active layer. 4 | /// 5 | mod auth; 6 | mod cache; 7 | mod config; 8 | mod layer; 9 | mod service; 10 | mod session; 11 | mod user; 12 | 13 | pub use auth::{Auth, HasPermission, Rights}; 14 | pub use cache::AuthCache; 15 | pub use config::AuthConfig; 16 | pub use layer::AuthSessionLayer; 17 | pub use service::AuthSessionService; 18 | pub use session::{AuthSession, Authentication}; 19 | 20 | #[cfg(feature = "advanced")] 21 | pub use session::AuthStatus; 22 | 23 | pub(crate) use user::AuthUser; 24 | 25 | pub use axum_session::databases::*; 26 | -------------------------------------------------------------------------------- /src/service.rs: -------------------------------------------------------------------------------- 1 | use crate::{AuthCache, AuthConfig, AuthSession, AuthUser, Authentication}; 2 | use axum_core::BoxError; 3 | use axum_session::{DatabasePool, Session}; 4 | use bytes::Bytes; 5 | use chrono::Utc; 6 | use futures::future::BoxFuture; 7 | use http::{Request, Response}; 8 | use http_body::Body as HttpBody; 9 | use serde::{de::DeserializeOwned, Serialize}; 10 | use std::{ 11 | convert::Infallible, 12 | fmt, 13 | hash::Hash, 14 | marker::PhantomData, 15 | task::{Context, Poll}, 16 | }; 17 | use tower_service::Service; 18 | 19 | #[derive(Clone)] 20 | pub struct AuthSessionService 21 | where 22 | User: Authentication + Send, 23 | Pool: Clone + Send + Sync + fmt::Debug + 'static, 24 | Type: Eq + Default + Clone + Send + Sync + Hash + Serialize + DeserializeOwned + 'static, 25 | Sess: DatabasePool + Clone + fmt::Debug + Sync + Send + 'static, 26 | { 27 | pub(crate) pool: Option, 28 | pub(crate) config: AuthConfig, 29 | pub(crate) cache: AuthCache, 30 | pub(crate) inner: S, 31 | pub phantom_session: PhantomData, 32 | } 33 | 34 | impl Service> 35 | for AuthSessionService 36 | where 37 | Pool: Clone + Send + Sync + fmt::Debug + 'static, 38 | Type: Eq 39 | + Default 40 | + Clone 41 | + Send 42 | + Sync 43 | + Hash 44 | + Serialize 45 | + DeserializeOwned 46 | + std::fmt::Display 47 | + 'static, 48 | Sess: DatabasePool + Clone + fmt::Debug + Sync + Send + 'static, 49 | User: Authentication + Clone + Send + Sync + 'static, 50 | S: Service, Response = Response, Error = Infallible> 51 | + Clone 52 | + Send 53 | + 'static, 54 | S::Future: Send + 'static, 55 | ReqBody: Send + 'static, 56 | Infallible: From<>>::Error>, 57 | ResBody: HttpBody + Default + Send + 'static, 58 | ResBody::Error: Into, 59 | { 60 | type Response = Response; 61 | type Error = Infallible; 62 | type Future = BoxFuture<'static, Result>; 63 | 64 | fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { 65 | self.inner.poll_ready(cx) 66 | } 67 | 68 | fn call(&mut self, mut req: Request) -> Self::Future { 69 | let pool = self.pool.clone(); 70 | let config = self.config.clone(); 71 | let cache = self.cache.clone(); 72 | let not_ready_inner = self.inner.clone(); 73 | let mut ready_inner = std::mem::replace(&mut self.inner, not_ready_inner); 74 | 75 | Box::pin(async move { 76 | let axum_session = match req.extensions().get::>().cloned() { 77 | Some(session) => session, 78 | None => { 79 | tracing::error!("axum session extension is not loaded."); 80 | let mut res = Response::default(); 81 | *res.status_mut() = http::StatusCode::INTERNAL_SERVER_ERROR; 82 | return Ok(res); 83 | } 84 | }; 85 | 86 | let id = axum_session 87 | .get::(&config.session_id) 88 | .map_or(config.anonymous_user_id.clone(), Some) 89 | .unwrap_or_else(|| Type::default()); 90 | 91 | let current_user = if id != Type::default() { 92 | if config.cache { 93 | if let Some(mut user) = cache.inner.get_mut(&id) { 94 | tracing::debug!("user id: {} found in cache", id); 95 | user.expires = Utc::now() + config.max_age; 96 | user.current_user.clone() 97 | } else { 98 | tracing::debug!("loading user id: {} from load_user", id); 99 | let current_user = User::load_user(id.clone(), pool.as_ref()).await.ok(); 100 | let user = AuthUser:: { 101 | current_user: current_user.clone(), 102 | expires: Utc::now() + config.max_age, 103 | phantom_pool: Default::default(), 104 | phantom_type: Default::default(), 105 | }; 106 | 107 | cache.inner.insert(id.clone(), user); 108 | current_user 109 | } 110 | } else { 111 | User::load_user(id.clone(), pool.as_ref()).await.ok() 112 | } 113 | } else { 114 | None 115 | }; 116 | 117 | // Lets clean up the cache now that we did all our user stuff. 118 | if config.cache { 119 | let last_sweep = { *cache.last_expiry_sweep.read().await }; 120 | 121 | if last_sweep <= Utc::now() { 122 | tracing::info!("clearing old users from user cache."); 123 | cache.inner.retain(|_k, v| v.expires > Utc::now()); 124 | *cache.last_expiry_sweep.write().await = Utc::now() + config.max_age; 125 | } 126 | } 127 | 128 | let session = AuthSession { 129 | id, 130 | current_user, 131 | cache, 132 | session: axum_session, 133 | pool, 134 | config, 135 | }; 136 | 137 | // Sets a clone of the Store in the Extensions for Direct usage and sets the Session for Direct usage 138 | req.extensions_mut().insert(session); 139 | ready_inner.call(req).await 140 | }) 141 | } 142 | } 143 | 144 | impl fmt::Debug for AuthSessionService 145 | where 146 | S: fmt::Debug, 147 | User: Authentication + fmt::Debug + Clone + Send, 148 | Pool: Clone + Send + Sync + fmt::Debug + 'static, 149 | Type: Eq + Default + Clone + Send + Sync + Hash + Serialize + DeserializeOwned + 'static, 150 | Sess: DatabasePool + Clone + fmt::Debug + Sync + Send + 'static, 151 | { 152 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 153 | f.debug_struct("AuthSessionService") 154 | .field("pool", &self.pool) 155 | .field("config", &self.config) 156 | .field("inner", &self.inner) 157 | .finish() 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/session.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "advanced")] 2 | use crate::AuthUser; 3 | use crate::{AuthCache, AuthConfig}; 4 | use anyhow::Error; 5 | use async_trait::async_trait; 6 | use axum_core::extract::FromRequestParts; 7 | use axum_session::{DatabasePool, Session}; 8 | #[cfg(feature = "advanced")] 9 | use chrono::Utc; 10 | use http::{request::Parts, StatusCode}; 11 | use serde::{de::DeserializeOwned, Serialize}; 12 | use std::{fmt, hash::Hash}; 13 | 14 | /// AuthSession that is generated when a user is routed via Axum 15 | /// 16 | /// Contains the loaded user data, ID and an Session. 17 | /// 18 | #[derive(Debug, Clone)] 19 | pub struct AuthSession 20 | where 21 | User: Authentication + Send, 22 | Pool: Clone + Send + Sync + fmt::Debug + 'static, 23 | Type: Eq + Default + Clone + Send + Sync + Hash + Serialize + DeserializeOwned + 'static, 24 | Sess: DatabasePool + Clone + fmt::Debug + Sync + Send + 'static, 25 | { 26 | pub id: Type, 27 | pub current_user: Option, 28 | pub session: Session, 29 | pub(crate) cache: AuthCache, 30 | #[allow(dead_code)] 31 | pub(crate) pool: Option, 32 | #[allow(dead_code)] 33 | pub(crate) config: AuthConfig, 34 | } 35 | 36 | #[async_trait] 37 | pub trait Authentication 38 | where 39 | User: Send, 40 | Pool: Clone + Send + Sync + fmt::Debug + 'static, 41 | Type: Eq + Default + Clone + Send + Sync + Hash + Serialize + DeserializeOwned + 'static, 42 | { 43 | async fn load_user(userid: Type, pool: Option<&Pool>) -> Result; 44 | fn is_authenticated(&self) -> bool; 45 | fn is_active(&self) -> bool; 46 | fn is_anonymous(&self) -> bool; 47 | } 48 | 49 | impl FromRequestParts for AuthSession 50 | where 51 | User: Authentication + Clone + Send + Sync + 'static, 52 | Pool: Clone + Send + Sync + fmt::Debug + 'static, 53 | Type: Eq + Default + Clone + Send + Sync + Hash + Serialize + DeserializeOwned + 'static, 54 | Sess: DatabasePool + Clone + fmt::Debug + Sync + Send + 'static, 55 | S: Send + Sync, 56 | { 57 | type Rejection = (http::StatusCode, &'static str); 58 | 59 | async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { 60 | parts 61 | .extensions 62 | .get::>() 63 | .cloned() 64 | .ok_or(( 65 | StatusCode::INTERNAL_SERVER_ERROR, 66 | "Can't extract AuthSession. Is `AuthSessionLayer` enabled?", 67 | )) 68 | } 69 | } 70 | 71 | impl AuthSession 72 | where 73 | User: Authentication + Clone + Send, 74 | Pool: Clone + Send + Sync + fmt::Debug + 'static, 75 | Type: Eq + Default + Clone + Send + Sync + Hash + Serialize + DeserializeOwned + 'static, 76 | Sess: DatabasePool + Clone + fmt::Debug + Sync + Send + 'static, 77 | { 78 | /// Checks if the user is Authenticated 79 | /// 80 | /// # Examples 81 | /// ```rust no_run 82 | /// auth.is_authenticated(); 83 | /// ``` 84 | /// 85 | pub fn is_authenticated(&self) -> bool { 86 | match &self.current_user { 87 | Some(n) => n.is_authenticated(), 88 | None => false, 89 | } 90 | } 91 | 92 | /// Checks if the user is Active 93 | /// 94 | /// # Examples 95 | /// ```rust no_run 96 | /// auth.is_active(); 97 | /// ``` 98 | /// 99 | pub fn is_active(&self) -> bool { 100 | match &self.current_user { 101 | Some(n) => n.is_active(), 102 | None => false, 103 | } 104 | } 105 | 106 | /// Checks if the user is Anonymous 107 | /// 108 | /// # Examples 109 | /// ```rust no_run 110 | /// auth.is_anonymous(); 111 | /// ``` 112 | /// 113 | pub fn is_anonymous(&self) -> bool { 114 | match &self.current_user { 115 | Some(n) => n.is_anonymous(), 116 | None => true, 117 | } 118 | } 119 | 120 | /// Sets the Session Data to be saved for Long Term 121 | /// 122 | /// # Examples 123 | /// ```rust no_run 124 | /// auth.remember_user(true); 125 | /// ``` 126 | /// 127 | pub fn remember_user(&self, remember_me: bool) { 128 | self.session.set_longterm(remember_me); 129 | } 130 | 131 | /// Sets the user id into the Session so it can auto login the user upon Axum request. 132 | /// 133 | /// # Examples 134 | /// ```rust no_run 135 | /// auth.login_user(user.id); 136 | /// ``` 137 | /// 138 | pub fn login_user(&self, id: Type) { 139 | self.session.set(&self.config.session_id, id); 140 | self.session.renew(); 141 | } 142 | 143 | /// Tells the system to clear the user so they get reloaded upon next Axum request. 144 | /// 145 | /// # Examples 146 | /// ```rust no_run 147 | /// auth.cache_clear_user(user.id); 148 | /// ``` 149 | /// 150 | pub fn cache_clear_user(&self, id: Type) { 151 | let _ = self.cache.inner.remove(&id); 152 | } 153 | 154 | /// Emptys the cache to force reload of all users. 155 | /// 156 | /// # Examples 157 | /// ```rust no_run 158 | /// auth.cache_clear_all(); 159 | /// ``` 160 | /// 161 | pub fn cache_clear_all(&self) { 162 | self.cache.inner.clear(); 163 | } 164 | 165 | /// Removes the user id from the Session preventing the system from auto login unless guest id is set. 166 | /// 167 | /// # Examples 168 | /// ```rust no_run 169 | /// auth.logout_user(); 170 | /// ``` 171 | /// 172 | pub fn logout_user(&self) { 173 | self.session.remove(&self.config.session_id); 174 | self.session.renew(); 175 | } 176 | 177 | /// Used to check if a long living AuthSession is still logged in, 178 | /// if the user logged out or if the user switched account id's during 179 | /// the last request the AuthSession was created from. This does not check 180 | /// if the session itself is not the same or reloaded. 181 | /// 182 | /// # Examples 183 | /// ```rust no_run 184 | /// auth.is_logged_in(); 185 | /// ``` 186 | /// 187 | #[cfg(feature = "advanced")] 188 | pub fn is_logged_in(&mut self) -> AuthStatus { 189 | if let Some(id) = self.session.get::(&self.config.session_id) { 190 | if id == self.id { 191 | if !self.config.cache || self.cache.inner.contains_key(&self.id) { 192 | AuthStatus::LoggedIn 193 | } else { 194 | AuthStatus::StaleUser 195 | } 196 | } else { 197 | AuthStatus::DifferentID 198 | } 199 | } else { 200 | AuthStatus::LoggedOut 201 | } 202 | } 203 | 204 | /// Reloads the user data into current user and cache. 205 | /// 206 | /// # Examples 207 | /// ```rust no_run 208 | /// auth.reload_user().await; 209 | /// ``` 210 | /// 211 | #[cfg(feature = "advanced")] 212 | pub async fn reload_user(&mut self) { 213 | let current_user = User::load_user(self.id.clone(), self.pool.as_ref()) 214 | .await 215 | .ok(); 216 | 217 | if self.config.cache { 218 | let user = if let Some((_id, mut user)) = self.cache.inner.remove(&self.id) { 219 | user.expires = Utc::now() + self.config.max_age; 220 | user.current_user = current_user.clone(); 221 | user 222 | } else { 223 | AuthUser:: { 224 | current_user: current_user.clone(), 225 | expires: Utc::now() + self.config.max_age, 226 | phantom_pool: Default::default(), 227 | phantom_type: Default::default(), 228 | } 229 | }; 230 | 231 | self.cache.inner.insert(self.id.clone(), user); 232 | } 233 | 234 | self.current_user = current_user; 235 | } 236 | 237 | /// Updates the users expiration time so a request will not 238 | /// remove them from the cache. 239 | /// 240 | /// THIS WILL NOT RELOAD THE USERS DATA 241 | /// 242 | /// # Examples 243 | /// ```rust no_run 244 | /// auth.update_user_expiration(); 245 | /// ``` 246 | /// 247 | #[cfg(feature = "advanced")] 248 | pub fn update_user_expiration(&mut self) { 249 | if self.config.cache { 250 | if let Some(mut user) = self.cache.inner.get_mut(&self.id) { 251 | user.expires = Utc::now() + self.config.max_age; 252 | } 253 | } 254 | } 255 | 256 | /// Updates the users id to what is currently in the session. 257 | /// if the session doesnt exist or the id was removed it does nothing. 258 | /// 259 | /// THIS WILL NOT RELOAD THE USERS DATA 260 | /// 261 | /// # Examples 262 | /// ```rust no_run 263 | /// auth.sync_user_id(); 264 | /// ``` 265 | /// 266 | #[cfg(feature = "advanced")] 267 | pub fn sync_user_id(&mut self) { 268 | if let Some(id) = self.session.get::(&self.config.session_id) { 269 | self.id = id.clone(); 270 | } 271 | } 272 | } 273 | 274 | /// Used to display how the users Auth data is compared to what 275 | /// a AuthSessions Data was set as. To ensure nothing changed. 276 | /// 277 | /// # Examples 278 | /// ```rust no_run 279 | /// auth.is_logged_in(); 280 | /// ``` 281 | /// 282 | #[cfg(feature = "advanced")] 283 | pub enum AuthStatus { 284 | /// If the user id did not change and is logged in 285 | LoggedIn, 286 | /// If the users id did not match or got changed internally 287 | /// by another request. 288 | DifferentID, 289 | /// the user is logged out. 290 | LoggedOut, 291 | /// The user account was removed from cache 292 | /// so it needs reloading. 293 | StaleUser, 294 | } 295 | -------------------------------------------------------------------------------- /src/user.rs: -------------------------------------------------------------------------------- 1 | use crate::Authentication; 2 | use chrono::{DateTime, Utc}; 3 | use serde::{de::DeserializeOwned, Serialize}; 4 | use std::{fmt, hash::Hash, marker::PhantomData}; 5 | 6 | /// AuthSession that is generated when a user is routed via Axum 7 | /// 8 | /// Contains the loaded user data, ID and an AxumSession. 9 | /// 10 | #[derive(Debug, Clone)] 11 | pub(crate) struct AuthUser 12 | where 13 | User: Authentication + Send, 14 | Type: Eq + Default + Clone + Send + Sync + Hash + Serialize + DeserializeOwned + 'static, 15 | Pool: Clone + Send + Sync + fmt::Debug + 'static, 16 | { 17 | pub current_user: Option, 18 | pub expires: DateTime, 19 | pub phantom_pool: PhantomData, 20 | pub phantom_type: PhantomData, 21 | } 22 | --------------------------------------------------------------------------------