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        if let Some(exp) = self.exp {
91            let now = std::time::SystemTime::now()
92                .duration_since(std::time::UNIX_EPOCH)
93                .unwrap()
94                .as_secs() as i64;
95            now >= exp
96        } else {
97            false
98        }
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    #[test]
107    fn test_token_scopes() {
108        let token = TokenIntrospection {
109            active: true,
110            client_id: Some("test".to_string()),
111            token_type: Some("Bearer".to_string()),
112            scope: Some("user:read channel:read".to_string()),
113            exp: Some(9999999999),
114        };
115
116        assert_eq!(token.scopes(), vec!["user:read", "channel:read"]);
117        assert!(token.has_scope("user:read"));
118        assert!(token.has_scope("channel:read"));
119        assert!(!token.has_scope("chat:write"));
120    }
121
122    #[test]
123    fn test_token_expiry() {
124        let expired = TokenIntrospection {
125            active: true,
126            client_id: Some("test".to_string()),
127            token_type: Some("Bearer".to_string()),
128            scope: Some("user:read".to_string()),
129            exp: Some(0), // Expired in 1970!
130        };
131
132        assert!(expired.is_expired());
133
134        let valid = TokenIntrospection {
135            active: true,
136            client_id: Some("test".to_string()),
137            token_type: Some("Bearer".to_string()),
138            scope: Some("user:read".to_string()),
139            exp: Some(9999999999), // Far future
140        };
141
142        assert!(!valid.is_expired());
143    }
144}