Skip to main content

rullst_connect/
user.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4/// Represents a standardized user profile returned from any OAuth2 provider.
5#[derive(Debug, Clone, Serialize, Deserialize)]
6pub struct ConnectUser {
7    /// The unique identifier of the user in the provider's system.
8    pub id: String,
9
10    /// The full name or display name of the user.
11    pub name: String,
12
13    /// The email address of the user, if available and granted.
14    pub email: Option<String>,
15
16    /// Indicates whether the provider has verified the user's email address.
17    pub email_verified: Option<bool>,
18
19    /// The URL to the user's avatar/profile picture, if available.
20    pub avatar_url: Option<String>,
21
22    /// The raw JSON response received from the provider's user endpoint.
23    /// Useful for extracting provider-specific fields not covered by this struct.
24    pub raw_data: Value,
25
26    /// The access token retrieved during the OAuth2 flow.
27    #[serde(with = "secret_serde")]
28    pub access_token: secrecy::SecretString,
29
30    /// The refresh token retrieved during the OAuth2 flow (if provided).
31    #[serde(with = "opt_secret_serde")]
32    pub refresh_token: Option<secrecy::SecretString>,
33
34    /// The token expiration time in seconds from the time it was granted (if provided).
35    pub expires_in: Option<u64>,
36}
37
38mod secret_serde {
39    use secrecy::{ExposeSecret, SecretString};
40    use serde::{Deserialize, Deserializer, Serialize, Serializer};
41
42    pub fn serialize<S>(secret: &SecretString, serializer: S) -> Result<S::Ok, S::Error>
43    where
44        S: Serializer,
45    {
46        secret.expose_secret().serialize(serializer)
47    }
48
49    pub fn deserialize<'de, D>(deserializer: D) -> Result<SecretString, D::Error>
50    where
51        D: Deserializer<'de>,
52    {
53        let s = String::deserialize(deserializer)?;
54        Ok(SecretString::from(s))
55    }
56}
57
58mod opt_secret_serde {
59    use secrecy::{ExposeSecret, SecretString};
60    use serde::{Deserialize, Deserializer, Serialize, Serializer};
61
62    pub fn serialize<S>(secret: &Option<SecretString>, serializer: S) -> Result<S::Ok, S::Error>
63    where
64        S: Serializer,
65    {
66        match secret {
67            Some(s) => s.expose_secret().serialize(serializer),
68            None => serializer.serialize_none(),
69        }
70    }
71
72    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<SecretString>, D::Error>
73    where
74        D: Deserializer<'de>,
75    {
76        let opt = Option::<String>::deserialize(deserializer)?;
77        Ok(opt.map(SecretString::from))
78    }
79}
80
81use async_trait::async_trait;
82
83/// Helper trait to seamlessly integrate `ConnectUser` with databases and ORMs (like SQLx, Diesel, rullst-orm).
84/// By implementing this trait on your custom database User model or repository, you can easily
85/// save or update users directly from the OAuth profile.
86#[async_trait]
87pub trait IntoDatabaseUser<T> {
88    /// Inserts or updates the user in the database based on the OAuth profile.
89    /// Returns the database-specific User model or an error.
90    async fn sync_from_oauth(profile: &ConnectUser) -> Result<T, crate::error::ConnectError>;
91}
92
93/// Represents the response from a device authorization request (RFC 8628).
94#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
95pub struct DeviceAuthorizationResponse {
96    pub device_code: String,
97    pub user_code: String,
98    pub verification_uri: String,
99    pub verification_uri_complete: Option<String>,
100    pub expires_in: u64,
101    pub interval: Option<u64>,
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107    use serde_json::json;
108
109    #[test]
110    fn test_connect_user_serialization() {
111        let user = ConnectUser {
112            id: "123".to_string(),
113            name: "Test User".to_string(),
114            email: Some("test@example.com".to_string()),
115            email_verified: Some(true),
116            avatar_url: Some("https://example.com/avatar.png".to_string()),
117            raw_data: json!({"custom_field": "custom_value"}),
118            access_token: secrecy::SecretString::from("access123".to_string()),
119            refresh_token: Some(secrecy::SecretString::from("refresh123".to_string())),
120            expires_in: Some(3600),
121        };
122
123        let serialized = serde_json::to_string(&user).unwrap();
124        let deserialized: ConnectUser = serde_json::from_str(&serialized).unwrap();
125
126        assert_eq!(user.id, deserialized.id);
127        assert_eq!(user.name, deserialized.name);
128        assert_eq!(user.email, deserialized.email);
129        assert_eq!(user.email_verified, deserialized.email_verified);
130        assert_eq!(user.avatar_url, deserialized.avatar_url);
131        assert_eq!(user.raw_data, deserialized.raw_data);
132        use secrecy::ExposeSecret;
133        assert_eq!(
134            user.access_token.expose_secret(),
135            deserialized.access_token.expose_secret()
136        );
137        assert_eq!(
138            user.refresh_token.as_ref().map(|s| s.expose_secret()),
139            deserialized
140                .refresh_token
141                .as_ref()
142                .map(|s| s.expose_secret())
143        );
144        assert_eq!(user.expires_in, deserialized.expires_in);
145    }
146}