Skip to main content

kick_api/models/
user.rs

1use serde::{Deserialize, Serialize};
2
3/// User information
4///
5/// Returned when fetching user data via the `/users` endpoint
6///
7/// # Example Response
8/// ```json
9/// {
10///   "user_id": 123456,
11///   "name": "username",
12///   "email": "user@example.com",
13///   "profile_picture": "https://..."
14/// }
15/// ```
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct User {
18    /// Unique user identifier
19    pub user_id: u64,
20
21    /// Username
22    pub name: String,
23
24    /// Email address (only visible to the authenticated user)
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub email: Option<String>,
27
28    /// Profile picture URL
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub profile_picture: Option<String>,
31}
32
33/// Token introspection response
34///
35/// Used to validate OAuth tokens (implements RFC 7662)
36///
37/// # Example Response (Active Token)
38/// ```json
39/// {
40///   "active": true,
41///   "client_id": "01XXXXX",
42///   "token_type": "Bearer",
43///   "scope": "user:read channel:read",
44///   "exp": 1234567890
45/// }
46/// ```
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct TokenIntrospection {
49    /// Whether the token is currently active and valid
50    pub active: bool,
51
52    /// Client ID that issued the token (only if active=true)
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub client_id: Option<String>,
55
56    /// Token type (e.g., "Bearer") (only if active=true)
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub token_type: Option<String>,
59
60    /// Space-separated list of scopes (only if active=true)
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub scope: Option<String>,
63
64    /// Expiration timestamp (Unix epoch) (only if active=true)
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub exp: Option<i64>,
67}
68
69impl TokenIntrospection {
70    /// Check if the token is active
71    pub fn is_active(&self) -> bool {
72        self.active
73    }
74
75    /// Get the scopes as a Vec<String>
76    pub fn scopes(&self) -> Vec<String> {
77        self.scope
78            .as_ref()
79            .map(|s| s.split_whitespace().map(String::from).collect())
80            .unwrap_or_default()
81    }
82
83    /// Check if the token has a specific scope
84    pub fn has_scope(&self, scope: &str) -> bool {
85        self.scopes().iter().any(|s| s == scope)
86    }
87
88    /// Check if the token is expired
89    pub fn is_expired(&self) -> bool {
90        let Some(exp) = self.exp else {
91            return false;
92        };
93        let Ok(duration) = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)
94        else {
95            return false;
96        };
97        duration.as_secs() as i64 >= exp
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    fn test_token_scopes() {
107        let token = TokenIntrospection {
108            active: true,
109            client_id: Some("test".to_string()),
110            token_type: Some("Bearer".to_string()),
111            scope: Some("user:read channel:read".to_string()),
112            exp: Some(9999999999),
113        };
114
115        assert_eq!(token.scopes(), vec!["user:read", "channel:read"]);
116        assert!(token.has_scope("user:read"));
117        assert!(token.has_scope("channel:read"));
118        assert!(!token.has_scope("chat:write"));
119    }
120
121    #[test]
122    fn test_token_expiry() {
123        let expired = TokenIntrospection {
124            active: true,
125            client_id: Some("test".to_string()),
126            token_type: Some("Bearer".to_string()),
127            scope: Some("user:read".to_string()),
128            exp: Some(0), // Expired in 1970!
129        };
130
131        assert!(expired.is_expired());
132
133        let valid = TokenIntrospection {
134            active: true,
135            client_id: Some("test".to_string()),
136            token_type: Some("Bearer".to_string()),
137            scope: Some("user:read".to_string()),
138            exp: Some(9999999999), // Far future
139        };
140
141        assert!(!valid.is_expired());
142    }
143}