use base64::Engine;
use reqwest::header::{self, HeaderMap};
use serde::Deserialize;
use std::time::Duration;
use std::time::Instant;
use crate::{
endpoint::Endpoint,
errors::{PaypalError, ResponseError},
AuthAssertionClaims, HeaderParams, LIVE_ENDPOINT, SANDBOX_ENDPOINT,
};
#[derive(Debug, Deserialize, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct AccessToken {
pub scope: String,
pub access_token: String,
pub token_type: String,
pub app_id: String,
pub expires_in: u64,
pub nonce: String,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Auth {
pub client_id: String,
pub secret: String,
pub access_token: Option<AccessToken>,
pub expires: Option<(Instant, Duration)>,
}
#[derive(Debug, Clone)]
pub struct Client {
pub(crate) client: reqwest::Client,
pub env: PaypalEnv,
pub auth: Auth,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum PaypalEnv {
Live,
Sandbox,
Mock(String),
}
impl PaypalEnv {
pub fn endpoint(&self) -> &str {
match &self {
PaypalEnv::Live => LIVE_ENDPOINT,
PaypalEnv::Sandbox => SANDBOX_ENDPOINT,
PaypalEnv::Mock(endpoint) => endpoint.as_str(),
}
}
pub fn make_url(&self, target: &str) -> String {
assert!(target.starts_with('/'), "target path must start with '/'");
format!("{}{}", self.endpoint(), target)
}
}
impl Client {
pub fn new(client_id: String, secret: String, env: PaypalEnv) -> Client {
Client {
client: reqwest::Client::new(),
env,
auth: Auth {
client_id,
secret,
access_token: None,
expires: None,
},
}
}
async fn setup_headers(
&self,
builder: reqwest::RequestBuilder,
header_params: HeaderParams,
) -> Result<reqwest::RequestBuilder, ResponseError> {
let mut headers = HeaderMap::new();
headers.append(header::ACCEPT, "application/json".parse().unwrap());
if let Some(token) = &self.auth.access_token {
headers.append(
header::AUTHORIZATION,
format!("Bearer {}", token.access_token).parse().unwrap(),
);
}
if let Some(merchant_payer_id) = header_params.merchant_payer_id {
let claims = AuthAssertionClaims {
iss: self.auth.client_id.clone(),
payer_id: merchant_payer_id,
};
let jwt_header = jsonwebtoken::Header::new(jsonwebtoken::Algorithm::HS256);
let token = jsonwebtoken::encode(
&jwt_header,
&claims,
&jsonwebtoken::EncodingKey::from_secret(self.auth.secret.as_ref()),
)
.unwrap();
let encoded_token = base64::engine::general_purpose::STANDARD_NO_PAD.encode(token);
headers.append("PayPal-Auth-Assertion", encoded_token.parse().unwrap());
}
if let Some(client_metadata_id) = header_params.client_metadata_id {
headers.append("PayPal-Client-Metadata-Id", client_metadata_id.parse().unwrap());
}
if let Some(partner_attribution_id) = header_params.partner_attribution_id {
headers.append("PayPal-Partner-Attribution-Id", partner_attribution_id.parse().unwrap());
}
if let Some(request_id) = header_params.request_id {
headers.append("PayPal-Request-Id", request_id.parse().unwrap());
}
headers.append("Prefer", "return=representation".parse().unwrap());
if let Some(content_type) = header_params.content_type {
headers.append(header::CONTENT_TYPE, content_type.parse().unwrap());
}
Ok(builder.headers(headers))
}
pub async fn get_access_token(&mut self) -> Result<(), ResponseError> {
if !self.access_token_expired() {
return Ok(());
}
let res = self
.client
.post(self.env.make_url("/v1/oauth2/token"))
.basic_auth(&self.auth.client_id, Some(&self.auth.secret))
.header("Content-Type", "x-www-form-urlencoded")
.header("Accept", "application/json")
.body("grant_type=client_credentials")
.send()
.await
.map_err(ResponseError::HttpError)?;
if res.status().is_success() {
let token = res.json::<AccessToken>().await.map_err(ResponseError::HttpError)?;
self.auth.expires = Some((Instant::now(), Duration::new(token.expires_in, 0)));
self.auth.access_token = Some(token);
Ok(())
} else {
Err(ResponseError::ApiError(
res.json::<PaypalError>().await.map_err(ResponseError::HttpError)?,
))
}
}
pub fn access_token_expired(&self) -> bool {
if let Some(expires) = self.auth.expires {
expires.0.elapsed() >= expires.1
} else {
true
}
}
pub async fn execute_ext<E>(&self, endpoint: &E, headers: HeaderParams) -> Result<E::Response, ResponseError>
where
E: Endpoint,
{
let mut url = self.env.make_url(&endpoint.relative_path());
if let Some(query) = endpoint.query() {
let query_string = serde_qs::to_string(&query).expect("serialize the query correctly");
url.push_str(&query_string);
}
let mut request = self.client.request(endpoint.method(), url);
request = self.setup_headers(request, headers).await?;
if let Some(body) = endpoint.body() {
request = request.json(&body);
}
let res = request.send().await?;
if res.status().is_success() {
let response_body = res.json::<E::Response>().await?;
Ok(response_body)
} else {
Err(ResponseError::ApiError(res.json::<PaypalError>().await?))
}
}
pub async fn execute<E>(&self, endpoint: &E) -> Result<E::Response, ResponseError>
where
E: Endpoint,
{
self.execute_ext(endpoint, HeaderParams::default()).await
}
}