ferro_rs/auth/provider.rs
1//! User provider trait for retrieving authenticated users from storage
2//!
3//! The application must implement this trait and register it with the container
4//! to enable `Auth::user()`.
5
6use async_trait::async_trait;
7use sea_orm::{EntityTrait, PrimaryKeyTrait};
8use std::marker::PhantomData;
9use std::sync::Arc;
10
11use super::authenticatable::Authenticatable;
12use crate::database::DB;
13use crate::error::FrameworkError;
14
15/// Trait for retrieving authenticated users from storage
16///
17/// The application must implement this trait and register it with the container
18/// to enable `Auth::user()`.
19///
20/// # Example
21///
22/// ```rust,ignore
23/// use ferro_rs::auth::{UserProvider, Authenticatable};
24/// use ferro_rs::FrameworkError;
25/// use async_trait::async_trait;
26/// use std::sync::Arc;
27///
28/// pub struct DatabaseUserProvider;
29///
30/// #[async_trait]
31/// impl UserProvider for DatabaseUserProvider {
32/// async fn retrieve_by_id(&self, id: i64) -> Result<Option<Arc<dyn Authenticatable>>, FrameworkError> {
33/// let user = User::query()
34/// .filter(Column::Id.eq(id as i32))
35/// .first()
36/// .await?;
37/// Ok(user.map(|u| Arc::new(u) as Arc<dyn Authenticatable>))
38/// }
39/// }
40/// ```
41#[async_trait]
42pub trait UserProvider: Send + Sync + 'static {
43 /// Retrieve a user by their unique identifier
44 async fn retrieve_by_id(
45 &self,
46 id: i64,
47 ) -> Result<Option<Arc<dyn Authenticatable>>, FrameworkError>;
48
49 /// Retrieve a user by credentials (for custom authentication flows)
50 ///
51 /// Default implementation returns None (not supported).
52 /// Override this if you need to authenticate by credentials other than ID.
53 async fn retrieve_by_credentials(
54 &self,
55 _credentials: &serde_json::Value,
56 ) -> Result<Option<Arc<dyn Authenticatable>>, FrameworkError> {
57 Ok(None)
58 }
59
60 /// Validate credentials against a user
61 ///
62 /// Default implementation returns false (not supported).
63 /// Override this if you need password validation.
64 async fn validate_credentials(
65 &self,
66 _user: &dyn Authenticatable,
67 _credentials: &serde_json::Value,
68 ) -> Result<bool, FrameworkError> {
69 Ok(false)
70 }
71}
72
73/// A generic [`UserProvider`] that loads a model by primary key.
74///
75/// Works for any entity whose `Model` is [`Authenticatable`] and whose primary
76/// key is a single integer column (`i32`/`i64`). This removes the hand-written
77/// provider apps otherwise need just to hydrate `Auth::user()` from the session.
78///
79/// ```rust,ignore
80/// use ferro_rs::{bind, ModelUserProvider, UserProvider};
81///
82/// // in bootstrap:
83/// bind!(dyn UserProvider, ModelUserProvider::<crate::models::user::Entity>::default());
84/// ```
85///
86/// Only [`retrieve_by_id`](UserProvider::retrieve_by_id) is implemented;
87/// credential lookup/validation use the trait defaults. For composite or
88/// non-integer keys, or password login, implement `UserProvider` by hand.
89pub struct ModelUserProvider<E: EntityTrait> {
90 // `fn() -> E` keeps the struct `Send + Sync` regardless of `E`.
91 _marker: PhantomData<fn() -> E>,
92}
93
94impl<E: EntityTrait> ModelUserProvider<E> {
95 /// Create a new provider for entity `E`.
96 pub fn new() -> Self {
97 Self {
98 _marker: PhantomData,
99 }
100 }
101}
102
103impl<E: EntityTrait> Default for ModelUserProvider<E> {
104 fn default() -> Self {
105 Self::new()
106 }
107}
108
109#[async_trait]
110impl<E> UserProvider for ModelUserProvider<E>
111where
112 E: EntityTrait + 'static,
113 E::Model: Authenticatable + Clone,
114 <E::PrimaryKey as PrimaryKeyTrait>::ValueType: TryFrom<i64> + Send,
115{
116 async fn retrieve_by_id(
117 &self,
118 id: i64,
119 ) -> Result<Option<Arc<dyn Authenticatable>>, FrameworkError> {
120 // Narrow the session's i64 id to the entity's primary-key value type.
121 // Fails only when the id is out of range for a narrower pk (e.g. i32).
122 let pk = <<E as EntityTrait>::PrimaryKey as PrimaryKeyTrait>::ValueType::try_from(id)
123 .map_err(|_| {
124 FrameworkError::internal(format!(
125 "authenticated id {id} is out of range for {}'s primary key",
126 std::any::type_name::<E>()
127 ))
128 })?;
129
130 let db = DB::connection()?;
131 let model = E::find_by_id(pk)
132 .one(db.inner())
133 .await
134 .map_err(|e| FrameworkError::database(e.to_string()))?;
135
136 Ok(model.map(|m| Arc::new(m) as Arc<dyn Authenticatable>))
137 }
138}