1use crate::{
2 domain::value_objects::{ArticleId, CreatorId, TenantId, TransactionId, WalletAddress},
3 error::Result,
4};
5use chrono::{DateTime, Duration, Utc};
6use serde::{Deserialize, Serialize};
7use uuid::Uuid;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
11pub struct AccessTokenId(Uuid);
12
13impl AccessTokenId {
14 pub fn new() -> Self {
15 Self(Uuid::new_v4())
16 }
17
18 pub fn from_uuid(uuid: Uuid) -> Self {
19 Self(uuid)
20 }
21
22 pub fn parse(value: &str) -> Result<Self> {
23 let uuid = Uuid::parse_str(value).map_err(|e| {
24 crate::error::AllSourceError::InvalidInput(format!(
25 "Invalid access token ID '{value}': {e}"
26 ))
27 })?;
28 Ok(Self(uuid))
29 }
30
31 pub fn as_uuid(&self) -> Uuid {
32 self.0
33 }
34}
35
36impl Default for AccessTokenId {
37 fn default() -> Self {
38 Self::new()
39 }
40}
41
42impl std::fmt::Display for AccessTokenId {
43 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 write!(f, "{}", self.0)
45 }
46}
47
48#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
50pub enum AccessMethod {
51 #[default]
53 Paid,
54 Bundle,
56 Free,
58 Subscription,
60}
61
62const DEFAULT_ACCESS_DAYS: i64 = 30;
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct AccessToken {
78 id: AccessTokenId,
79 tenant_id: TenantId,
80 article_id: ArticleId,
81 creator_id: CreatorId,
82 reader_wallet: WalletAddress,
83 transaction_id: Option<TransactionId>,
84 access_method: AccessMethod,
85 token_hash: String,
86 issued_at: DateTime<Utc>,
87 expires_at: DateTime<Utc>,
88 revoked: bool,
89 revoked_at: Option<DateTime<Utc>>,
90 revocation_reason: Option<String>,
91 last_accessed_at: Option<DateTime<Utc>>,
92 access_count: u32,
93 metadata: serde_json::Value,
94}
95
96impl AccessToken {
97 pub fn new_paid(
99 tenant_id: TenantId,
100 article_id: ArticleId,
101 creator_id: CreatorId,
102 reader_wallet: WalletAddress,
103 transaction_id: TransactionId,
104 token_hash: String,
105 ) -> Result<Self> {
106 Self::validate_token_hash(&token_hash)?;
107
108 let now = Utc::now();
109 let expires_at = now + Duration::days(DEFAULT_ACCESS_DAYS);
110
111 Ok(Self {
112 id: AccessTokenId::new(),
113 tenant_id,
114 article_id,
115 creator_id,
116 reader_wallet,
117 transaction_id: Some(transaction_id),
118 access_method: AccessMethod::Paid,
119 token_hash,
120 issued_at: now,
121 expires_at,
122 revoked: false,
123 revoked_at: None,
124 revocation_reason: None,
125 last_accessed_at: None,
126 access_count: 0,
127 metadata: serde_json::json!({}),
128 })
129 }
130
131 pub fn new_with_duration(
133 tenant_id: TenantId,
134 article_id: ArticleId,
135 creator_id: CreatorId,
136 reader_wallet: WalletAddress,
137 transaction_id: Option<TransactionId>,
138 access_method: AccessMethod,
139 token_hash: String,
140 duration_days: i64,
141 ) -> Result<Self> {
142 Self::validate_token_hash(&token_hash)?;
143
144 if duration_days <= 0 || duration_days > 365 {
145 return Err(crate::error::AllSourceError::ValidationError(
146 "Access duration must be between 1 and 365 days".to_string(),
147 ));
148 }
149
150 let now = Utc::now();
151 let expires_at = now + Duration::days(duration_days);
152
153 Ok(Self {
154 id: AccessTokenId::new(),
155 tenant_id,
156 article_id,
157 creator_id,
158 reader_wallet,
159 transaction_id,
160 access_method,
161 token_hash,
162 issued_at: now,
163 expires_at,
164 revoked: false,
165 revoked_at: None,
166 revocation_reason: None,
167 last_accessed_at: None,
168 access_count: 0,
169 metadata: serde_json::json!({}),
170 })
171 }
172
173 pub fn new_free(
175 tenant_id: TenantId,
176 article_id: ArticleId,
177 creator_id: CreatorId,
178 reader_wallet: WalletAddress,
179 token_hash: String,
180 duration_days: i64,
181 ) -> Result<Self> {
182 Self::new_with_duration(
183 tenant_id,
184 article_id,
185 creator_id,
186 reader_wallet,
187 None,
188 AccessMethod::Free,
189 token_hash,
190 duration_days,
191 )
192 }
193
194 #[allow(clippy::too_many_arguments)]
196 pub fn reconstruct(
197 id: AccessTokenId,
198 tenant_id: TenantId,
199 article_id: ArticleId,
200 creator_id: CreatorId,
201 reader_wallet: WalletAddress,
202 transaction_id: Option<TransactionId>,
203 access_method: AccessMethod,
204 token_hash: String,
205 issued_at: DateTime<Utc>,
206 expires_at: DateTime<Utc>,
207 revoked: bool,
208 revoked_at: Option<DateTime<Utc>>,
209 revocation_reason: Option<String>,
210 last_accessed_at: Option<DateTime<Utc>>,
211 access_count: u32,
212 metadata: serde_json::Value,
213 ) -> Self {
214 Self {
215 id,
216 tenant_id,
217 article_id,
218 creator_id,
219 reader_wallet,
220 transaction_id,
221 access_method,
222 token_hash,
223 issued_at,
224 expires_at,
225 revoked,
226 revoked_at,
227 revocation_reason,
228 last_accessed_at,
229 access_count,
230 metadata,
231 }
232 }
233
234 pub fn id(&self) -> &AccessTokenId {
237 &self.id
238 }
239
240 pub fn tenant_id(&self) -> &TenantId {
241 &self.tenant_id
242 }
243
244 pub fn article_id(&self) -> &ArticleId {
245 &self.article_id
246 }
247
248 pub fn creator_id(&self) -> &CreatorId {
249 &self.creator_id
250 }
251
252 pub fn reader_wallet(&self) -> &WalletAddress {
253 &self.reader_wallet
254 }
255
256 pub fn transaction_id(&self) -> Option<&TransactionId> {
257 self.transaction_id.as_ref()
258 }
259
260 pub fn access_method(&self) -> AccessMethod {
261 self.access_method
262 }
263
264 pub fn token_hash(&self) -> &str {
265 &self.token_hash
266 }
267
268 pub fn issued_at(&self) -> DateTime<Utc> {
269 self.issued_at
270 }
271
272 pub fn expires_at(&self) -> DateTime<Utc> {
273 self.expires_at
274 }
275
276 pub fn is_revoked(&self) -> bool {
277 self.revoked
278 }
279
280 pub fn revoked_at(&self) -> Option<DateTime<Utc>> {
281 self.revoked_at
282 }
283
284 pub fn revocation_reason(&self) -> Option<&str> {
285 self.revocation_reason.as_deref()
286 }
287
288 pub fn last_accessed_at(&self) -> Option<DateTime<Utc>> {
289 self.last_accessed_at
290 }
291
292 pub fn access_count(&self) -> u32 {
293 self.access_count
294 }
295
296 pub fn metadata(&self) -> &serde_json::Value {
297 &self.metadata
298 }
299
300 pub fn is_expired(&self) -> bool {
304 Utc::now() > self.expires_at
305 }
306
307 pub fn is_valid(&self) -> bool {
309 !self.revoked && !self.is_expired()
310 }
311
312 pub fn grants_access_to(&self, article_id: &ArticleId, wallet: &WalletAddress) -> bool {
314 self.is_valid() && &self.article_id == article_id && &self.reader_wallet == wallet
315 }
316
317 pub fn remaining_seconds(&self) -> i64 {
319 if self.is_expired() {
320 0
321 } else {
322 (self.expires_at - Utc::now()).num_seconds().max(0)
323 }
324 }
325
326 pub fn remaining_days(&self) -> i64 {
328 if self.is_expired() {
329 0
330 } else {
331 (self.expires_at - Utc::now()).num_days().max(0)
332 }
333 }
334
335 pub fn record_access(&mut self) {
337 self.last_accessed_at = Some(Utc::now());
338 self.access_count += 1;
339 }
340
341 pub fn revoke(&mut self, reason: &str) -> Result<()> {
343 if self.revoked {
344 return Err(crate::error::AllSourceError::ValidationError(
345 "Token is already revoked".to_string(),
346 ));
347 }
348
349 self.revoked = true;
350 self.revoked_at = Some(Utc::now());
351 self.revocation_reason = Some(reason.to_string());
352 Ok(())
353 }
354
355 pub fn extend(&mut self, additional_days: i64) -> Result<()> {
357 if self.revoked {
358 return Err(crate::error::AllSourceError::ValidationError(
359 "Cannot extend a revoked token".to_string(),
360 ));
361 }
362
363 if additional_days <= 0 || additional_days > 365 {
364 return Err(crate::error::AllSourceError::ValidationError(
365 "Extension must be between 1 and 365 days".to_string(),
366 ));
367 }
368
369 self.expires_at += Duration::days(additional_days);
370 Ok(())
371 }
372
373 pub fn update_metadata(&mut self, metadata: serde_json::Value) {
375 self.metadata = metadata;
376 }
377
378 fn validate_token_hash(hash: &str) -> Result<()> {
381 if hash.is_empty() {
382 return Err(crate::error::AllSourceError::InvalidInput(
383 "Token hash cannot be empty".to_string(),
384 ));
385 }
386
387 if hash.len() < 32 || hash.len() > 128 {
389 return Err(crate::error::AllSourceError::InvalidInput(format!(
390 "Invalid token hash length: {}",
391 hash.len()
392 )));
393 }
394
395 Ok(())
396 }
397}
398
399#[cfg(test)]
400mod tests {
401 use super::*;
402
403 const VALID_WALLET: &str = "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM";
404 const VALID_TOKEN_HASH: &str =
405 "a1b2c3d4e5f6789012345678901234567890123456789012345678901234abcd";
406
407 fn test_tenant_id() -> TenantId {
408 TenantId::new("test-tenant".to_string()).unwrap()
409 }
410
411 fn test_article_id() -> ArticleId {
412 ArticleId::new("test-article".to_string()).unwrap()
413 }
414
415 fn test_creator_id() -> CreatorId {
416 CreatorId::new()
417 }
418
419 fn test_wallet() -> WalletAddress {
420 WalletAddress::new(VALID_WALLET.to_string()).unwrap()
421 }
422
423 fn test_transaction_id() -> TransactionId {
424 TransactionId::new()
425 }
426
427 #[test]
428 fn test_create_paid_access_token() {
429 let token = AccessToken::new_paid(
430 test_tenant_id(),
431 test_article_id(),
432 test_creator_id(),
433 test_wallet(),
434 test_transaction_id(),
435 VALID_TOKEN_HASH.to_string(),
436 );
437
438 assert!(token.is_ok());
439 let token = token.unwrap();
440 assert!(token.is_valid());
441 assert!(!token.is_expired());
442 assert!(!token.is_revoked());
443 assert_eq!(token.access_method(), AccessMethod::Paid);
444 assert!(token.transaction_id().is_some());
445 }
446
447 #[test]
448 fn test_create_free_access_token() {
449 let token = AccessToken::new_free(
450 test_tenant_id(),
451 test_article_id(),
452 test_creator_id(),
453 test_wallet(),
454 VALID_TOKEN_HASH.to_string(),
455 7, );
457
458 assert!(token.is_ok());
459 let token = token.unwrap();
460 assert!(token.is_valid());
461 assert_eq!(token.access_method(), AccessMethod::Free);
462 assert!(token.transaction_id().is_none());
463 }
464
465 #[test]
466 fn test_reject_empty_token_hash() {
467 let result = AccessToken::new_paid(
468 test_tenant_id(),
469 test_article_id(),
470 test_creator_id(),
471 test_wallet(),
472 test_transaction_id(),
473 String::new(),
474 );
475
476 assert!(result.is_err());
477 }
478
479 #[test]
480 fn test_reject_invalid_duration() {
481 let result = AccessToken::new_with_duration(
482 test_tenant_id(),
483 test_article_id(),
484 test_creator_id(),
485 test_wallet(),
486 None,
487 AccessMethod::Free,
488 VALID_TOKEN_HASH.to_string(),
489 0, );
491
492 assert!(result.is_err());
493
494 let result = AccessToken::new_with_duration(
495 test_tenant_id(),
496 test_article_id(),
497 test_creator_id(),
498 test_wallet(),
499 None,
500 AccessMethod::Free,
501 VALID_TOKEN_HASH.to_string(),
502 400, );
504
505 assert!(result.is_err());
506 }
507
508 #[test]
509 fn test_grants_access_to() {
510 let article_id = test_article_id();
511 let wallet = test_wallet();
512
513 let token = AccessToken::new_paid(
514 test_tenant_id(),
515 article_id.clone(),
516 test_creator_id(),
517 wallet.clone(),
518 test_transaction_id(),
519 VALID_TOKEN_HASH.to_string(),
520 )
521 .unwrap();
522
523 assert!(token.grants_access_to(&article_id, &wallet));
524
525 let other_article = ArticleId::new("other-article".to_string()).unwrap();
527 assert!(!token.grants_access_to(&other_article, &wallet));
528
529 let other_wallet =
531 WalletAddress::new("11111111111111111111111111111111".to_string()).unwrap();
532 assert!(!token.grants_access_to(&article_id, &other_wallet));
533 }
534
535 #[test]
536 fn test_revoke_token() {
537 let mut token = AccessToken::new_paid(
538 test_tenant_id(),
539 test_article_id(),
540 test_creator_id(),
541 test_wallet(),
542 test_transaction_id(),
543 VALID_TOKEN_HASH.to_string(),
544 )
545 .unwrap();
546
547 assert!(token.is_valid());
548
549 token.revoke("Refund processed").unwrap();
550
551 assert!(!token.is_valid());
552 assert!(token.is_revoked());
553 assert!(token.revoked_at().is_some());
554 assert_eq!(token.revocation_reason(), Some("Refund processed"));
555 }
556
557 #[test]
558 fn test_cannot_revoke_twice() {
559 let mut token = AccessToken::new_paid(
560 test_tenant_id(),
561 test_article_id(),
562 test_creator_id(),
563 test_wallet(),
564 test_transaction_id(),
565 VALID_TOKEN_HASH.to_string(),
566 )
567 .unwrap();
568
569 token.revoke("First revoke").unwrap();
570 let result = token.revoke("Second revoke");
571 assert!(result.is_err());
572 }
573
574 #[test]
575 fn test_record_access() {
576 let mut token = AccessToken::new_paid(
577 test_tenant_id(),
578 test_article_id(),
579 test_creator_id(),
580 test_wallet(),
581 test_transaction_id(),
582 VALID_TOKEN_HASH.to_string(),
583 )
584 .unwrap();
585
586 assert_eq!(token.access_count(), 0);
587 assert!(token.last_accessed_at().is_none());
588
589 token.record_access();
590
591 assert_eq!(token.access_count(), 1);
592 assert!(token.last_accessed_at().is_some());
593
594 token.record_access();
595 assert_eq!(token.access_count(), 2);
596 }
597
598 #[test]
599 fn test_extend_token() {
600 let mut token = AccessToken::new_paid(
601 test_tenant_id(),
602 test_article_id(),
603 test_creator_id(),
604 test_wallet(),
605 test_transaction_id(),
606 VALID_TOKEN_HASH.to_string(),
607 )
608 .unwrap();
609
610 let original_expiry = token.expires_at();
611 token.extend(7).unwrap();
612
613 assert!(token.expires_at() > original_expiry);
614 }
615
616 #[test]
617 fn test_cannot_extend_revoked_token() {
618 let mut token = AccessToken::new_paid(
619 test_tenant_id(),
620 test_article_id(),
621 test_creator_id(),
622 test_wallet(),
623 test_transaction_id(),
624 VALID_TOKEN_HASH.to_string(),
625 )
626 .unwrap();
627
628 token.revoke("Revoked").unwrap();
629 let result = token.extend(7);
630 assert!(result.is_err());
631 }
632
633 #[test]
634 fn test_remaining_time() {
635 let token = AccessToken::new_paid(
636 test_tenant_id(),
637 test_article_id(),
638 test_creator_id(),
639 test_wallet(),
640 test_transaction_id(),
641 VALID_TOKEN_HASH.to_string(),
642 )
643 .unwrap();
644
645 assert!(token.remaining_days() >= 29);
647 assert!(token.remaining_seconds() > 0);
648 }
649
650 #[test]
651 fn test_serde_serialization() {
652 let token = AccessToken::new_paid(
653 test_tenant_id(),
654 test_article_id(),
655 test_creator_id(),
656 test_wallet(),
657 test_transaction_id(),
658 VALID_TOKEN_HASH.to_string(),
659 )
660 .unwrap();
661
662 let json = serde_json::to_string(&token);
663 assert!(json.is_ok());
664
665 let deserialized: AccessToken = serde_json::from_str(&json.unwrap()).unwrap();
666 assert_eq!(deserialized.access_method(), AccessMethod::Paid);
667 }
668}