firebase-sdk-rs 0.1.0

A simple client for firebase
Documentation
use crate::service_account_key::ServiceAccountKey;
use anyhow::anyhow;
use chrono::Utc;
use jsonwebtoken::{Algorithm, EncodingKey, Header, encode};
use reqwest::StatusCode;
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::sync::Arc;
use std::sync::atomic::{AtomicI64, Ordering};
use tokio::sync::RwLock;

pub struct FirebaseClient {
    pub service_account_key: ServiceAccountKey,
    pub reqwest_client: reqwest::Client,
    pub token_response: Arc<RwLock<Option<TokenResponse>>>,
    pub token_expiry: AtomicI64,
}

#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct TokenResponse {
    pub access_token: String,
    pub expires_in: i64,
    pub token_type: String,
}

impl FirebaseClient {
    pub fn new_from_file(path: &str) -> anyhow::Result<Self> {
        let service_account_key = ServiceAccountKey::from_file(path)?;
        let reqwest_client = reqwest::Client::new();
        Ok(Self {
            service_account_key,
            reqwest_client,
            token_response: Arc::new(RwLock::new(None)),
            token_expiry: AtomicI64::new(0),
        })
    }

    pub fn new_from_env(app_env: Option<String>) -> anyhow::Result<Self> {
        let service_account_key = ServiceAccountKey::from_env(app_env)?;
        let reqwest_client = reqwest::Client::new();
        Ok(Self {
            service_account_key,
            reqwest_client,
            token_response: Arc::new(RwLock::new(None)),
            token_expiry: AtomicI64::new(0),
        })
    }

    pub fn create_jwt(&self, scopes: &str) -> anyhow::Result<String> {
        let now = Utc::now().timestamp();
        let claims = json!({
            "iss": self.service_account_key.client_email,
            "scope": scopes,
            "aud": self.service_account_key.token_uri,
            "iat": now,
            "exp": now + 3600,
        });

        let mut header = Header::new(Algorithm::RS256);
        header.kid = Some(self.service_account_key.private_key_id.clone());
        header.typ = Some("JWT".to_string());
        let encoding_key =
            EncodingKey::from_rsa_pem(self.service_account_key.private_key.as_bytes())?;
        let jwt = encode(&header, &claims, &encoding_key)?;
        self.token_expiry.store(now + 3600, Ordering::Relaxed);
        Ok(jwt)
    }

    pub async fn get_access_token(&self, scopes: &str) -> anyhow::Result<()> {
        let jwt = self.create_jwt(scopes)?;
        let params = [
            ("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer"),
            ("assertion", &jwt),
        ];
        let res = self
            .reqwest_client
            .post(&self.service_account_key.token_uri)
            .form(&params)
            .send()
            .await?
            .json::<TokenResponse>()
            .await?;
        let mut token_response = self.token_response.write().await;
        *token_response = Some(res);
        Ok(())
    }

    pub async fn send_message(&self, message: &impl Serialize) -> anyhow::Result<StatusCode> {
        let now = Utc::now().timestamp();
        if now > self.token_expiry.load(Ordering::Relaxed) {
            let scopes = "https://www.googleapis.com/auth/firebase.messaging";
            self.get_access_token(scopes).await?;
        }

        let token_response = self
            .token_response
            .read()
            .await
            .clone()
            .ok_or(anyhow!("No token response"))?;

        let url = format!(
            "https://fcm.googleapis.com/v1/projects/{}/messages:send",
            self.service_account_key.project_id
        );

        let res = self
            .reqwest_client
            .post(&url)
            .bearer_auth(token_response.access_token)
            .json(&json!({"message": message}))
            .send()
            .await?;
        println!("res {:?}", res);
        Ok(res.status())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::message::{Notification, TopicMessage};
    #[tokio::test]
    pub async fn token_test() {
        let firebase_client = FirebaseClient::new_from_env(None).unwrap();
        let scopes = "https://www.googleapis.com/auth/firebase.messaging";
        let token_response = firebase_client.get_access_token(&scopes).await;
        assert!(token_response.is_ok());
    }

    #[tokio::test]
    pub async fn topic_message_test() {
        let firebase_client = FirebaseClient::new_from_env(None).unwrap();
        let message = TopicMessage {
            topic: "test".to_string(),
            notification: Some(Notification {
                title: "title".to_string(),
                body: "body".to_string(),
            }),
            data: None,
        };
        let message_response = firebase_client.send_message(&message).await;
        assert!(message_response.is_ok());
    }
}