Skip to main content

reauth_types/
claims.rs

1use serde::{Deserialize, Serialize};
2
3use crate::SubscriptionStatus;
4
5/// JWT claims for domain end-users.
6///
7/// Issued by the Reauth API and verified by SDKs.
8/// Contains user identity, roles, and subscription information.
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct DomainEndUserClaims {
11    /// User ID (subject) - the end_user_id
12    pub sub: String,
13
14    /// Domain ID (UUID as string)
15    pub domain_id: String,
16
17    /// Root domain (e.g., "example.com")
18    pub domain: String,
19
20    /// Active organization ID (UUID as string)
21    pub active_org_id: String,
22
23    /// User's role in the active organization ("owner" | "member")
24    pub org_role: String,
25
26    /// User's roles (e.g., ["admin", "user"])
27    pub roles: Vec<String>,
28
29    /// Subscription information (always present)
30    pub subscription: SubscriptionClaims,
31
32    /// Token expiration (Unix timestamp)
33    pub exp: i64,
34
35    /// Token issued at (Unix timestamp)
36    pub iat: i64,
37}
38
39/// Subscription info embedded in JWT claims.
40///
41/// Uses snake_case to match the JSON format used in tokens.
42#[derive(Debug, Clone, Default, Serialize, Deserialize)]
43pub struct SubscriptionClaims {
44    /// Current subscription status.
45    /// Uses the `SubscriptionStatus` enum with `is_active()`, `has_access()` helpers.
46    pub status: SubscriptionStatus,
47
48    /// Machine-readable plan identifier (e.g., "pro")
49    pub plan_code: Option<String>,
50
51    /// Human-readable plan name (e.g., "Pro Plan")
52    pub plan_name: Option<String>,
53
54    /// Unix timestamp when current period ends
55    pub current_period_end: Option<i64>,
56
57    /// Whether subscription will cancel at period end
58    pub cancel_at_period_end: Option<bool>,
59
60    /// Unix timestamp when trial ends (if applicable)
61    pub trial_ends_at: Option<i64>,
62
63    /// Stripe subscription ID (for backend use)
64    pub subscription_id: Option<String>,
65}
66
67impl SubscriptionClaims {
68    /// Create a "none" subscription for users without a subscription.
69    pub fn none() -> Self {
70        Self {
71            status: SubscriptionStatus::None,
72            ..Default::default()
73        }
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    #[test]
82    fn test_subscription_claims_none() {
83        let sub = SubscriptionClaims::none();
84        assert_eq!(sub.status, SubscriptionStatus::None);
85        assert!(sub.plan_code.is_none());
86        assert!(sub.plan_name.is_none());
87    }
88
89    #[test]
90    fn test_subscription_claims_status_helpers() {
91        let active_sub = SubscriptionClaims {
92            status: SubscriptionStatus::Active,
93            ..Default::default()
94        };
95        assert!(active_sub.status.is_active());
96        assert!(active_sub.status.has_access());
97
98        let trialing_sub = SubscriptionClaims {
99            status: SubscriptionStatus::Trialing,
100            ..Default::default()
101        };
102        assert!(trialing_sub.status.is_active());
103        assert!(trialing_sub.status.has_access());
104
105        let canceled_sub = SubscriptionClaims {
106            status: SubscriptionStatus::Canceled,
107            ..Default::default()
108        };
109        assert!(!canceled_sub.status.is_active());
110        assert!(!canceled_sub.status.has_access());
111    }
112
113    #[test]
114    fn test_domain_end_user_claims_serde() {
115        let claims = DomainEndUserClaims {
116            sub: "user123".to_string(),
117            domain_id: "00000000-0000-0000-0000-000000000001".to_string(),
118            domain: "example.com".to_string(),
119            active_org_id: "00000000-0000-0000-0000-000000000002".to_string(),
120            org_role: "owner".to_string(),
121            roles: vec!["user".to_string()],
122            subscription: SubscriptionClaims {
123                status: SubscriptionStatus::Active,
124                plan_code: Some("pro".to_string()),
125                plan_name: Some("Pro Plan".to_string()),
126                current_period_end: Some(1735689600),
127                cancel_at_period_end: Some(false),
128                trial_ends_at: None,
129                subscription_id: Some("sub_123".to_string()),
130            },
131            exp: 1735689600,
132            iat: 1735603200,
133        };
134
135        let json = serde_json::to_string(&claims).unwrap();
136        let parsed: DomainEndUserClaims = serde_json::from_str(&json).unwrap();
137
138        assert_eq!(parsed.sub, "user123");
139        assert_eq!(parsed.domain, "example.com");
140        assert_eq!(parsed.active_org_id, "00000000-0000-0000-0000-000000000002");
141        assert_eq!(parsed.org_role, "owner");
142        assert_eq!(parsed.subscription.status, SubscriptionStatus::Active);
143        assert!(parsed.subscription.status.is_active());
144    }
145}