torii_core/
plugin.rs

1//! Plugin system
2//!
3//! This module contains the core plugin system including migrations.
4//!
5//! See [`Plugin`] trait for the required methods for a plugin.
6//!
7//! See [`PluginManager`] for the plugin manager which is responsible for managing the plugins.
8use dashmap::DashMap;
9use downcast_rs::{DowncastSync, impl_downcast};
10use std::any::Any;
11use std::sync::Arc;
12use std::time::Duration;
13
14use crate::storage::{SessionStorage, UserStorage};
15
16/// A trait for plugins.
17pub trait Plugin: Any + Send + Sync + DowncastSync {
18    /// The unique name of the plugin instance.
19    fn name(&self) -> String;
20}
21impl_downcast!(sync Plugin);
22
23/// Manages a collection of plugins.
24pub struct PluginManager<U: UserStorage, S: SessionStorage> {
25    plugins: DashMap<String, Arc<dyn Plugin>>,
26    user_storage: Arc<U>,
27    session_storage: Arc<S>,
28}
29
30impl<U: UserStorage, S: SessionStorage> PluginManager<U, S> {
31    /// Creates a new empty plugin manager.
32    ///
33    /// # Example
34    /// ```
35    /// use torii_core::PluginManager;
36    ///
37    /// let plugin_manager = PluginManager::new();
38    /// ```
39    pub fn new(user_storage: Arc<U>, session_storage: Arc<S>) -> Self {
40        Self {
41            plugins: DashMap::new(),
42            user_storage,
43            session_storage,
44        }
45    }
46
47    /// Gets a plugin instance by its type.
48    ///
49    /// # Example
50    /// ```
51    /// use torii_core::{PluginManager, Plugin};
52    ///
53    /// struct MyPlugin;
54    /// impl Plugin for MyPlugin { /* ... */ }
55    ///
56    /// let mut plugin_manager = PluginManager::new();
57    /// plugin_manager.register(MyPlugin);
58    ///
59    /// let plugin = plugin_manager.get_plugin::<MyPlugin>("my_plugin");
60    /// ```
61    pub fn get_plugin<T: Plugin + 'static>(&self, name: &str) -> Option<Arc<T>> {
62        let plugin = self.plugins.get(name)?;
63        plugin.value().clone().downcast_arc::<T>().ok()
64    }
65
66    /// Registers a new plugin with the plugin manager.
67    ///
68    /// # Example
69    /// ```
70    /// use torii_core::{PluginManager, Plugin};
71    ///
72    /// struct MyPlugin;
73    /// impl Plugin for MyPlugin { /* ... */ }
74    ///
75    /// let mut plugin_manager = PluginManager::new();
76    /// plugin_manager.register(MyPlugin);
77    /// ```
78    pub fn register_plugin<T: Plugin + 'static>(&mut self, plugin: T) {
79        let name = plugin.name();
80        let plugin = Arc::new(plugin);
81        self.plugins.insert(name.clone(), plugin.clone());
82        tracing::info!(plugin.name = name, "Registered plugin");
83    }
84
85    pub fn user_storage(&self) -> &Arc<U> {
86        &self.user_storage
87    }
88
89    pub fn session_storage(&self) -> &Arc<S> {
90        &self.session_storage
91    }
92
93    /// Starts a background task to cleanup expired sessions.
94    ///
95    /// # Example
96    /// ```
97    /// use torii_core::PluginManager;
98    /// use torii_core::SessionCleanupConfig;
99    ///
100    /// let plugin_manager = PluginManager::new();
101    /// plugin_manager.start_session_cleanup_task(SessionCleanupConfig::default());
102    /// ```
103    pub async fn start_session_cleanup_task(&self, config: SessionCleanupConfig) {
104        let storage = self.session_storage.clone();
105        tokio::spawn(async move {
106            let mut interval = tokio::time::interval(config.interval);
107            loop {
108                interval.tick().await;
109                if let Err(e) = storage.cleanup_expired_sessions().await {
110                    tracing::error!("Failed to cleanup sessions: {}", e);
111                }
112            }
113        });
114    }
115}
116
117#[derive(Clone)]
118pub struct SessionCleanupConfig {
119    /// How often to run the cleanup task (default: 1 hour)
120    pub interval: Duration,
121    /// Maximum age of sessions before they're cleaned up (default: 30 days)
122    pub max_session_age: Duration,
123}
124
125impl Default for SessionCleanupConfig {
126    fn default() -> Self {
127        Self {
128            interval: Duration::from_secs(3600),           // 1 hour
129            max_session_age: Duration::from_secs(2592000), // 30 days
130        }
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use async_trait::async_trait;
137
138    use crate::{Error, NewUser, Session, User, UserId, session::SessionId};
139
140    use super::*;
141
142    #[derive(Debug, Clone)]
143    struct TestPlugin;
144
145    impl Plugin for TestPlugin {
146        fn name(&self) -> String {
147            "test".to_string()
148        }
149    }
150
151    impl TestPlugin {
152        fn new() -> Self {
153            Self
154        }
155    }
156
157    struct TestStorage {
158        users: DashMap<UserId, User>,
159        sessions: DashMap<SessionId, Session>,
160    }
161
162    impl TestStorage {
163        fn new() -> Self {
164            Self {
165                users: DashMap::new(),
166                sessions: DashMap::new(),
167            }
168        }
169    }
170
171    #[async_trait]
172    impl UserStorage for TestStorage {
173        type Error = Error;
174
175        async fn get_user(&self, id: &UserId) -> Result<Option<User>, Self::Error> {
176            Ok(self.users.get(id).map(|u| u.clone()))
177        }
178
179        async fn get_user_by_email(&self, email: &str) -> Result<Option<User>, Self::Error> {
180            Ok(self
181                .users
182                .iter()
183                .find(|u| u.email == email)
184                .map(|u| u.clone()))
185        }
186
187        async fn get_or_create_user_by_email(&self, email: &str) -> Result<User, Self::Error> {
188            match self.get_user_by_email(email).await {
189                Ok(Some(user)) => Ok(user),
190                Ok(None) => {
191                    self.create_user(
192                        &NewUser::builder()
193                            .id(UserId::new_random())
194                            .email(email.to_string())
195                            .build()
196                            .unwrap(),
197                    )
198                    .await
199                }
200                Err(e) => Err(e),
201            }
202        }
203
204        async fn create_user(&self, new_user: &NewUser) -> Result<User, Self::Error> {
205            let user = User::builder()
206                .email(new_user.email.clone())
207                .created_at(chrono::Utc::now())
208                .email_verified_at(None)
209                .name(None)
210                .updated_at(chrono::Utc::now())
211                .build()
212                .unwrap();
213            self.users.insert(user.id.clone(), user.clone());
214            Ok(user)
215        }
216
217        async fn update_user(&self, user: &User) -> Result<User, Self::Error> {
218            self.users.insert(user.id.clone(), user.clone());
219            Ok(user.clone())
220        }
221
222        async fn delete_user(&self, id: &UserId) -> Result<(), Self::Error> {
223            self.users.remove(id);
224            Ok(())
225        }
226
227        async fn set_user_email_verified(&self, user_id: &UserId) -> Result<(), Self::Error> {
228            let mut user = self.users.get_mut(user_id).unwrap();
229            user.email_verified_at = Some(chrono::Utc::now());
230            self.users.insert(user_id.clone(), user.clone());
231            Ok(())
232        }
233    }
234
235    #[async_trait]
236    impl SessionStorage for TestStorage {
237        type Error = Error;
238
239        async fn get_session(&self, id: &SessionId) -> Result<Session, Self::Error> {
240            Ok(self.sessions.get(id).unwrap().clone())
241        }
242
243        async fn create_session(&self, session: &Session) -> Result<Session, Self::Error> {
244            self.sessions.insert(session.id.clone(), session.clone());
245            Ok(session.clone())
246        }
247
248        async fn delete_session(&self, id: &SessionId) -> Result<(), Self::Error> {
249            self.sessions.remove(id);
250            Ok(())
251        }
252
253        async fn delete_sessions_for_user(&self, user_id: &UserId) -> Result<(), Self::Error> {
254            self.sessions.retain(|_, s| s.user_id != *user_id);
255            Ok(())
256        }
257
258        async fn cleanup_expired_sessions(&self) -> Result<(), Self::Error> {
259            let now = chrono::Utc::now();
260            self.sessions.retain(|_, s| s.expires_at > now);
261            Ok(())
262        }
263    }
264
265    // Setup test storage for testing
266    fn setup_test_storage() -> (Arc<TestStorage>, Arc<TestStorage>) {
267        let user_storage = Arc::new(TestStorage::new());
268        let session_storage = Arc::new(TestStorage::new());
269        (user_storage, session_storage)
270    }
271
272    #[tokio::test]
273    async fn test_plugin_manager() {
274        let (user_storage, session_storage) = setup_test_storage();
275        let mut plugin_manager = PluginManager::new(user_storage, session_storage);
276        plugin_manager.register_plugin(TestPlugin::new());
277        let plugin = plugin_manager.get_plugin::<TestPlugin>("test").unwrap();
278        assert_eq!(plugin.name(), "test");
279    }
280
281    #[tokio::test]
282    async fn test_session_cleanup() {
283        let (user_storage, session_storage) = setup_test_storage();
284        let plugin_manager = PluginManager::new(user_storage.clone(), session_storage.clone());
285
286        // Create an expired session
287        let expired_session = Session {
288            id: SessionId::new("expired"),
289            user_id: UserId::new("test"),
290            user_agent: None,
291            ip_address: None,
292            created_at: chrono::Utc::now(),
293            updated_at: chrono::Utc::now(),
294            expires_at: chrono::Utc::now() - chrono::Duration::hours(1),
295        };
296        session_storage
297            .create_session(&expired_session)
298            .await
299            .expect("Failed to create expired session");
300
301        // Create a valid session
302        let valid_session = Session {
303            id: SessionId::new("valid"),
304            user_id: UserId::new("test"),
305            user_agent: None,
306            ip_address: None,
307            created_at: chrono::Utc::now(),
308            updated_at: chrono::Utc::now(),
309            expires_at: chrono::Utc::now() + chrono::Duration::hours(1),
310        };
311        session_storage
312            .create_session(&valid_session)
313            .await
314            .expect("Failed to create valid session");
315
316        // Configure cleanup to run frequently
317        let config = SessionCleanupConfig {
318            interval: Duration::from_millis(100),
319            max_session_age: Duration::from_secs(3600),
320        };
321
322        // Start cleanup task
323        plugin_manager.start_session_cleanup_task(config).await;
324
325        // Wait for cleanup to run
326        tokio::time::sleep(Duration::from_millis(200)).await;
327    }
328}