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