use async_trait::async_trait;
use std::collections::HashMap;
use std::sync::Arc;
use crate::config::AuthConfig;
use crate::email::EmailProvider;
use crate::entity::AuthSession;
use crate::error::{AuthError, AuthResult};
use crate::schema::AuthSchema;
#[cfg(test)]
use crate::session::SessionManager;
use crate::store::AuthStore;
use crate::types::{AuthRequest, AuthResponse, HttpMethod};
type MetadataMap = HashMap<String, serde_json::Value>;
pub struct AuthInitParts {
pub metadata: MetadataMap,
pub email_provider: Option<Arc<dyn EmailProvider>>,
}
#[derive(Debug)]
pub enum BeforeRequestAction {
Respond(AuthResponse),
InjectSession {
user_id: String,
session_token: String,
},
}
#[async_trait]
pub trait AuthPlugin<S: AuthSchema>: Send + Sync {
fn name(&self) -> &'static str;
fn routes(&self) -> Vec<AuthRoute>;
async fn on_init(&self, ctx: &mut AuthInitContext<S>) -> AuthResult<()> {
let _ = ctx;
Ok(())
}
async fn before_request(
&self,
_req: &AuthRequest,
_ctx: &AuthContext<S>,
) -> AuthResult<Option<BeforeRequestAction>> {
Ok(None)
}
async fn on_request(
&self,
req: &AuthRequest,
ctx: &AuthContext<S>,
) -> AuthResult<Option<AuthResponse>>;
async fn on_user_created(&self, user: &S::User, ctx: &AuthContext<S>) -> AuthResult<()> {
let _ = (user, ctx);
Ok(())
}
async fn on_session_created(
&self,
session: &S::Session,
ctx: &AuthContext<S>,
) -> AuthResult<()> {
let _ = (session, ctx);
Ok(())
}
async fn on_user_deleted(&self, user_id: &str, ctx: &AuthContext<S>) -> AuthResult<()> {
let _ = (user_id, ctx);
Ok(())
}
async fn on_session_deleted(
&self,
session_token: &str,
ctx: &AuthContext<S>,
) -> AuthResult<()> {
let _ = (session_token, ctx);
Ok(())
}
}
#[macro_export]
macro_rules! impl_auth_plugin {
(@pat get) => { $crate::HttpMethod::Get };
(@pat post) => { $crate::HttpMethod::Post };
(@pat put) => { $crate::HttpMethod::Put };
(@pat delete) => { $crate::HttpMethod::Delete };
(@pat patch) => { $crate::HttpMethod::Patch };
(@pat head) => { $crate::HttpMethod::Head };
(@route get) => { $crate::AuthRoute::get };
(@route post) => { $crate::AuthRoute::post };
(@route put) => { $crate::AuthRoute::put };
(@route delete) => { $crate::AuthRoute::delete };
(
$plugin:ty, $name:expr;
routes {
$( $method:ident $path:literal => $handler:ident, $op_id:literal );* $(;)?
}
$( extra { $($extra:tt)* } )?
) => {
#[::async_trait::async_trait]
impl<S: $crate::AuthSchema> $crate::AuthPlugin<S> for $plugin {
fn name(&self) -> &'static str { $name }
fn routes(&self) -> Vec<$crate::AuthRoute> {
vec![
$( $crate::AuthRoute::new($crate::impl_auth_plugin!(@pat $method), $path, $op_id), )*
]
}
async fn on_request(
&self,
req: &$crate::AuthRequest,
ctx: &$crate::AuthContext<S>,
) -> $crate::AuthResult<Option<$crate::AuthResponse>> {
match (req.method(), req.path()) {
$(
($crate::impl_auth_plugin!(@pat $method), $path) => {
Ok(Some(self.$handler(req, ctx).await?))
}
)*
_ => Ok(None),
}
}
$( $($extra)* )?
}
};
}
#[derive(Debug, Clone)]
pub struct AuthRoute {
pub path: String,
pub method: HttpMethod,
pub operation_id: String,
}
pub struct AuthInitContext<S: AuthSchema> {
pub config: Arc<AuthConfig>,
pub database: Arc<dyn AuthStore<S>>,
pub email_provider: Option<Arc<dyn EmailProvider>>,
pub metadata: MetadataMap,
}
pub struct AuthContext<S: AuthSchema> {
pub config: Arc<AuthConfig>,
pub database: Arc<dyn AuthStore<S>>,
pub email_provider: Option<Arc<dyn EmailProvider>>,
pub metadata: MetadataMap,
}
impl AuthRoute {
pub fn new(
method: HttpMethod,
path: impl Into<String>,
operation_id: impl Into<String>,
) -> Self {
Self {
path: path.into(),
method,
operation_id: operation_id.into(),
}
}
pub fn get(path: impl Into<String>, operation_id: impl Into<String>) -> Self {
Self::new(HttpMethod::Get, path, operation_id)
}
pub fn post(path: impl Into<String>, operation_id: impl Into<String>) -> Self {
Self::new(HttpMethod::Post, path, operation_id)
}
pub fn put(path: impl Into<String>, operation_id: impl Into<String>) -> Self {
Self::new(HttpMethod::Put, path, operation_id)
}
pub fn delete(path: impl Into<String>, operation_id: impl Into<String>) -> Self {
Self::new(HttpMethod::Delete, path, operation_id)
}
}
impl<S: AuthSchema> AuthInitContext<S> {
pub fn new(config: Arc<AuthConfig>, database: Arc<dyn AuthStore<S>>) -> Self {
let email_provider = config.email_provider.clone();
Self {
config,
database,
email_provider,
metadata: MetadataMap::new(),
}
}
pub fn set_metadata(&mut self, key: impl Into<String>, value: serde_json::Value) {
_ = self.metadata.insert(key.into(), value);
}
pub fn get_metadata(&self, key: &str) -> Option<&serde_json::Value> {
self.metadata.get(key)
}
pub fn into_parts(self) -> AuthInitParts {
AuthInitParts {
metadata: self.metadata,
email_provider: self.email_provider,
}
}
}
impl<S: AuthSchema> AuthContext<S> {
pub fn new(config: Arc<AuthConfig>, database: Arc<dyn AuthStore<S>>) -> Self {
let email_provider = config.email_provider.clone();
Self {
config,
database,
email_provider,
metadata: MetadataMap::new(),
}
}
pub fn with_metadata(
config: Arc<AuthConfig>,
database: Arc<dyn AuthStore<S>>,
metadata: MetadataMap,
) -> Self {
let email_provider = config.email_provider.clone();
Self {
config,
database,
email_provider,
metadata,
}
}
pub fn set_metadata(&mut self, key: impl Into<String>, value: serde_json::Value) {
_ = self.metadata.insert(key.into(), value);
}
pub fn get_metadata(&self, key: &str) -> Option<&serde_json::Value> {
self.metadata.get(key)
}
pub fn email_provider(&self) -> AuthResult<&dyn EmailProvider> {
self.email_provider
.as_deref()
.ok_or_else(|| AuthError::config("No email provider configured"))
}
pub fn session_manager(&self) -> crate::session::SessionManager<S> {
crate::session::SessionManager::new(self.config.clone(), self.database.clone())
}
pub async fn require_session(&self, req: &AuthRequest) -> AuthResult<(S::User, S::Session)> {
let session_manager = self.session_manager();
if let Some(token) = session_manager.extract_session_token(req)
&& let Some(session) = session_manager.get_session(&token).await?
&& let Some(user) = self.database.get_user_by_id(&session.user_id()).await?
{
return Ok((user, session));
}
Err(AuthError::Unauthenticated)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::entity::AuthUser;
use crate::test_store::test_database;
#[test]
fn auth_route_constructors() {
let get = AuthRoute::get("/test", "getTest");
assert_eq!(get.method, HttpMethod::Get);
assert_eq!(get.path, "/test");
assert_eq!(get.operation_id, "getTest");
let post = AuthRoute::post("/create", "createItem");
assert_eq!(post.method, HttpMethod::Post);
let put = AuthRoute::put("/update", "updateItem");
assert_eq!(put.method, HttpMethod::Put);
let delete = AuthRoute::delete("/remove", "deleteItem");
assert_eq!(delete.method, HttpMethod::Delete);
}
#[test]
fn auth_route_new() {
let route = AuthRoute::new(HttpMethod::Patch, "/patch", "patchIt");
assert_eq!(route.method, HttpMethod::Patch);
assert_eq!(route.path, "/patch");
}
#[test]
fn auth_context_new() {
let config = Arc::new(AuthConfig::new("test-secret-min-32-chars-1234567"));
let runtime = tokio::runtime::Runtime::new().expect("runtime should build");
let db = runtime.block_on(test_database());
let ctx = AuthContext::new(config.clone(), db);
assert!(ctx.email_provider.is_none());
assert!(ctx.metadata.is_empty());
}
#[test]
fn auth_context_metadata() {
let config = Arc::new(AuthConfig::new("test-secret-min-32-chars-1234567"));
let runtime = tokio::runtime::Runtime::new().expect("runtime should build");
let db = runtime.block_on(test_database());
let mut ctx = AuthContext::new(config, db);
ctx.set_metadata("key", serde_json::json!("value"));
assert_eq!(ctx.get_metadata("key"), Some(&serde_json::json!("value")));
assert!(ctx.get_metadata("missing").is_none());
}
#[test]
fn auth_context_email_provider_error_when_none() {
let config = Arc::new(AuthConfig::new("test-secret-min-32-chars-1234567"));
let runtime = tokio::runtime::Runtime::new().expect("runtime should build");
let db = runtime.block_on(test_database());
let ctx = AuthContext::new(config, db);
assert!(ctx.email_provider().is_err());
}
#[tokio::test]
async fn auth_context_require_session_unauthenticated() {
let config = Arc::new(AuthConfig::new("test-secret-min-32-chars-1234567"));
let db = test_database().await;
let ctx = AuthContext::new(config, db);
let req = AuthRequest::new(HttpMethod::Get, "/test");
let result = ctx.require_session(&req).await;
assert!(result.is_err());
}
#[tokio::test]
async fn auth_context_require_session_with_valid_session() {
let config = Arc::new(AuthConfig::new("test-secret-min-32-chars-1234567"));
let db = test_database().await;
let user = db
.create_user(crate::types::CreateUser::new().with_email("test@test.com"))
.await
.unwrap();
let sm = SessionManager::new(config.clone(), db.clone());
let session = sm.create_session(&user, None, None).await.unwrap();
let ctx = AuthContext::new(config.clone(), db);
let mut req = AuthRequest::new(HttpMethod::Get, "/test");
let _ = req.headers.insert(
"cookie".into(),
format!("better-auth.session_token={}", session.token()),
);
let (found_user, _found_session) = ctx.require_session(&req).await.unwrap();
assert_eq!(found_user.id(), user.id());
}
}