Skip to main content

hinge_rs/client/
auth.rs

1use super::HingeClient;
2use crate::errors::HingeError;
3use crate::models::{HingeAuthToken, LoginTokens, SendbirdAuthToken};
4use crate::storage::Storage;
5use chrono::Utc;
6use serde_json::json;
7
8impl<S: Storage + Clone> HingeClient<S> {
9    async fn ensure_device_registered(&mut self) -> Result<(), HingeError> {
10        if self.installed {
11            return Ok(());
12        }
13
14        let url = format!("{}/identity/install", self.settings.base_url);
15        let body = json!({"installId": self.install_id});
16        let res = self
17            .http_post(&url, &body)
18            .await
19            .map_err(|e| HingeError::Http(format!("Failed to register device: {}", e)))?;
20
21        if !res.status().is_success() {
22            return Err(HingeError::Http(format!(
23                "Device registration failed with status {}",
24                res.status()
25            )));
26        }
27        self.installed = true;
28        Ok(())
29    }
30
31    pub async fn initiate_login(&mut self) -> Result<(), HingeError> {
32        self.ensure_device_registered().await?;
33
34        let url = format!("{}/auth/sms/v2/initiate", self.settings.base_url);
35        let body = json!({"deviceId": self.device_id, "phoneNumber": self.phone_number});
36        let res = self
37            .http_post(&url, &body)
38            .await
39            .map_err(|e| HingeError::Http(format!("Failed to initiate SMS login: {}", e)))?;
40
41        if !res.status().is_success() {
42            return Err(HingeError::Http(format!(
43                "SMS initiation failed with status {}",
44                res.status()
45            )));
46        }
47        Ok(())
48    }
49
50    pub async fn submit_otp(&mut self, otp: &str) -> Result<LoginTokens, HingeError> {
51        let url = format!("{}/auth/sms/v2", self.settings.base_url);
52        let body = json!({
53            "installId": self.install_id,
54            "deviceId": self.device_id,
55            "phoneNumber": self.phone_number,
56            "otp": otp,
57        });
58        let res = self.http_post(&url, &body).await?;
59
60        if res.status() == reqwest::StatusCode::PRECONDITION_FAILED {
61            let v: serde_json::Value = res.json().await?;
62            let case_id = v
63                .get("caseId")
64                .and_then(|v| v.as_str())
65                .unwrap_or("")
66                .to_string();
67            let email = v
68                .get("email")
69                .and_then(|v| v.as_str())
70                .unwrap_or("")
71                .to_string();
72            return Err(HingeError::Email2FA { case_id, email });
73        }
74        if !res.status().is_success() {
75            return Err(HingeError::Http(format!("status {}", res.status())));
76        }
77
78        let v = self.parse_response::<LoginTokens>(res).await?;
79        if let Some(t) = v.hinge_auth_token.clone() {
80            self.hinge_auth = Some(t);
81        }
82        if let Some(t) = v.sendbird_auth_token.clone() {
83            self.sendbird_auth = Some(t);
84        }
85        Ok(v)
86    }
87
88    pub async fn submit_email_code(
89        &mut self,
90        case_id: &str,
91        email_code: &str,
92    ) -> Result<LoginTokens, HingeError> {
93        let url = format!("{}/auth/device/validate", self.settings.base_url);
94        let body = json!({
95            "installId": self.install_id,
96            "code": email_code,
97            "caseId": case_id,
98            "deviceId": self.device_id,
99        });
100        let res = self.http_post(&url, &body).await?;
101        if !res.status().is_success() {
102            return Err(HingeError::Http(format!("status {}", res.status())));
103        }
104
105        let t = self.parse_response::<HingeAuthToken>(res).await?;
106        self.hinge_auth = Some(t);
107        let _ = self.authenticate_with_sendbird().await;
108        Ok(LoginTokens {
109            hinge_auth_token: self.hinge_auth.clone(),
110            sendbird_auth_token: self.sendbird_auth.clone(),
111        })
112    }
113
114    pub(super) async fn authenticate_with_sendbird(&mut self) -> Result<(), HingeError> {
115        let _hinge = self
116            .hinge_auth
117            .as_ref()
118            .ok_or_else(|| HingeError::Auth("hinge token missing".into()))?;
119        let url = format!("{}/message/authenticate", self.settings.base_url);
120        let res = self
121            .http
122            .post(url)
123            .headers(self.default_headers()?)
124            .json(&json!({"refresh": false}))
125            .send()
126            .await?;
127
128        if !res.status().is_success() {
129            return Err(HingeError::Http(format!("status {}", res.status())));
130        }
131        let v = self.parse_response::<SendbirdAuthToken>(res).await?;
132        self.sendbird_auth = Some(v);
133
134        if self.auto_persist
135            && let Some(path) = &self.session_path
136        {
137            let _ = self.save_session(path);
138        }
139        Ok(())
140    }
141
142    pub async fn is_session_valid(&mut self) -> Result<bool, HingeError> {
143        if self.hinge_auth.is_none() {
144            log::warn!("Hinge token is empty, session is invalid.");
145            return Ok(false);
146        }
147
148        if self.sendbird_auth.is_none() {
149            log::warn!("Sendbird JWT is empty, reauthenticating...");
150            if let Err(e) = self.authenticate_with_sendbird().await {
151                log::error!("Failed to reauthenticate with Sendbird: {}", e);
152                return Ok(false);
153            }
154        }
155
156        let now = Utc::now();
157        let hinge_token_valid = self
158            .hinge_auth
159            .as_ref()
160            .is_some_and(|hinge_auth| hinge_auth.expires > now);
161        let sendbird_needs_refresh = self
162            .sendbird_auth
163            .as_ref()
164            .is_none_or(|sb_auth| sb_auth.expires <= now);
165
166        if sendbird_needs_refresh {
167            log::warn!("Sendbird JWT has expired or is missing, reauthenticating...");
168            if let Err(e) = self.authenticate_with_sendbird().await {
169                log::error!("Failed to reauthenticate with Sendbird: {}", e);
170                return Ok(false);
171            }
172        }
173
174        let sendbird_token_valid = self
175            .sendbird_auth
176            .as_ref()
177            .is_some_and(|sb_auth| sb_auth.expires > now);
178        let is_valid = hinge_token_valid && sendbird_token_valid;
179        log::info!(
180            "Session validity check: is_valid={}, hinge_token_valid={}, sendbird_token_valid={}",
181            is_valid,
182            hinge_token_valid,
183            sendbird_token_valid
184        );
185
186        Ok(is_valid)
187    }
188}