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)]
137mod tests {
138 use super::*;
139
140 #[test]
141 fn test_claims_builder() {
142 let user_id = Uuid::new_v4();
143 let claims = Claims::builder()
144 .user_id(user_id)
145 .role("admin")
146 .role("user")
147 .claim("org_id", serde_json::json!("org-123"))
148 .duration_secs(7200)
149 .build()
150 .unwrap();
151
152 assert_eq!(claims.user_id(), Some(user_id));
153 assert!(claims.has_role("admin"));
154 assert!(claims.has_role("user"));
155 assert!(!claims.has_role("superadmin"));
156 assert_eq!(
157 claims.get_claim("org_id"),
158 Some(&serde_json::json!("org-123"))
159 );
160 assert!(!claims.is_expired());
161 }
162
163 #[test]
164 fn test_claims_expiration() {
165 let claims = Claims {
166 sub: "user-1".to_string(),
167 iat: 0,
168 exp: 1, roles: vec![],
170 custom: HashMap::new(),
171 };
172
173 assert!(claims.is_expired());
174 }
175
176 #[test]
177 fn test_claims_serialization() {
178 let claims = Claims::builder()
179 .subject("user-1")
180 .role("admin")
181 .build()
182 .unwrap();
183
184 let json = serde_json::to_string(&claims).unwrap();
185 let deserialized: Claims = serde_json::from_str(&json).unwrap();
186
187 assert_eq!(deserialized.sub, claims.sub);
188 assert_eq!(deserialized.roles, claims.roles);
189 }
190}