1use async_trait::async_trait;
2use std::collections::HashMap;
3use std::sync::Arc;
4
5use crate::config::AuthConfig;
6use crate::email::EmailProvider;
7use crate::entity::AuthSession;
8use crate::error::{AuthError, AuthResult};
9use crate::schema::AuthSchema;
10#[cfg(test)]
11use crate::session::SessionManager;
12use crate::store::AuthStore;
13use crate::types::{AuthRequest, AuthResponse, HttpMethod};
14
15type MetadataMap = HashMap<String, serde_json::Value>;
16
17pub struct AuthInitParts {
18 pub metadata: MetadataMap,
19 pub email_provider: Option<Arc<dyn EmailProvider>>,
20}
21
22#[derive(Debug)]
24pub enum BeforeRequestAction {
25 Respond(AuthResponse),
27 InjectSession {
29 user_id: String,
30 session_token: String,
31 },
32}
33
34#[async_trait]
37pub trait AuthPlugin<S: AuthSchema>: Send + Sync {
38 fn name(&self) -> &'static str;
40
41 fn routes(&self) -> Vec<AuthRoute>;
43
44 async fn on_init(&self, ctx: &mut AuthInitContext<S>) -> AuthResult<()> {
46 let _ = ctx;
47 Ok(())
48 }
49
50 async fn before_request(
57 &self,
58 _req: &AuthRequest,
59 _ctx: &AuthContext<S>,
60 ) -> AuthResult<Option<BeforeRequestAction>> {
61 Ok(None)
62 }
63
64 async fn on_request(
66 &self,
67 req: &AuthRequest,
68 ctx: &AuthContext<S>,
69 ) -> AuthResult<Option<AuthResponse>>;
70
71 async fn on_user_created(&self, user: &S::User, ctx: &AuthContext<S>) -> AuthResult<()> {
73 let _ = (user, ctx);
74 Ok(())
75 }
76
77 async fn on_session_created(
79 &self,
80 session: &S::Session,
81 ctx: &AuthContext<S>,
82 ) -> AuthResult<()> {
83 let _ = (session, ctx);
84 Ok(())
85 }
86
87 async fn on_user_deleted(&self, user_id: &str, ctx: &AuthContext<S>) -> AuthResult<()> {
89 let _ = (user_id, ctx);
90 Ok(())
91 }
92
93 async fn on_session_deleted(
95 &self,
96 session_token: &str,
97 ctx: &AuthContext<S>,
98 ) -> AuthResult<()> {
99 let _ = (session_token, ctx);
100 Ok(())
101 }
102}
103
104#[macro_export]
117macro_rules! impl_auth_plugin {
118 (@pat get) => { $crate::HttpMethod::Get };
119 (@pat post) => { $crate::HttpMethod::Post };
120 (@pat put) => { $crate::HttpMethod::Put };
121 (@pat delete) => { $crate::HttpMethod::Delete };
122 (@pat patch) => { $crate::HttpMethod::Patch };
123 (@pat head) => { $crate::HttpMethod::Head };
124
125 (@route get) => { $crate::AuthRoute::get };
126 (@route post) => { $crate::AuthRoute::post };
127 (@route put) => { $crate::AuthRoute::put };
128 (@route delete) => { $crate::AuthRoute::delete };
129
130 (
131 $plugin:ty, $name:expr;
132 routes {
133 $( $method:ident $path:literal => $handler:ident, $op_id:literal );* $(;)?
134 }
135 $( extra { $($extra:tt)* } )?
136 ) => {
137 #[::async_trait::async_trait]
138 impl<S: $crate::AuthSchema> $crate::AuthPlugin<S> for $plugin {
139 fn name(&self) -> &'static str { $name }
140
141 fn routes(&self) -> Vec<$crate::AuthRoute> {
142 vec![
143 $( $crate::AuthRoute::new($crate::impl_auth_plugin!(@pat $method), $path, $op_id), )*
144 ]
145 }
146
147 async fn on_request(
148 &self,
149 req: &$crate::AuthRequest,
150 ctx: &$crate::AuthContext<S>,
151 ) -> $crate::AuthResult<Option<$crate::AuthResponse>> {
152 match (req.method(), req.path()) {
153 $(
154 ($crate::impl_auth_plugin!(@pat $method), $path) => {
155 Ok(Some(self.$handler(req, ctx).await?))
156 }
157 )*
158 _ => Ok(None),
159 }
160 }
161
162 $( $($extra)* )?
163 }
164 };
165}
166
167#[derive(Debug, Clone)]
169pub struct AuthRoute {
170 pub path: String,
171 pub method: HttpMethod,
172 pub operation_id: String,
174}
175
176pub struct AuthInitContext<S: AuthSchema> {
178 pub config: Arc<AuthConfig>,
179 pub database: Arc<dyn AuthStore<S>>,
180 pub email_provider: Option<Arc<dyn EmailProvider>>,
181 pub metadata: MetadataMap,
182}
183
184pub struct AuthContext<S: AuthSchema> {
186 pub config: Arc<AuthConfig>,
187 pub database: Arc<dyn AuthStore<S>>,
188 pub email_provider: Option<Arc<dyn EmailProvider>>,
189 pub metadata: MetadataMap,
190}
191
192impl AuthRoute {
193 pub fn new(
194 method: HttpMethod,
195 path: impl Into<String>,
196 operation_id: impl Into<String>,
197 ) -> Self {
198 Self {
199 path: path.into(),
200 method,
201 operation_id: operation_id.into(),
202 }
203 }
204
205 pub fn get(path: impl Into<String>, operation_id: impl Into<String>) -> Self {
206 Self::new(HttpMethod::Get, path, operation_id)
207 }
208
209 pub fn post(path: impl Into<String>, operation_id: impl Into<String>) -> Self {
210 Self::new(HttpMethod::Post, path, operation_id)
211 }
212
213 pub fn put(path: impl Into<String>, operation_id: impl Into<String>) -> Self {
214 Self::new(HttpMethod::Put, path, operation_id)
215 }
216
217 pub fn delete(path: impl Into<String>, operation_id: impl Into<String>) -> Self {
218 Self::new(HttpMethod::Delete, path, operation_id)
219 }
220}
221
222impl<S: AuthSchema> AuthInitContext<S> {
223 pub fn new(config: Arc<AuthConfig>, database: Arc<dyn AuthStore<S>>) -> Self {
224 let email_provider = config.email_provider.clone();
225 Self {
226 config,
227 database,
228 email_provider,
229 metadata: MetadataMap::new(),
230 }
231 }
232
233 pub fn set_metadata(&mut self, key: impl Into<String>, value: serde_json::Value) {
234 _ = self.metadata.insert(key.into(), value);
235 }
236
237 pub fn get_metadata(&self, key: &str) -> Option<&serde_json::Value> {
238 self.metadata.get(key)
239 }
240
241 pub fn into_parts(self) -> AuthInitParts {
242 AuthInitParts {
243 metadata: self.metadata,
244 email_provider: self.email_provider,
245 }
246 }
247}
248
249impl<S: AuthSchema> AuthContext<S> {
250 pub fn new(config: Arc<AuthConfig>, database: Arc<dyn AuthStore<S>>) -> Self {
251 let email_provider = config.email_provider.clone();
252 Self {
253 config,
254 database,
255 email_provider,
256 metadata: MetadataMap::new(),
257 }
258 }
259
260 pub fn with_metadata(
261 config: Arc<AuthConfig>,
262 database: Arc<dyn AuthStore<S>>,
263 metadata: MetadataMap,
264 ) -> Self {
265 let email_provider = config.email_provider.clone();
266 Self {
267 config,
268 database,
269 email_provider,
270 metadata,
271 }
272 }
273
274 pub fn set_metadata(&mut self, key: impl Into<String>, value: serde_json::Value) {
275 _ = self.metadata.insert(key.into(), value);
276 }
277
278 pub fn get_metadata(&self, key: &str) -> Option<&serde_json::Value> {
279 self.metadata.get(key)
280 }
281
282 pub fn email_provider(&self) -> AuthResult<&dyn EmailProvider> {
284 self.email_provider
285 .as_deref()
286 .ok_or_else(|| AuthError::config("No email provider configured"))
287 }
288
289 pub fn session_manager(&self) -> crate::session::SessionManager<S> {
291 crate::session::SessionManager::new(self.config.clone(), self.database.clone())
292 }
293
294 pub async fn require_session(&self, req: &AuthRequest) -> AuthResult<(S::User, S::Session)> {
300 let session_manager = self.session_manager();
301
302 if let Some(token) = session_manager.extract_session_token(req)
303 && let Some(session) = session_manager.get_session(&token).await?
304 && let Some(user) = self.database.get_user_by_id(&session.user_id()).await?
305 {
306 return Ok((user, session));
307 }
308
309 Err(AuthError::Unauthenticated)
310 }
311}
312
313#[cfg(test)]
314mod tests {
315 use super::*;
316 use crate::entity::AuthUser;
317 use crate::test_store::test_database;
318
319 #[test]
321 fn auth_route_constructors() {
322 let get = AuthRoute::get("/test", "getTest");
323 assert_eq!(get.method, HttpMethod::Get);
324 assert_eq!(get.path, "/test");
325 assert_eq!(get.operation_id, "getTest");
326
327 let post = AuthRoute::post("/create", "createItem");
328 assert_eq!(post.method, HttpMethod::Post);
329
330 let put = AuthRoute::put("/update", "updateItem");
331 assert_eq!(put.method, HttpMethod::Put);
332
333 let delete = AuthRoute::delete("/remove", "deleteItem");
334 assert_eq!(delete.method, HttpMethod::Delete);
335 }
336
337 #[test]
339 fn auth_route_new() {
340 let route = AuthRoute::new(HttpMethod::Patch, "/patch", "patchIt");
341 assert_eq!(route.method, HttpMethod::Patch);
342 assert_eq!(route.path, "/patch");
343 }
344
345 #[test]
347 fn auth_context_new() {
348 let config = Arc::new(AuthConfig::new("test-secret-min-32-chars-1234567"));
349 let runtime = tokio::runtime::Runtime::new().expect("runtime should build");
350 let db = runtime.block_on(test_database());
351 let ctx = AuthContext::new(config.clone(), db);
352 assert!(ctx.email_provider.is_none());
353 assert!(ctx.metadata.is_empty());
354 }
355
356 #[test]
358 fn auth_context_metadata() {
359 let config = Arc::new(AuthConfig::new("test-secret-min-32-chars-1234567"));
360 let runtime = tokio::runtime::Runtime::new().expect("runtime should build");
361 let db = runtime.block_on(test_database());
362 let mut ctx = AuthContext::new(config, db);
363
364 ctx.set_metadata("key", serde_json::json!("value"));
365 assert_eq!(ctx.get_metadata("key"), Some(&serde_json::json!("value")));
366 assert!(ctx.get_metadata("missing").is_none());
367 }
368
369 #[test]
371 fn auth_context_email_provider_error_when_none() {
372 let config = Arc::new(AuthConfig::new("test-secret-min-32-chars-1234567"));
373 let runtime = tokio::runtime::Runtime::new().expect("runtime should build");
374 let db = runtime.block_on(test_database());
375 let ctx = AuthContext::new(config, db);
376 assert!(ctx.email_provider().is_err());
377 }
378
379 #[tokio::test]
381 async fn auth_context_require_session_unauthenticated() {
382 let config = Arc::new(AuthConfig::new("test-secret-min-32-chars-1234567"));
383 let db = test_database().await;
384 let ctx = AuthContext::new(config, db);
385 let req = AuthRequest::new(HttpMethod::Get, "/test");
386 let result = ctx.require_session(&req).await;
387 assert!(result.is_err());
388 }
389
390 #[tokio::test]
392 async fn auth_context_require_session_with_valid_session() {
393 let config = Arc::new(AuthConfig::new("test-secret-min-32-chars-1234567"));
394 let db = test_database().await;
395
396 let user = db
398 .create_user(crate::types::CreateUser::new().with_email("test@test.com"))
399 .await
400 .unwrap();
401
402 let sm = SessionManager::new(config.clone(), db.clone());
404 let session = sm.create_session(&user, None, None).await.unwrap();
405
406 let ctx = AuthContext::new(config.clone(), db);
408 let mut req = AuthRequest::new(HttpMethod::Get, "/test");
409 let _ = req.headers.insert(
410 "cookie".into(),
411 format!("better-auth.session_token={}", session.token()),
412 );
413
414 let (found_user, _found_session) = ctx.require_session(&req).await.unwrap();
415 assert_eq!(found_user.id(), user.id());
416 }
417}