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