nxthdr 0.2.0

Command line interface for the nxthdr platform
use anyhow::{Context, Result};
use serde::de::DeserializeOwned;
use std::time::{SystemTime, UNIX_EPOCH};

use crate::config;

const DEFAULT_API_BASE_URL: &str = "https://peerlab.nxthdr.dev";

pub struct ApiClient {
    base_url: String,
    client: reqwest::Client,
}

impl ApiClient {
    pub fn new() -> Self {
        let base_url =
            std::env::var("NXTHDR_API_URL").unwrap_or_else(|_| DEFAULT_API_BASE_URL.to_string());

        Self {
            base_url,
            client: reqwest::Client::new(),
        }
    }

    async fn get_valid_token(&self) -> Result<String> {
        let tokens =
            config::load_tokens().context("Not logged in. Please run 'nxthdr login' first.")?;

        let now = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_secs() as i64;

        if tokens.expires_at < now {
            config::delete_tokens()?;
            anyhow::bail!("Access token expired. Please run 'nxthdr login' again.");
        }

        Ok(tokens.access_token)
    }

    pub async fn get<T: DeserializeOwned>(&self, path: &str) -> Result<T> {
        let token = self.get_valid_token().await?;
        let url = format!("{}{}", self.base_url, path);

        tracing::debug!("GET {}", url);

        let response = self
            .client
            .get(&url)
            .bearer_auth(&token)
            .send()
            .await
            .context("Failed to send request")?;

        let status = response.status();

        if !status.is_success() {
            let error_body = response.text().await.unwrap_or_default();
            anyhow::bail!("API request failed with status {}: {}", status, error_body);
        }

        let data = response
            .json::<T>()
            .await
            .context("Failed to parse response")?;

        Ok(data)
    }

    pub async fn post<T: DeserializeOwned, B: serde::Serialize>(
        &self,
        path: &str,
        body: &B,
    ) -> Result<T> {
        let token = self.get_valid_token().await?;
        let url = format!("{}{}", self.base_url, path);

        tracing::debug!("POST {}", url);

        let response = self
            .client
            .post(&url)
            .bearer_auth(&token)
            .json(body)
            .send()
            .await
            .context("Failed to send request")?;

        let status = response.status();

        if !status.is_success() {
            let error_body = response.text().await.unwrap_or_default();
            anyhow::bail!("API request failed with status {}: {}", status, error_body);
        }

        let data = response
            .json::<T>()
            .await
            .context("Failed to parse response")?;

        Ok(data)
    }

    pub async fn delete(&self, path: &str) -> Result<()> {
        let token = self.get_valid_token().await?;
        let url = format!("{}{}", self.base_url, path);

        tracing::debug!("DELETE {}", url);

        let response = self
            .client
            .delete(&url)
            .bearer_auth(&token)
            .send()
            .await
            .context("Failed to send request")?;

        let status = response.status();

        if !status.is_success() {
            let error_body = response.text().await.unwrap_or_default();
            anyhow::bail!("API request failed with status {}: {}", status, error_body);
        }

        Ok(())
    }
}