1use crate::application::dto::{
2 AccessTokenDto, CheckAccessRequest, CheckAccessResponse, GrantAccessResponse,
3 GrantFreeAccessRequest, ListAccessTokensResponse, RevokeAccessRequest, RevokeAccessResponse,
4};
5use crate::domain::entities::{AccessToken, AccessTokenId};
6use crate::domain::repositories::{AccessTokenRepository, ArticleRepository};
7use crate::domain::value_objects::{ArticleId, WalletAddress};
8use crate::error::Result;
9use sha2::{Digest, Sha256};
10use std::sync::Arc;
11
12pub struct GrantFreeAccessUseCase {
25 access_token_repository: Arc<dyn AccessTokenRepository>,
26 article_repository: Arc<dyn ArticleRepository>,
27}
28
29impl GrantFreeAccessUseCase {
30 pub fn new(
31 access_token_repository: Arc<dyn AccessTokenRepository>,
32 article_repository: Arc<dyn ArticleRepository>,
33 ) -> Self {
34 Self {
35 access_token_repository,
36 article_repository,
37 }
38 }
39
40 pub async fn execute(&self, request: GrantFreeAccessRequest) -> Result<GrantAccessResponse> {
41 let article_id = ArticleId::new(request.article_id)?;
43 let reader_wallet = WalletAddress::new(request.reader_wallet)?;
44 let tenant_id = crate::domain::value_objects::TenantId::new(request.tenant_id)?;
45
46 let article = self
48 .article_repository
49 .find_by_id(&article_id)
50 .await?
51 .ok_or_else(|| {
52 crate::error::AllSourceError::EntityNotFound("Article not found".to_string())
53 })?;
54
55 if self
57 .access_token_repository
58 .has_valid_access(&article_id, &reader_wallet)
59 .await?
60 {
61 return Err(crate::error::AllSourceError::ValidationError(
62 "Reader already has valid access to this article".to_string(),
63 ));
64 }
65
66 let duration_days = request.duration_days.unwrap_or(30);
68
69 let raw_token = generate_raw_token(&article_id, &reader_wallet);
71 let token_hash = hash_token(&raw_token);
72
73 let access_token = AccessToken::new_free(
75 tenant_id,
76 article_id,
77 *article.creator_id(),
78 reader_wallet,
79 token_hash,
80 duration_days,
81 )?;
82
83 self.access_token_repository.save(&access_token).await?;
85
86 Ok(GrantAccessResponse {
87 access_token: AccessTokenDto::from(&access_token),
88 raw_token,
89 })
90 }
91}
92
93pub struct CheckAccessUseCase {
97 repository: Arc<dyn AccessTokenRepository>,
98}
99
100impl CheckAccessUseCase {
101 pub fn new(repository: Arc<dyn AccessTokenRepository>) -> Self {
102 Self { repository }
103 }
104
105 pub async fn execute(&self, request: CheckAccessRequest) -> Result<CheckAccessResponse> {
106 let article_id = ArticleId::new(request.article_id)?;
107 let wallet = WalletAddress::new(request.wallet_address)?;
108
109 let token = self
110 .repository
111 .find_valid_token(&article_id, &wallet)
112 .await?;
113
114 match token {
115 Some(token) => Ok(CheckAccessResponse {
116 has_access: true,
117 remaining_days: Some(token.remaining_days()),
118 access_token: Some(AccessTokenDto::from(&token)),
119 }),
120 None => Ok(CheckAccessResponse {
121 has_access: false,
122 remaining_days: None,
123 access_token: None,
124 }),
125 }
126 }
127}
128
129pub struct ValidateTokenUseCase {
133 repository: Arc<dyn AccessTokenRepository>,
134}
135
136impl ValidateTokenUseCase {
137 pub fn new(repository: Arc<dyn AccessTokenRepository>) -> Self {
138 Self { repository }
139 }
140
141 pub async fn execute(
142 &self,
143 raw_token: &str,
144 article_id: &str,
145 wallet_address: &str,
146 ) -> Result<AccessTokenDto> {
147 let article_id = ArticleId::new(article_id.to_string())?;
148 let wallet = WalletAddress::new(wallet_address.to_string())?;
149
150 let token_hash = hash_token(raw_token);
152
153 let mut token = self
155 .repository
156 .find_by_hash(&token_hash)
157 .await?
158 .ok_or_else(|| crate::error::AllSourceError::EntityNotFound("Token not found".to_string()))?;
159
160 if !token.grants_access_to(&article_id, &wallet) {
162 return Err(crate::error::AllSourceError::ValidationError(
163 "Token does not grant access to this article".to_string(),
164 ));
165 }
166
167 token.record_access();
169
170 Ok(AccessTokenDto::from(&token))
174 }
175}
176
177pub struct RevokeAccessUseCase {
181 repository: Arc<dyn AccessTokenRepository>,
182}
183
184impl RevokeAccessUseCase {
185 pub fn new(repository: Arc<dyn AccessTokenRepository>) -> Self {
186 Self { repository }
187 }
188
189 pub async fn execute(&self, request: RevokeAccessRequest) -> Result<RevokeAccessResponse> {
190 let token_id = AccessTokenId::parse(&request.token_id)?;
191
192 let mut token = self
194 .repository
195 .find_by_id(&token_id)
196 .await?
197 .ok_or_else(|| crate::error::AllSourceError::EntityNotFound("Token not found".to_string()))?;
198
199 token.revoke(&request.reason)?;
201
202 let revoked = self.repository.revoke(&token_id, &request.reason).await?;
204
205 Ok(RevokeAccessResponse {
206 revoked,
207 access_token: AccessTokenDto::from(&token),
208 })
209 }
210}
211
212pub struct ExtendAccessUseCase;
216
217impl ExtendAccessUseCase {
218 pub fn execute(mut token: AccessToken, additional_days: i64) -> Result<AccessTokenDto> {
219 token.extend(additional_days)?;
220 Ok(AccessTokenDto::from(&token))
221 }
222}
223
224pub struct RecordAccessUseCase;
228
229impl RecordAccessUseCase {
230 pub fn execute(mut token: AccessToken) -> AccessTokenDto {
231 token.record_access();
232 AccessTokenDto::from(&token)
233 }
234}
235
236pub struct ListAccessTokensUseCase;
240
241impl ListAccessTokensUseCase {
242 pub fn execute(tokens: Vec<AccessToken>) -> ListAccessTokensResponse {
243 let token_dtos: Vec<AccessTokenDto> = tokens.iter().map(AccessTokenDto::from).collect();
244 let count = token_dtos.len();
245
246 ListAccessTokensResponse {
247 tokens: token_dtos,
248 count,
249 }
250 }
251}
252
253pub struct CleanupExpiredTokensUseCase {
257 repository: Arc<dyn AccessTokenRepository>,
258}
259
260impl CleanupExpiredTokensUseCase {
261 pub fn new(repository: Arc<dyn AccessTokenRepository>) -> Self {
262 Self { repository }
263 }
264
265 pub async fn execute(&self, before: chrono::DateTime<chrono::Utc>) -> Result<usize> {
266 self.repository.delete_expired(before).await
267 }
268}
269
270fn generate_raw_token(article_id: &ArticleId, wallet: &WalletAddress) -> String {
272 use rand::Rng;
273 let random_bytes: [u8; 32] = rand::thread_rng().gen();
274 let mut hasher = Sha256::new();
275 hasher.update(article_id.to_string().as_bytes());
276 hasher.update(wallet.to_string().as_bytes());
277 hasher.update(random_bytes);
278 hasher.update(chrono::Utc::now().to_rfc3339().as_bytes());
279 format!("{:x}", hasher.finalize())
280}
281
282fn hash_token(raw_token: &str) -> String {
284 let mut hasher = Sha256::new();
285 hasher.update(raw_token.as_bytes());
286 format!("{:x}", hasher.finalize())
287}
288
289#[cfg(test)]
290mod tests {
291 use super::*;
292 use crate::domain::entities::PaywallArticle;
293 use crate::domain::repositories::{AccessTokenQuery, ArticleQuery};
294 use crate::domain::value_objects::{CreatorId, TenantId, TransactionId};
295 use async_trait::async_trait;
296 use chrono::{DateTime, Utc};
297 use std::sync::Mutex;
298
299 const VALID_WALLET: &str = "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM";
300 const VALID_TOKEN_HASH: &str =
301 "a1b2c3d4e5f6789012345678901234567890123456789012345678901234abcd";
302
303 struct MockAccessTokenRepository {
304 tokens: Mutex<Vec<AccessToken>>,
305 }
306
307 impl MockAccessTokenRepository {
308 fn new() -> Self {
309 Self {
310 tokens: Mutex::new(Vec::new()),
311 }
312 }
313 }
314
315 #[async_trait]
316 impl AccessTokenRepository for MockAccessTokenRepository {
317 async fn save(&self, token: &AccessToken) -> Result<()> {
318 self.tokens.lock().unwrap().push(token.clone());
319 Ok(())
320 }
321
322 async fn find_by_id(&self, id: &AccessTokenId) -> Result<Option<AccessToken>> {
323 let tokens = self.tokens.lock().unwrap();
324 Ok(tokens.iter().find(|t| t.id() == id).cloned())
325 }
326
327 async fn find_by_hash(&self, token_hash: &str) -> Result<Option<AccessToken>> {
328 let tokens = self.tokens.lock().unwrap();
329 Ok(tokens
330 .iter()
331 .find(|t| t.token_hash() == token_hash)
332 .cloned())
333 }
334
335 async fn find_by_transaction(
336 &self,
337 _transaction_id: &TransactionId,
338 ) -> Result<Option<AccessToken>> {
339 Ok(None)
340 }
341
342 async fn find_by_article_and_wallet(
343 &self,
344 article_id: &ArticleId,
345 wallet: &WalletAddress,
346 ) -> Result<Vec<AccessToken>> {
347 let tokens = self.tokens.lock().unwrap();
348 Ok(tokens
349 .iter()
350 .filter(|t| t.article_id() == article_id && t.reader_wallet() == wallet)
351 .cloned()
352 .collect())
353 }
354
355 async fn find_valid_token(
356 &self,
357 article_id: &ArticleId,
358 wallet: &WalletAddress,
359 ) -> Result<Option<AccessToken>> {
360 let tokens = self.tokens.lock().unwrap();
361 Ok(tokens
362 .iter()
363 .find(|t| {
364 t.article_id() == article_id && t.reader_wallet() == wallet && t.is_valid()
365 })
366 .cloned())
367 }
368
369 async fn find_by_reader(
370 &self,
371 _wallet: &WalletAddress,
372 _limit: usize,
373 _offset: usize,
374 ) -> Result<Vec<AccessToken>> {
375 Ok(Vec::new())
376 }
377
378 async fn find_by_article(
379 &self,
380 _article_id: &ArticleId,
381 _limit: usize,
382 _offset: usize,
383 ) -> Result<Vec<AccessToken>> {
384 Ok(Vec::new())
385 }
386
387 async fn find_by_creator(
388 &self,
389 _creator_id: &CreatorId,
390 _limit: usize,
391 _offset: usize,
392 ) -> Result<Vec<AccessToken>> {
393 Ok(Vec::new())
394 }
395
396 async fn count(&self) -> Result<usize> {
397 Ok(self.tokens.lock().unwrap().len())
398 }
399
400 async fn count_valid(&self) -> Result<usize> {
401 Ok(0)
402 }
403
404 async fn count_by_article(&self, _article_id: &ArticleId) -> Result<usize> {
405 Ok(0)
406 }
407
408 async fn revoke(&self, _id: &AccessTokenId, _reason: &str) -> Result<bool> {
409 Ok(true)
410 }
411
412 async fn revoke_by_transaction(
413 &self,
414 _transaction_id: &TransactionId,
415 _reason: &str,
416 ) -> Result<usize> {
417 Ok(1)
418 }
419
420 async fn delete_expired(&self, _before: DateTime<Utc>) -> Result<usize> {
421 Ok(0)
422 }
423
424 async fn query(&self, _query: &AccessTokenQuery) -> Result<Vec<AccessToken>> {
425 Ok(Vec::new())
426 }
427 }
428
429 struct MockArticleRepository {
430 articles: Mutex<Vec<PaywallArticle>>,
431 }
432
433 impl MockArticleRepository {
434 fn new() -> Self {
435 Self {
436 articles: Mutex::new(Vec::new()),
437 }
438 }
439
440 fn add_article(&self, article: PaywallArticle) {
441 self.articles.lock().unwrap().push(article);
442 }
443 }
444
445 #[async_trait]
446 impl ArticleRepository for MockArticleRepository {
447 async fn save(&self, _article: &PaywallArticle) -> Result<()> {
448 Ok(())
449 }
450
451 async fn find_by_id(&self, id: &ArticleId) -> Result<Option<PaywallArticle>> {
452 let articles = self.articles.lock().unwrap();
453 Ok(articles.iter().find(|a| a.id() == id).cloned())
454 }
455
456 async fn find_by_url(&self, _url: &str) -> Result<Option<PaywallArticle>> {
457 Ok(None)
458 }
459
460 async fn find_by_creator(
461 &self,
462 _creator_id: &CreatorId,
463 _limit: usize,
464 _offset: usize,
465 ) -> Result<Vec<PaywallArticle>> {
466 Ok(Vec::new())
467 }
468
469 async fn find_by_tenant(
470 &self,
471 _tenant_id: &TenantId,
472 _limit: usize,
473 _offset: usize,
474 ) -> Result<Vec<PaywallArticle>> {
475 Ok(Vec::new())
476 }
477
478 async fn find_active_by_creator(
479 &self,
480 _creator_id: &CreatorId,
481 _limit: usize,
482 _offset: usize,
483 ) -> Result<Vec<PaywallArticle>> {
484 Ok(Vec::new())
485 }
486
487 async fn find_by_status(
488 &self,
489 _status: crate::domain::entities::ArticleStatus,
490 _limit: usize,
491 _offset: usize,
492 ) -> Result<Vec<PaywallArticle>> {
493 Ok(Vec::new())
494 }
495
496 async fn count(&self) -> Result<usize> {
497 Ok(0)
498 }
499
500 async fn count_by_creator(&self, _creator_id: &CreatorId) -> Result<usize> {
501 Ok(0)
502 }
503
504 async fn count_by_status(
505 &self,
506 _status: crate::domain::entities::ArticleStatus,
507 ) -> Result<usize> {
508 Ok(0)
509 }
510
511 async fn delete(&self, _id: &ArticleId) -> Result<bool> {
512 Ok(false)
513 }
514
515 async fn query(&self, _query: &ArticleQuery) -> Result<Vec<PaywallArticle>> {
516 Ok(Vec::new())
517 }
518
519 async fn find_top_by_revenue(
520 &self,
521 _creator_id: Option<&CreatorId>,
522 _limit: usize,
523 ) -> Result<Vec<PaywallArticle>> {
524 Ok(Vec::new())
525 }
526
527 async fn find_recent(
528 &self,
529 _creator_id: Option<&CreatorId>,
530 _limit: usize,
531 ) -> Result<Vec<PaywallArticle>> {
532 Ok(Vec::new())
533 }
534 }
535
536 #[tokio::test]
537 async fn test_grant_free_access() {
538 let token_repo = Arc::new(MockAccessTokenRepository::new());
539 let article_repo = Arc::new(MockArticleRepository::new());
540
541 let tenant_id = TenantId::new("test-tenant".to_string()).unwrap();
543 let article_id = ArticleId::new("test-article".to_string()).unwrap();
544 let creator_id = CreatorId::new();
545
546 let article = PaywallArticle::new(
547 article_id.clone(),
548 tenant_id.clone(),
549 creator_id,
550 "Test Article".to_string(),
551 "https://example.com/article".to_string(),
552 50,
553 )
554 .unwrap();
555 article_repo.add_article(article);
556
557 let use_case = GrantFreeAccessUseCase::new(token_repo.clone(), article_repo);
558
559 let request = GrantFreeAccessRequest {
560 tenant_id: "test-tenant".to_string(),
561 article_id: "test-article".to_string(),
562 reader_wallet: VALID_WALLET.to_string(),
563 duration_days: Some(7),
564 reason: Some("Promotional access".to_string()),
565 };
566
567 let response = use_case.execute(request).await;
568 assert!(response.is_ok());
569
570 let response = response.unwrap();
571 assert!(!response.raw_token.is_empty());
572 assert!(response.access_token.is_valid);
573 }
574
575 #[tokio::test]
576 async fn test_check_access_no_token() {
577 let token_repo = Arc::new(MockAccessTokenRepository::new());
578 let use_case = CheckAccessUseCase::new(token_repo);
579
580 let request = CheckAccessRequest {
581 article_id: "test-article".to_string(),
582 wallet_address: VALID_WALLET.to_string(),
583 };
584
585 let response = use_case.execute(request).await;
586 assert!(response.is_ok());
587
588 let response = response.unwrap();
589 assert!(!response.has_access);
590 assert!(response.access_token.is_none());
591 }
592
593 #[tokio::test]
594 async fn test_check_access_with_token() {
595 let token_repo = Arc::new(MockAccessTokenRepository::new());
596
597 let tenant_id = TenantId::new("test-tenant".to_string()).unwrap();
599 let article_id = ArticleId::new("test-article".to_string()).unwrap();
600 let creator_id = CreatorId::new();
601 let wallet = WalletAddress::new(VALID_WALLET.to_string()).unwrap();
602
603 let token = AccessToken::new_free(
604 tenant_id,
605 article_id.clone(),
606 creator_id,
607 wallet.clone(),
608 VALID_TOKEN_HASH.to_string(),
609 30,
610 )
611 .unwrap();
612
613 token_repo.save(&token).await.unwrap();
614
615 let use_case = CheckAccessUseCase::new(token_repo);
616
617 let request = CheckAccessRequest {
618 article_id: "test-article".to_string(),
619 wallet_address: VALID_WALLET.to_string(),
620 };
621
622 let response = use_case.execute(request).await;
623 assert!(response.is_ok());
624
625 let response = response.unwrap();
626 assert!(response.has_access);
627 assert!(response.access_token.is_some());
628 }
629
630 #[test]
631 fn test_extend_access() {
632 let tenant_id = TenantId::new("test-tenant".to_string()).unwrap();
633 let article_id = ArticleId::new("test-article".to_string()).unwrap();
634 let creator_id = CreatorId::new();
635 let wallet = WalletAddress::new(VALID_WALLET.to_string()).unwrap();
636
637 let token = AccessToken::new_free(
638 tenant_id,
639 article_id,
640 creator_id,
641 wallet,
642 VALID_TOKEN_HASH.to_string(),
643 7,
644 )
645 .unwrap();
646
647 let original_days = token.remaining_days();
648
649 let result = ExtendAccessUseCase::execute(token, 7);
650 assert!(result.is_ok());
651
652 let dto = result.unwrap();
653 assert!(dto.remaining_days >= original_days + 6); }
655
656 #[test]
657 fn test_list_access_tokens() {
658 let tenant_id = TenantId::new("test-tenant".to_string()).unwrap();
659 let article_id = ArticleId::new("test-article".to_string()).unwrap();
660 let creator_id = CreatorId::new();
661 let wallet = WalletAddress::new(VALID_WALLET.to_string()).unwrap();
662
663 let tokens = vec![AccessToken::new_free(
664 tenant_id,
665 article_id,
666 creator_id,
667 wallet,
668 VALID_TOKEN_HASH.to_string(),
669 30,
670 )
671 .unwrap()];
672
673 let response = ListAccessTokensUseCase::execute(tokens);
674 assert_eq!(response.count, 1);
675 assert_eq!(response.tokens.len(), 1);
676 }
677
678 #[test]
679 fn test_token_hashing() {
680 let raw_token = "test_token_12345";
681 let hash1 = hash_token(raw_token);
682 let hash2 = hash_token(raw_token);
683
684 assert_eq!(hash1, hash2);
686
687 assert_eq!(hash1.len(), 64);
689 }
690}