|
| 1 | +use std::cell::RefCell; |
| 2 | + |
| 3 | +use cached::Cached; |
| 4 | +use openssl::base64; |
| 5 | +use openssl::rsa::Padding; |
| 6 | +use openssl::x509::X509; |
| 7 | +use reqwest::Client as HttpClient; |
| 8 | + |
| 9 | +use crate::auth::AUTH; |
1 | 10 | use crate::environment::ApiEnvironment; |
2 | 11 | use crate::services::{ |
3 | 12 | AccountBalanceBuilder, B2bBuilder, B2cBuilder, BulkInvoiceBuilder, C2bRegisterBuilder, |
4 | 13 | C2bSimulateBuilder, CancelInvoiceBuilder, MpesaExpressRequestBuilder, OnboardBuilder, |
5 | 14 | OnboardModifyBuilder, ReconciliationBuilder, SingleInvoiceBuilder, TransactionReversalBuilder, |
6 | 15 | TransactionStatusBuilder, |
7 | 16 | }; |
8 | | -use crate::{ApiError, MpesaError}; |
9 | | -use openssl::base64; |
10 | | -use openssl::rsa::Padding; |
11 | | -use openssl::x509::X509; |
12 | | -use reqwest::Client as HttpClient; |
| 17 | +use crate::{auth, MpesaResult}; |
13 | 18 | use secrecy::{ExposeSecret, Secret}; |
14 | | -use serde_json::Value; |
15 | | -use std::cell::RefCell; |
16 | 19 |
|
17 | 20 | /// Source: [test credentials](https://developer.safaricom.co.ke/test_credentials) |
18 | 21 | const DEFAULT_INITIATOR_PASSWORD: &str = "Safcom496!"; |
19 | 22 | /// Get current package version from metadata |
20 | 23 | const CARGO_PACKAGE_VERSION: &str = env!("CARGO_PKG_VERSION"); |
21 | 24 |
|
22 | | -/// `Result` enum type alias |
23 | | -pub type MpesaResult<T> = Result<T, MpesaError>; |
24 | | - |
25 | 25 | /// Mpesa client that will facilitate communication with the Safaricom API |
26 | 26 | #[derive(Clone, Debug)] |
27 | 27 | pub struct Mpesa<Env: ApiEnvironment> { |
@@ -72,6 +72,16 @@ impl<'mpesa, Env: ApiEnvironment> Mpesa<Env> { |
72 | 72 | p.expose_secret().into() |
73 | 73 | } |
74 | 74 |
|
| 75 | + /// Get the client key |
| 76 | + pub(crate) fn client_key(&self) -> &str { |
| 77 | + &self.client_key |
| 78 | + } |
| 79 | + |
| 80 | + /// Get the client secret |
| 81 | + pub(crate) fn client_secret(&self) -> &str { |
| 82 | + self.client_secret.expose_secret() |
| 83 | + } |
| 84 | + |
75 | 85 | /// Optional in development but required for production, you will need to call this method and set your production initiator password. |
76 | 86 | /// If in development, default initiator password is already pre-set |
77 | 87 | /// ```ignore |
@@ -107,31 +117,27 @@ impl<'mpesa, Env: ApiEnvironment> Mpesa<Env> { |
107 | 117 | /// # Errors |
108 | 118 | /// Returns a `MpesaError` on failure |
109 | 119 | pub(crate) async fn auth(&self) -> MpesaResult<String> { |
110 | | - let url = format!( |
111 | | - "{}/oauth/v1/generate?grant_type=client_credentials", |
112 | | - self.environment.base_url() |
113 | | - ); |
114 | | - let response = self |
115 | | - .http_client |
116 | | - .get(&url) |
117 | | - .basic_auth(&self.client_key, Some(&self.client_secret.expose_secret())) |
118 | | - .send() |
119 | | - .await?; |
120 | | - if response.status().is_success() { |
121 | | - let value = response.json::<Value>().await?; |
122 | | - let access_token = value |
123 | | - .get("access_token") |
124 | | - .ok_or_else(|| String::from("Failed to extract token from the response")) |
125 | | - .unwrap(); |
126 | | - let access_token = access_token |
127 | | - .as_str() |
128 | | - .ok_or_else(|| String::from("Error converting access token to string")) |
129 | | - .unwrap(); |
130 | | - |
131 | | - return Ok(access_token.to_string()); |
| 120 | + if let Some(token) = AUTH.lock().await.cache_get(&self.client_key) { |
| 121 | + return Ok(token.to_owned()); |
132 | 122 | } |
133 | | - let error = response.json::<ApiError>().await?; |
134 | | - Err(MpesaError::AuthenticationError(error)) |
| 123 | + |
| 124 | + // Generate a new access token |
| 125 | + let new_token = match auth::auth_prime_cache(self).await { |
| 126 | + Ok(token) => token, |
| 127 | + Err(e) => return Err(e), |
| 128 | + }; |
| 129 | + |
| 130 | + // Double-check if the access token is cached by another thread |
| 131 | + if let Some(token) = AUTH.lock().await.cache_get(&self.client_key) { |
| 132 | + return Ok(token.to_owned()); |
| 133 | + } |
| 134 | + |
| 135 | + // Cache the new token |
| 136 | + AUTH.lock() |
| 137 | + .await |
| 138 | + .cache_set(self.client_key.clone(), new_token.to_owned()); |
| 139 | + |
| 140 | + Ok(new_token) |
135 | 141 | } |
136 | 142 |
|
137 | 143 | /// **B2C Builder** |
@@ -529,9 +535,8 @@ impl<'mpesa, Env: ApiEnvironment> Mpesa<Env> { |
529 | 535 |
|
530 | 536 | #[cfg(test)] |
531 | 537 | mod tests { |
532 | | - use crate::Sandbox; |
533 | | - |
534 | 538 | use super::*; |
| 539 | + use crate::Sandbox; |
535 | 540 |
|
536 | 541 | #[test] |
537 | 542 | fn test_setting_initator_password() { |
|
0 commit comments