1use std::time::{Duration, Instant};
2
3use reqwest::Client;
4use serde::Deserialize;
5use tracing::{debug, info};
6
7use crate::error::{Result, RointeError};
8
9const API_KEY: &str = "AIzaSyBi1DFJlBr9Cezf2BwfaT-PRPYmi3X3pdA";
16
17const VERIFY_PASSWORD_URL: &str =
19 "https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword";
20
21const REFRESH_TOKEN_URL: &str = "https://securetoken.googleapis.com/v1/token";
23
24const TOKEN_REFRESH_BUFFER: Duration = Duration::from_secs(300);
27
28#[derive(Debug, Deserialize)]
29struct LoginResponse {
30 #[serde(rename = "idToken")]
31 id_token: String,
32 #[serde(rename = "refreshToken")]
33 refresh_token: String,
34 #[serde(rename = "expiresIn")]
35 expires_in: String,
36 #[serde(rename = "localId")]
37 local_id: String,
38}
39
40#[derive(Debug, Deserialize)]
41struct RefreshResponse {
42 id_token: String,
43 refresh_token: String,
44 expires_in: String,
45}
46
47#[derive(Debug, Clone)]
54pub struct FirebaseAuth {
55 pub id_token: String,
57 pub refresh_token: String,
59 pub local_id: String,
61 expires_at: Instant,
62 client: Client,
63}
64
65impl FirebaseAuth {
66 pub async fn login(client: Client, email: &str, password: &str) -> Result<Self> {
71 let url = format!("{}?key={}", VERIFY_PASSWORD_URL, API_KEY);
72 let params = [
73 ("email", email),
74 ("password", password),
75 ("returnSecureToken", "true"),
76 ];
77
78 let resp = client
79 .post(&url)
80 .form(¶ms)
81 .send()
82 .await
83 .map_err(RointeError::Network)?;
84
85 if !resp.status().is_success() {
86 let body = resp.text().await.unwrap_or_default();
87 return Err(RointeError::Auth(format!("Login failed: {body}")));
88 }
89
90 let login: LoginResponse = resp.json().await.map_err(RointeError::Network)?;
91 let expires_in: u64 = login.expires_in.parse().unwrap_or(3600);
92 let expires_at = Instant::now() + Duration::from_secs(expires_in);
93
94 info!("Authenticated as localId={}", login.local_id);
95
96 Ok(Self {
97 id_token: login.id_token,
98 refresh_token: login.refresh_token,
99 local_id: login.local_id,
100 expires_at,
101 client,
102 })
103 }
104
105 pub async fn refresh(&mut self) -> Result<()> {
110 let url = format!("{}?key={}", REFRESH_TOKEN_URL, API_KEY);
111 let params = [
112 ("grant_type", "refresh_token"),
113 ("refresh_token", self.refresh_token.as_str()),
114 ];
115
116 let resp = self
117 .client
118 .post(&url)
119 .form(¶ms)
120 .send()
121 .await
122 .map_err(RointeError::Network)?;
123
124 if !resp.status().is_success() {
125 let body = resp.text().await.unwrap_or_default();
126 return Err(RointeError::Auth(format!("Token refresh failed: {body}")));
127 }
128
129 let refreshed: RefreshResponse = resp.json().await.map_err(RointeError::Network)?;
130 let expires_in: u64 = refreshed.expires_in.parse().unwrap_or(3600);
131
132 self.id_token = refreshed.id_token;
133 self.refresh_token = refreshed.refresh_token;
134 self.expires_at = Instant::now() + Duration::from_secs(expires_in);
135
136 debug!("Token refreshed, valid for {expires_in}s");
137 Ok(())
138 }
139
140 pub async fn ensure_valid_token(&mut self) -> Result<String> {
145 if Instant::now() + TOKEN_REFRESH_BUFFER >= self.expires_at {
146 self.refresh().await?;
147 }
148 Ok(self.id_token.clone())
149 }
150}