forge_core/auth/
claims.rs1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3use uuid::Uuid;
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct Claims {
8 pub sub: String,
10 pub iat: i64,
12 pub exp: i64,
14 #[serde(default)]
16 pub roles: Vec<String>,
17 #[serde(flatten)]
19 pub custom: HashMap<String, serde_json::Value>,
20}
21
22impl Claims {
23 pub fn user_id(&self) -> Option<Uuid> {
25 Uuid::parse_str(&self.sub).ok()
26 }
27
28 pub fn is_expired(&self) -> bool {
30 let now = chrono::Utc::now().timestamp();
31 self.exp < now
32 }
33
34 pub fn has_role(&self, role: &str) -> bool {
36 self.roles.iter().any(|r| r == role)
37 }
38
39 pub fn get_claim(&self, key: &str) -> Option<&serde_json::Value> {
41 self.custom.get(key)
42 }
43
44 pub fn tenant_id(&self) -> Option<Uuid> {
46 self.custom
47 .get("tenant_id")
48 .and_then(|v| v.as_str())
49 .and_then(|s| Uuid::parse_str(s).ok())
50 }
51
52 pub fn builder() -> ClaimsBuilder {
54 ClaimsBuilder::new()
55 }
56}
57
58#[derive(Debug, Default)]
60pub struct ClaimsBuilder {
61 sub: Option<String>,
62 roles: Vec<String>,
63 custom: HashMap<String, serde_json::Value>,
64 duration_secs: i64,
65}
66
67impl ClaimsBuilder {
68 pub fn new() -> Self {
70 Self {
71 sub: None,
72 roles: Vec::new(),
73 custom: HashMap::new(),
74 duration_secs: 3600, }
76 }
77
78 pub fn subject(mut self, sub: impl Into<String>) -> Self {
80 self.sub = Some(sub.into());
81 self
82 }
83
84 pub fn user_id(mut self, id: Uuid) -> Self {
86 self.sub = Some(id.to_string());
87 self
88 }
89
90 pub fn role(mut self, role: impl Into<String>) -> Self {
92 self.roles.push(role.into());
93 self
94 }
95
96 pub fn roles(mut self, roles: Vec<String>) -> Self {
98 self.roles = roles;
99 self
100 }
101
102 pub fn claim(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
104 self.custom.insert(key.into(), value);
105 self
106 }
107
108 pub fn tenant_id(mut self, id: Uuid) -> Self {
110 self.custom
111 .insert("tenant_id".to_string(), serde_json::json!(id.to_string()));
112 self
113 }
114
115 pub fn duration_secs(mut self, secs: i64) -> Self {
117 self.duration_secs = secs;
118 self
119 }
120
121 pub fn build(self) -> Result<Claims, String> {
123 let sub = self.sub.ok_or("Subject is required")?;
124 let now = chrono::Utc::now().timestamp();
125
126 Ok(Claims {
127 sub,
128 iat: now,
129 exp: now + self.duration_secs,
130 roles: self.roles,
131 custom: self.custom,
132 })
133 }
134}
135
136#[cfg(test)]
137#[allow(clippy::unwrap_used, clippy::indexing_slicing)]
138mod tests {
139 use super::*;
140
141 #[test]
142 fn test_claims_builder() {
143 let user_id = Uuid::new_v4();
144 let claims = Claims::builder()
145 .user_id(user_id)
146 .role("admin")
147 .role("user")
148 .claim("org_id", serde_json::json!("org-123"))
149 .duration_secs(7200)
150 .build()
151 .unwrap();
152
153 assert_eq!(claims.user_id(), Some(user_id));
154 assert!(claims.has_role("admin"));
155 assert!(claims.has_role("user"));
156 assert!(!claims.has_role("superadmin"));
157 assert_eq!(
158 claims.get_claim("org_id"),
159 Some(&serde_json::json!("org-123"))
160 );
161 assert!(!claims.is_expired());
162 }
163
164 #[test]
165 fn test_claims_expiration() {
166 let claims = Claims {
167 sub: "user-1".to_string(),
168 iat: 0,
169 exp: 1, roles: vec![],
171 custom: HashMap::new(),
172 };
173
174 assert!(claims.is_expired());
175 }
176
177 #[test]
178 fn test_claims_serialization() {
179 let claims = Claims::builder()
180 .subject("user-1")
181 .role("admin")
182 .build()
183 .unwrap();
184
185 let json = serde_json::to_string(&claims).unwrap();
186 let deserialized: Claims = serde_json::from_str(&json).unwrap();
187
188 assert_eq!(deserialized.sub, claims.sub);
189 assert_eq!(deserialized.roles, claims.roles);
190 }
191}