galoy-cli 0.1.7

A CLI Client for interacting with Galoy Backend
Documentation
use graphql_client::reqwest::post_graphql;
use reqwest::{Client, StatusCode};
use serde_json::{json, Value};

use crate::client::{
    errors::{api_error::ApiError, ClientError},
    queries::{
        captcha_create_challenge, user_login, CaptchaChallenge, CaptchaCreateChallenge, UserLogin,
        UserLoginInput,
    },
    GaloyClient,
};

pub struct LoginResponse {
    pub auth_token: String,
    pub totp_required: bool,
}

impl GaloyClient {
    pub async fn user_login_phone(
        &self,
        phone: String,
        code: String,
    ) -> Result<LoginResponse, ClientError> {
        let input = UserLoginInput { phone, code };

        let variables = user_login::Variables { input };

        let response_body =
            post_graphql::<UserLogin, _>(&self.graphql_client, &self.api, variables)
                .await
                .map_err(|err| ApiError::IssueGettingResponse(anyhow::Error::new(err)))?;

        let response_data = response_body.data.ok_or(ApiError::IssueParsingResponse)?;

        let login_result = response_data.user_login;

        if let (Some(auth_token), Some(totp_required)) =
            (login_result.auth_token, login_result.totp_required)
        {
            Ok(LoginResponse {
                auth_token,
                totp_required,
            })
        } else {
            let error_string: String = login_result
                .errors
                .iter()
                .map(|error| format!("{:?}", error))
                .collect::<Vec<String>>()
                .join(", ");

            Err(ClientError::ApiError(ApiError::RequestFailedWithError(
                error_string,
            )))
        }
    }

    pub async fn user_login_email(
        &self,
        email_login_id: String,
        code: String,
    ) -> Result<LoginResponse, ClientError> {
        let endpoint = self.api.trim_end_matches("/graphql");
        let url = format!("{}/auth/email/login", endpoint);
        let request_body = json!({ "code": code, "emailLoginId": email_login_id });

        let response = reqwest::Client::new()
            .post(&url)
            .json(&request_body)
            .send()
            .await
            .map_err(|err| ApiError::IssueGettingResponse(anyhow::Error::new(err)))?;

        let response_json: Value = response
            .json()
            .await
            .map_err(|_| ApiError::IssueParsingResponse)?;

        let auth_token = response_json["result"]["authToken"]
            .as_str()
            .ok_or(ApiError::IssueParsingResponse)?
            .to_string();

        let totp_required = response_json["result"]["totpRequired"]
            .as_bool()
            .ok_or(ApiError::IssueParsingResponse)?;

        Ok(LoginResponse {
            auth_token,
            totp_required,
        })
    }

    pub async fn create_captcha_challenge(&self) -> Result<CaptchaChallenge, ClientError> {
        let client = Client::builder().build().expect("Can't build client");
        let variables = captcha_create_challenge::Variables;
        let response_body =
            post_graphql::<CaptchaCreateChallenge, _>(&client, &self.api.clone(), variables)
                .await
                .map_err(|err| ApiError::IssueGettingResponse(anyhow::Error::new(err)))?;
        let response_data = response_body.data.ok_or(ApiError::IssueParsingResponse)?;
        let captcha_challenge_result = CaptchaChallenge::try_from(response_data)
            .map_err(|err| ApiError::IssueGettingResponse(anyhow::Error::new(err)))?;
        Ok(captcha_challenge_result)
    }

    pub async fn request_email_code(&self, email: String) -> Result<String, ClientError> {
        let endpoint = self.api.trim_end_matches("/graphql");
        let url = format!("{}/auth/email/code", endpoint);
        let request_body = json!({ "email": email });

        let response = reqwest::Client::new()
            .post(&url)
            .json(&request_body)
            .send()
            .await
            .map_err(|err| ApiError::IssueGettingResponse(anyhow::Error::new(err)))?;

        let response_json = response
            .json::<serde_json::Value>()
            .await
            .map_err(|_| ApiError::IssueParsingResponse)?;

        let email_login_id = response_json
            .get("result")
            .and_then(|r| r.as_str())
            .ok_or(ApiError::IssueParsingResponse)?;
        Ok(email_login_id.to_string())
    }

    pub async fn validate_totp_code(
        &self,
        auth_token: String,
        totp_code: String,
    ) -> Result<bool, ClientError> {
        let endpoint = self.api.trim_end_matches("/graphql");
        let url = format!("{}/auth/totp/validate", endpoint);
        let request_body = json!({ "totpCode": totp_code, "authToken": auth_token });

        let response = Client::new().post(&url).json(&request_body).send().await;

        match response {
            Ok(resp) => match resp.status() {
                StatusCode::OK => Ok(true),
                StatusCode::UNPROCESSABLE_ENTITY => Ok(false),
                StatusCode::INTERNAL_SERVER_ERROR => {
                    let error_details = resp
                        .text()
                        .await
                        .unwrap_or_else(|_| "Unknown error".to_string());
                    if error_details.contains("Request failed with status code 400") {
                        return Ok(false);
                    }
                    Err(ClientError::ApiError(ApiError::RequestFailedWithError(
                        error_details,
                    )))
                }
                _ => {
                    let status = resp.status();
                    Err(ClientError::ApiError(ApiError::RequestFailedWithError(
                        format!("Unexpected status code: {}", status),
                    )))
                }
            },
            Err(e) => {
                eprintln!("Network or other error: {}", e);
                Err(ClientError::ApiError(ApiError::IssueGettingResponse(
                    anyhow::Error::new(e),
                )))
            }
        }
    }
}