1use crate::{
2 Error, User,
3 repositories::{TokenRepository, UserRepository},
4 services::UserService,
5 storage::{SecureToken, TokenPurpose},
6};
7use chrono::Duration;
8use std::sync::Arc;
9
10pub struct MagicLinkService<U: UserRepository, T: TokenRepository> {
12 user_service: Arc<UserService<U>>,
13 token_repository: Arc<T>,
14}
15
16impl<U: UserRepository, T: TokenRepository> MagicLinkService<U, T> {
17 pub fn new(user_repository: Arc<U>, token_repository: Arc<T>) -> Self {
19 let user_service = Arc::new(UserService::new(user_repository));
20 Self {
21 user_service,
22 token_repository,
23 }
24 }
25
26 pub async fn generate_token(&self, email: &str) -> Result<SecureToken, Error> {
28 let user = self.user_service.get_or_create_user(email).await?;
30
31 let expires_in = Duration::minutes(15);
33 self.token_repository
34 .create_token(&user.id, TokenPurpose::MagicLink, expires_in)
35 .await
36 }
37
38 pub async fn generate_token_with_expiration(
40 &self,
41 email: &str,
42 expires_in: Duration,
43 ) -> Result<SecureToken, Error> {
44 let user = self.user_service.get_or_create_user(email).await?;
46
47 self.token_repository
48 .create_token(&user.id, TokenPurpose::MagicLink, expires_in)
49 .await
50 }
51
52 pub async fn verify_token(&self, token: &str) -> Result<Option<User>, Error> {
54 let secure_token = self
56 .token_repository
57 .verify_token(token, TokenPurpose::MagicLink)
58 .await?;
59
60 if let Some(secure_token) = secure_token {
61 let user = self.user_service.get_user(&secure_token.user_id).await?;
63 Ok(user)
64 } else {
65 Ok(None)
66 }
67 }
68
69 pub async fn cleanup_expired_tokens(&self) -> Result<(), Error> {
71 self.token_repository.cleanup_expired_tokens().await
72 }
73}
74
75#[cfg(test)]
76mod tests {
77 use super::*;
78 use crate::repositories::{TokenRepository, UserRepository};
79 use crate::storage::{SecureToken, TokenPurpose};
80 use crate::{User, UserId};
81 use async_trait::async_trait;
82 use chrono::{DateTime, Utc};
83 use std::collections::HashMap;
84 use std::sync::Arc;
85 use tokio::sync::Mutex;
86
87 #[derive(Debug, Clone)]
89 struct MockUser {
90 id: UserId,
91 email: String,
92 name: Option<String>,
93 created_at: DateTime<Utc>,
94 updated_at: DateTime<Utc>,
95 }
96
97 impl From<MockUser> for User {
98 fn from(user: MockUser) -> Self {
99 User {
100 id: user.id,
101 email: user.email,
102 name: user.name,
103 email_verified_at: None,
104 created_at: user.created_at,
105 updated_at: user.updated_at,
106 }
107 }
108 }
109
110 #[derive(Default)]
111 struct MockUserRepository {
112 users: Arc<Mutex<HashMap<UserId, MockUser>>>,
113 users_by_email: Arc<Mutex<HashMap<String, MockUser>>>,
114 }
115
116 #[async_trait]
117 impl UserRepository for MockUserRepository {
118 async fn create(&self, new_user: crate::storage::NewUser) -> Result<User, Error> {
119 let user = MockUser {
120 id: UserId::new_random(),
121 email: new_user.email.clone(),
122 name: new_user.name,
123 created_at: Utc::now(),
124 updated_at: Utc::now(),
125 };
126
127 self.users
128 .lock()
129 .await
130 .insert(user.id.clone(), user.clone());
131 self.users_by_email
132 .lock()
133 .await
134 .insert(new_user.email, user.clone());
135 Ok(user.into())
136 }
137
138 async fn find_by_id(&self, id: &UserId) -> Result<Option<User>, Error> {
139 Ok(self.users.lock().await.get(id).cloned().map(Into::into))
140 }
141
142 async fn find_by_email(&self, email: &str) -> Result<Option<User>, Error> {
143 Ok(self
144 .users_by_email
145 .lock()
146 .await
147 .get(email)
148 .cloned()
149 .map(Into::into))
150 }
151
152 async fn find_or_create_by_email(&self, email: &str) -> Result<User, Error> {
153 if let Some(user) = self.find_by_email(email).await? {
154 Ok(user)
155 } else {
156 let new_user = crate::storage::NewUser::builder()
157 .email(email.to_string())
158 .build()
159 .unwrap();
160 self.create(new_user).await
161 }
162 }
163
164 async fn update(&self, _user: &User) -> Result<User, Error> {
165 unimplemented!()
166 }
167
168 async fn delete(&self, _id: &UserId) -> Result<(), Error> {
169 unimplemented!()
170 }
171
172 async fn mark_email_verified(&self, _user_id: &UserId) -> Result<(), Error> {
173 Ok(())
174 }
175 }
176
177 #[derive(Default)]
178 struct MockTokenRepository {
179 tokens: Arc<Mutex<HashMap<String, SecureToken>>>,
180 }
181
182 #[async_trait]
183 impl TokenRepository for MockTokenRepository {
184 async fn create_token(
185 &self,
186 user_id: &UserId,
187 purpose: TokenPurpose,
188 expires_in: Duration,
189 ) -> Result<SecureToken, Error> {
190 let token_str = "test_token_123".to_string();
191 let expires_at = Utc::now() + expires_in;
192
193 let secure_token = SecureToken::new(
194 user_id.clone(),
195 token_str.clone(),
196 purpose,
197 None,
198 expires_at,
199 Utc::now(),
200 Utc::now(),
201 );
202
203 self.tokens
204 .lock()
205 .await
206 .insert(token_str, secure_token.clone());
207
208 Ok(secure_token)
209 }
210
211 async fn verify_token(
212 &self,
213 token: &str,
214 purpose: TokenPurpose,
215 ) -> Result<Option<SecureToken>, Error> {
216 let mut tokens = self.tokens.lock().await;
217 if let Some(secure_token) = tokens.get(token) {
218 if secure_token.purpose == purpose
219 && secure_token.expires_at > Utc::now()
220 && secure_token.used_at.is_none()
221 {
222 let mut verified_token = secure_token.clone();
223 verified_token.used_at = Some(Utc::now());
224 verified_token.updated_at = Utc::now();
225 tokens.insert(token.to_string(), verified_token.clone());
226 Ok(Some(verified_token))
227 } else {
228 Ok(None)
229 }
230 } else {
231 Ok(None)
232 }
233 }
234
235 async fn check_token(&self, token: &str, purpose: TokenPurpose) -> Result<bool, Error> {
236 let tokens = self.tokens.lock().await;
237 if let Some(secure_token) = tokens.get(token) {
238 Ok(secure_token.purpose == purpose
239 && secure_token.expires_at > Utc::now()
240 && secure_token.used_at.is_none())
241 } else {
242 Ok(false)
243 }
244 }
245
246 async fn cleanup_expired_tokens(&self) -> Result<(), Error> {
247 let mut tokens = self.tokens.lock().await;
248 let now = Utc::now();
249 tokens.retain(|_, token| token.expires_at > now);
250 Ok(())
251 }
252 }
253
254 #[tokio::test]
255 async fn test_generate_token() {
256 let user_repo = Arc::new(MockUserRepository::default());
257 let token_repo = Arc::new(MockTokenRepository::default());
258 let service = MagicLinkService::new(user_repo, token_repo);
259
260 let result = service.generate_token("test@example.com").await;
261 assert!(result.is_ok());
262
263 let token = result.unwrap();
264 assert_eq!(token.token, "test_token_123");
265 assert_eq!(token.purpose, TokenPurpose::MagicLink);
266 }
267
268 #[tokio::test]
269 async fn test_generate_token_with_expiration() {
270 let user_repo = Arc::new(MockUserRepository::default());
271 let token_repo = Arc::new(MockTokenRepository::default());
272 let service = MagicLinkService::new(user_repo, token_repo);
273
274 let expires_in = Duration::minutes(30);
275 let result = service
276 .generate_token_with_expiration("test@example.com", expires_in)
277 .await;
278 assert!(result.is_ok());
279
280 let token = result.unwrap();
281 assert_eq!(token.purpose, TokenPurpose::MagicLink);
282 }
283
284 #[tokio::test]
285 async fn test_verify_token_success() {
286 let user_repo = Arc::new(MockUserRepository::default());
287 let token_repo = Arc::new(MockTokenRepository::default());
288 let service = MagicLinkService::new(user_repo, token_repo);
289
290 let token = service.generate_token("test@example.com").await.unwrap();
292
293 let result = service.verify_token(&token.token).await;
295 assert!(result.is_ok());
296
297 let user = result.unwrap();
298 assert!(user.is_some());
299 assert_eq!(user.unwrap().email, "test@example.com");
300 }
301
302 #[tokio::test]
303 async fn test_verify_token_not_found() {
304 let user_repo = Arc::new(MockUserRepository::default());
305 let token_repo = Arc::new(MockTokenRepository::default());
306 let service = MagicLinkService::new(user_repo, token_repo);
307
308 let result = service.verify_token("invalid_token").await;
309 assert!(result.is_ok());
310 assert!(result.unwrap().is_none());
311 }
312
313 #[tokio::test]
314 async fn test_cleanup_expired_tokens() {
315 let user_repo = Arc::new(MockUserRepository::default());
316 let token_repo = Arc::new(MockTokenRepository::default());
317 let service = MagicLinkService::new(user_repo, token_repo);
318
319 let result = service.cleanup_expired_tokens().await;
320 assert!(result.is_ok());
321 }
322}