use std::time::{Duration, Instant};
use reqwest::Client;
use serde::Deserialize;
use tracing::{debug, info};
use crate::error::{Result, RointeError};
const API_KEY: &str = "AIzaSyBi1DFJlBr9Cezf2BwfaT-PRPYmi3X3pdA";
const VERIFY_PASSWORD_URL: &str =
"https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword";
const REFRESH_TOKEN_URL: &str = "https://securetoken.googleapis.com/v1/token";
const TOKEN_REFRESH_BUFFER: Duration = Duration::from_secs(300);
#[derive(Debug, Deserialize)]
struct LoginResponse {
#[serde(rename = "idToken")]
id_token: String,
#[serde(rename = "refreshToken")]
refresh_token: String,
#[serde(rename = "expiresIn")]
expires_in: String,
#[serde(rename = "localId")]
local_id: String,
}
#[derive(Debug, Deserialize)]
struct RefreshResponse {
id_token: String,
refresh_token: String,
expires_in: String,
}
#[derive(Debug, Clone)]
pub struct FirebaseAuth {
pub id_token: String,
pub refresh_token: String,
pub local_id: String,
expires_at: Instant,
client: Client,
}
impl FirebaseAuth {
pub async fn login(client: Client, email: &str, password: &str) -> Result<Self> {
let url = format!("{}?key={}", VERIFY_PASSWORD_URL, API_KEY);
let params = [
("email", email),
("password", password),
("returnSecureToken", "true"),
];
let resp = client
.post(&url)
.form(¶ms)
.send()
.await
.map_err(RointeError::Network)?;
if !resp.status().is_success() {
let body = resp.text().await.unwrap_or_default();
return Err(RointeError::Auth(format!("Login failed: {body}")));
}
let login: LoginResponse = resp.json().await.map_err(RointeError::Network)?;
let expires_in: u64 = login.expires_in.parse().unwrap_or(3600);
let expires_at = Instant::now() + Duration::from_secs(expires_in);
info!("Authenticated as localId={}", login.local_id);
Ok(Self {
id_token: login.id_token,
refresh_token: login.refresh_token,
local_id: login.local_id,
expires_at,
client,
})
}
pub async fn refresh(&mut self) -> Result<()> {
let url = format!("{}?key={}", REFRESH_TOKEN_URL, API_KEY);
let params = [
("grant_type", "refresh_token"),
("refresh_token", self.refresh_token.as_str()),
];
let resp = self
.client
.post(&url)
.form(¶ms)
.send()
.await
.map_err(RointeError::Network)?;
if !resp.status().is_success() {
let body = resp.text().await.unwrap_or_default();
return Err(RointeError::Auth(format!("Token refresh failed: {body}")));
}
let refreshed: RefreshResponse = resp.json().await.map_err(RointeError::Network)?;
let expires_in: u64 = refreshed.expires_in.parse().unwrap_or(3600);
self.id_token = refreshed.id_token;
self.refresh_token = refreshed.refresh_token;
self.expires_at = Instant::now() + Duration::from_secs(expires_in);
debug!("Token refreshed, valid for {expires_in}s");
Ok(())
}
pub async fn ensure_valid_token(&mut self) -> Result<String> {
if Instant::now() + TOKEN_REFRESH_BUFFER >= self.expires_at {
self.refresh().await?;
}
Ok(self.id_token.clone())
}
}