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