discovery-connect 1.0.1

Library to upload data to RetinAI Discovery via the public API.
Documentation
// Copyright 2023 Ikerian AG
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use reqwest::Client;
use serde::{Deserialize, Serialize};
use serde_json;

#[derive(Debug, Serialize)]
pub struct AuthPayload {
    pub username: String,
    pub password: String,
    pub grant_type: &'static str,
    pub mfa_code: Option<String>,
}

#[derive(Debug, Deserialize)]
pub struct AuthResponse {
    pub challenge: Option<String>,
    pub access_token: String,
    pub refresh_token: String,
}

/// Retrieves a new access token using a refresh token.
///
/// Sends a request to refresh an access token using the provided refresh token. This function constructs
/// a refresh token payload, sends it to the specified OAuth token endpoint, and handles the server's response.
///
/// # Arguments
///
/// * `client` - A reference to a `Client` object used to send HTTP requests.
/// * `url` - A `String` reference representing the base URL of the OAuth token endpoint.
/// * `refresh_token` - A `String` reference representing the refresh token used to obtain a new access token.
///
/// # Returns
///
/// A `Result` containing either the authentication response data on success (`AuthResponse`),
/// or an `reqwest::Error` on failure. The `AuthResponse` includes the new access token and other relevant information.
///
/// # Errors
///
/// Returns an `Err` of type `reqwest::Error` if the access token request fails, if the server response status
/// is not OK, or if there is an issue with the server's response format.
///
/// # Example
///
/// ```
/// use discovery_connect::auth::{access_token, AuthResponse};
/// use reqwest::Client;
///
/// async fn get_access_token_example() {
///     let client = Client::new();
///     let base_url = "https://api.example.discovery.retinai.com";
///     let refresh_token = "your_refresh_token";
///     let response = access_token(&client, &base_url, &refresh_token).await;
///     match response {
///         Ok(auth_response) => println!("Access token: {}", auth_response.access_token),
///         Err(e) => println!("Error: {}", e),
///     }
/// }
/// ```
pub async fn access_token(
    client: &Client,
    url: &str,
    refresh_token: &str,
) -> Result<AuthResponse, reqwest::Error> {
    let refresh_payload = serde_json::json!({
        "grant_type": "refresh_token",
        "refresh_token": refresh_token
    });

    let url = format!("{}/oauth/token/", url);

    match client.post(&url).json(&refresh_payload).send().await {
        Ok(response) => {
            if response.status() != reqwest::StatusCode::OK {
                let e: reqwest::Error = response.error_for_status().unwrap_err();
                return Err(e);
            }
            let response: AuthResponse = response.json().await.unwrap();
            Ok(response)
        }
        Err(error) => {
            eprintln!("  error: {:?}", error);
            Err(error)
        }
    }
}

/// Authenticates a user against a server and obtains an authentication token.
///
/// Asynchronously authenticates a user using their credentials and optionally a multi-factor authentication (MFA) code.
/// This function builds the authentication request, sends it to the server, and processes the response.
///
/// # Arguments
///
/// * `client` - A reference to a `reqwest::Client` used for making HTTP requests.
/// * `url` - A string slice representing the base URL of the authentication server.
/// * `client_id` - A string slice representing the client identifier.
/// * `client_secret` - A string slice representing the client secret.
/// * `email` - A string slice representing the user's email address.
/// * `password` - A string slice representing the user's password.
/// * `mfa_code` - An optional string slice representing the multi-factor authentication code.
///
/// # Returns
///
/// A `Result` containing either the authentication response data on success (`AuthResponse`),
/// or an `reqwest::Error` on failure.
///
/// # Errors
///
/// Returns an `Err` of type `reqwest::Error` if the authentication request fails or if the server
/// response status is not OK.
///
/// # Example
///
/// ```
/// use discovery_connect::{QueryClient};
/// use discovery_connect::auth::{login, AuthResponse};
/// use std::sync::Arc;
///
/// let qc = Arc::new(QueryClient::new(
///         "https://api.example.discovery.retinai.com",
///         "client_id",
///         "client_secret",
///         "user@example",
///         "password123",
///         std::time::Duration::from_secs(10)));
/// async {
///     let res = login(
///         &qc.client,
///         "https://api.example.discovery.retinai.com",
///         "client_id123",
///         "client_secret456",
///         "user@example.com",
///         "password123",
///         Some("123456")
///     ).await;
///     match res {
///         Ok(auth_response) => println!("Access Token: {:?}", auth_response.access_token),
///         Err(e) => println!("Error: {}", e),
///     }
/// };
/// ```
pub async fn login(
    client: &Client,
    url: &str,
    client_id: &str,
    client_secret: &str,
    email: &str,
    password: &str,
    mfa_code: Option<&str>,
) -> Result<AuthResponse, reqwest::Error> {
    let credentials = format!("{}:{}", client_id, client_secret);
    // let credentials = general_purpose::STANDARD.encode(credentials);
    let credentials = base64_url::encode(&credentials);

    let url = format!("{}/oauth/token/", url);

    let payload = AuthPayload {
        username: email.to_string(),
        password: password.to_string(),
        grant_type: "password",
        mfa_code: mfa_code.map(|s| s.to_string()),
    };

    let c = client
        .post(&url)
        .header("Authorization", format!("Basic {}", credentials))
        .json(&payload);

    match c.send().await {
        Ok(response) => {
            if response.status() != reqwest::StatusCode::OK {
                return Err(response.error_for_status().unwrap_err());
            }
            let response: AuthResponse = response.json().await.unwrap();
            Ok(response)
        }
        Err(error) => Err(error),
    }
}