1use anyhow::{anyhow, Result};
2use reqwest::Client;
3use serde::Deserialize;
4use std::sync::Arc;
5use tokio::sync::Mutex;
6
7const FIREBASE_WEB_API_KEY: &str = "AIzaSyA17Uwy37irVEQSwz6PIyX3wnkHrDBeleA";
11pub const PROJECT_ID: &str = "sbs-diet-app";
12
13#[derive(Debug, Deserialize)]
14struct RefreshTokenResponse {
15 id_token: String,
16 refresh_token: String,
17 expires_in: String,
18}
19
20#[derive(Debug, Clone)]
21struct CachedToken {
22 id_token: String,
23 expires_at: chrono::DateTime<chrono::Utc>,
24}
25
26#[derive(Clone)]
27pub struct FirebaseAuth {
28 client: Client,
29 refresh_token: Arc<Mutex<String>>,
30 cached_token: Arc<Mutex<Option<CachedToken>>>,
31}
32
33#[derive(Debug, Deserialize)]
34struct SignInResponse {
35 #[serde(rename = "idToken")]
36 id_token: String,
37 #[serde(rename = "refreshToken")]
38 refresh_token: String,
39 #[serde(rename = "expiresIn")]
40 expires_in: String,
41 #[allow(dead_code)]
42 #[serde(rename = "localId")]
43 local_id: String,
44}
45
46impl FirebaseAuth {
47 pub fn new(refresh_token: String) -> Self {
48 Self {
49 client: Client::new(),
50 refresh_token: Arc::new(Mutex::new(refresh_token)),
51 cached_token: Arc::new(Mutex::new(None)),
52 }
53 }
54
55 pub async fn sign_in_with_email(email: &str, password: &str) -> Result<Self> {
57 let client = Client::new();
58 let url = format!(
59 "https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key={}",
60 FIREBASE_WEB_API_KEY
61 );
62
63 let resp = client
64 .post(&url)
65 .header("X-Ios-Bundle-Identifier", "com.sbs.diet")
66 .json(&serde_json::json!({
67 "email": email,
68 "password": password,
69 "returnSecureToken": true
70 }))
71 .send()
72 .await?;
73
74 if !resp.status().is_success() {
75 let status = resp.status();
76 let body = resp.text().await.unwrap_or_default();
77 return Err(anyhow!("Sign-in failed: {} - {}", status, body));
78 }
79
80 let sign_in: SignInResponse = resp.json().await?;
81
82 let expires_in: i64 = sign_in.expires_in.parse().unwrap_or(3600);
83 let expires_at = chrono::Utc::now() + chrono::Duration::seconds(expires_in);
84
85 Ok(Self {
86 client,
87 refresh_token: Arc::new(Mutex::new(sign_in.refresh_token)),
88 cached_token: Arc::new(Mutex::new(Some(CachedToken {
89 id_token: sign_in.id_token,
90 expires_at,
91 }))),
92 })
93 }
94
95 pub async fn get_id_token(&self) -> Result<String> {
96 {
98 let cached = self.cached_token.lock().await;
99 if let Some(ref token) = *cached {
100 if token.expires_at > chrono::Utc::now() + chrono::Duration::seconds(60) {
101 return Ok(token.id_token.clone());
102 }
103 }
104 }
105
106 self.refresh_id_token().await
107 }
108
109 async fn refresh_id_token(&self) -> Result<String> {
110 let refresh_token = self.refresh_token.lock().await.clone();
111
112 let url = format!(
113 "https://securetoken.googleapis.com/v1/token?key={}",
114 FIREBASE_WEB_API_KEY
115 );
116
117 let resp = self
118 .client
119 .post(&url)
120 .header("X-Ios-Bundle-Identifier", "com.sbs.diet")
121 .form(&[
122 ("grant_type", "refresh_token"),
123 ("refresh_token", &refresh_token),
124 ])
125 .send()
126 .await?;
127
128 if !resp.status().is_success() {
129 let status = resp.status();
130 let body = resp.text().await.unwrap_or_default();
131 return Err(anyhow!(
132 "Failed to refresh token: {} - {}",
133 status,
134 body
135 ));
136 }
137
138 let token_resp: RefreshTokenResponse = resp.json().await?;
139
140 let expires_in: i64 = token_resp
141 .expires_in
142 .parse()
143 .unwrap_or(3600);
144 let expires_at = chrono::Utc::now() + chrono::Duration::seconds(expires_in);
145
146 *self.refresh_token.lock().await = token_resp.refresh_token;
148
149 let id_token = token_resp.id_token.clone();
151 *self.cached_token.lock().await = Some(CachedToken {
152 id_token: token_resp.id_token,
153 expires_at,
154 });
155
156 Ok(id_token)
157 }
158
159 pub async fn get_user_id(&self) -> Result<String> {
160 let token = self.get_id_token().await?;
161 let parts: Vec<&str> = token.split('.').collect();
163 if parts.len() != 3 {
164 return Err(anyhow!("Invalid JWT format"));
165 }
166
167 let payload = parts[1];
169 let padded = match payload.len() % 4 {
170 2 => format!("{}==", payload),
171 3 => format!("{}=", payload),
172 _ => payload.to_string(),
173 };
174
175 let decoded = base64_decode(&padded)?;
176 let claims: serde_json::Value = serde_json::from_slice(&decoded)?;
177 claims["user_id"]
178 .as_str()
179 .or_else(|| claims["sub"].as_str())
180 .map(|s| s.to_string())
181 .ok_or_else(|| anyhow!("No user_id or sub claim in token"))
182 }
183}
184
185fn base64_decode(input: &str) -> Result<Vec<u8>> {
186 let input = input.replace('-', "+").replace('_', "/");
188 let mut result = Vec::new();
189 let chars: Vec<u8> = input.bytes().collect();
190
191 let decode_char = |c: u8| -> Result<u8> {
192 match c {
193 b'A'..=b'Z' => Ok(c - b'A'),
194 b'a'..=b'z' => Ok(c - b'a' + 26),
195 b'0'..=b'9' => Ok(c - b'0' + 52),
196 b'+' => Ok(62),
197 b'/' => Ok(63),
198 b'=' => Ok(0),
199 _ => Err(anyhow!("Invalid base64 character: {}", c as char)),
200 }
201 };
202
203 for chunk in chars.chunks(4) {
204 if chunk.len() < 4 {
205 break;
206 }
207 let b0 = decode_char(chunk[0])?;
208 let b1 = decode_char(chunk[1])?;
209 let b2 = decode_char(chunk[2])?;
210 let b3 = decode_char(chunk[3])?;
211
212 result.push((b0 << 2) | (b1 >> 4));
213 if chunk[2] != b'=' {
214 result.push((b1 << 4) | (b2 >> 2));
215 }
216 if chunk[3] != b'=' {
217 result.push((b2 << 6) | b3);
218 }
219 }
220
221 Ok(result)
222}