Skip to main content

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}