mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-11-02 04:04:43 +08:00
feat(router): implement API endpoints for managing API keys (#511)
This commit is contained in:
@ -94,6 +94,9 @@ pub enum StripeErrorCode {
|
||||
#[error(error_type = StripeErrorType::InvalidRequestError, code = "resource_missing", message = "No such mandate")]
|
||||
MandateNotFound,
|
||||
|
||||
#[error(error_type = StripeErrorType::InvalidRequestError, code = "resource_missing", message = "No such API key")]
|
||||
ApiKeyNotFound,
|
||||
|
||||
#[error(error_type = StripeErrorType::InvalidRequestError, code = "parameter_missing", message = "Return url is not available")]
|
||||
ReturnUrlUnavailable,
|
||||
|
||||
@ -383,6 +386,7 @@ impl From<errors::ApiErrorResponse> for StripeErrorCode {
|
||||
Self::MerchantConnectorAccountNotFound
|
||||
}
|
||||
errors::ApiErrorResponse::MandateNotFound => Self::MandateNotFound,
|
||||
errors::ApiErrorResponse::ApiKeyNotFound => Self::ApiKeyNotFound,
|
||||
errors::ApiErrorResponse::MandateValidationFailed { reason } => {
|
||||
Self::PaymentIntentMandateInvalid { message: reason }
|
||||
}
|
||||
@ -453,6 +457,7 @@ impl actix_web::ResponseError for StripeErrorCode {
|
||||
| Self::MerchantAccountNotFound
|
||||
| Self::MerchantConnectorAccountNotFound
|
||||
| Self::MandateNotFound
|
||||
| Self::ApiKeyNotFound
|
||||
| Self::DuplicateMerchantAccount
|
||||
| Self::DuplicateMerchantConnectorAccount
|
||||
| Self::DuplicatePaymentMethod
|
||||
|
||||
@ -16,9 +16,10 @@ pub const REQUEST_TIME_OUT: u64 = 30;
|
||||
pub(crate) const NO_ERROR_MESSAGE: &str = "No error message";
|
||||
pub(crate) const NO_ERROR_CODE: &str = "No error code";
|
||||
|
||||
// General purpose base64 engine
|
||||
// General purpose base64 engines
|
||||
pub(crate) const BASE64_ENGINE: base64::engine::GeneralPurpose =
|
||||
base64::engine::general_purpose::STANDARD;
|
||||
|
||||
pub(crate) const BASE64_ENGINE_URL_SAFE: base64::engine::GeneralPurpose =
|
||||
base64::engine::general_purpose::URL_SAFE;
|
||||
|
||||
pub(crate) const API_KEY_LENGTH: usize = 64;
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
pub mod admin;
|
||||
pub mod api_keys;
|
||||
pub mod configs;
|
||||
pub mod customers;
|
||||
pub mod errors;
|
||||
|
||||
@ -7,7 +7,6 @@ use crate::{
|
||||
consts,
|
||||
core::errors::{self, RouterResponse, RouterResult, StorageErrorExt},
|
||||
db::StorageInterface,
|
||||
env::{self, Env},
|
||||
pii::Secret,
|
||||
services::api as service_api,
|
||||
types::{
|
||||
@ -20,12 +19,11 @@ use crate::{
|
||||
|
||||
#[inline]
|
||||
pub fn create_merchant_api_key() -> String {
|
||||
let id = Uuid::new_v4().simple();
|
||||
match env::which() {
|
||||
Env::Development => format!("dev_{id}"),
|
||||
Env::Production => format!("prd_{id}"),
|
||||
Env::Sandbox => format!("snd_{id}"),
|
||||
}
|
||||
format!(
|
||||
"{}_{}",
|
||||
router_env::env::prefix_for_env(),
|
||||
Uuid::new_v4().simple()
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn create_merchant_account(
|
||||
|
||||
219
crates/router/src/core/api_keys.rs
Normal file
219
crates/router/src/core/api_keys.rs
Normal file
@ -0,0 +1,219 @@
|
||||
use common_utils::{date_time, errors::CustomResult, fp_utils};
|
||||
use error_stack::{report, IntoReport, ResultExt};
|
||||
use masking::{PeekInterface, Secret};
|
||||
use router_env::{instrument, tracing};
|
||||
|
||||
use crate::{
|
||||
consts,
|
||||
core::errors::{self, RouterResponse, StorageErrorExt},
|
||||
db::StorageInterface,
|
||||
services::ApplicationResponse,
|
||||
types::{api, storage, transformers::ForeignInto},
|
||||
utils,
|
||||
};
|
||||
|
||||
// Defining new types `PlaintextApiKey` and `HashedApiKey` in the hopes of reducing the possibility
|
||||
// of plaintext API key being stored in the data store.
|
||||
pub struct PlaintextApiKey(Secret<String>);
|
||||
pub struct HashedApiKey(String);
|
||||
|
||||
impl PlaintextApiKey {
|
||||
const HASH_KEY_LEN: usize = 32;
|
||||
|
||||
const PREFIX_LEN: usize = 8;
|
||||
|
||||
pub fn new(length: usize) -> Self {
|
||||
let key = common_utils::crypto::generate_cryptographically_secure_random_string(length);
|
||||
Self(key.into())
|
||||
}
|
||||
|
||||
pub fn new_hash_key() -> [u8; Self::HASH_KEY_LEN] {
|
||||
common_utils::crypto::generate_cryptographically_secure_random_bytes()
|
||||
}
|
||||
|
||||
pub fn new_key_id() -> String {
|
||||
let env = router_env::env::prefix_for_env();
|
||||
utils::generate_id(consts::ID_LENGTH, env)
|
||||
}
|
||||
|
||||
pub fn prefix(&self) -> String {
|
||||
self.0.peek().chars().take(Self::PREFIX_LEN).collect()
|
||||
}
|
||||
|
||||
pub fn peek(&self) -> &str {
|
||||
self.0.peek()
|
||||
}
|
||||
|
||||
pub fn keyed_hash(&self, key: &[u8; Self::HASH_KEY_LEN]) -> HashedApiKey {
|
||||
/*
|
||||
Decisions regarding API key hashing algorithm chosen:
|
||||
|
||||
- Since API key hash verification would be done for each request, there is a requirement
|
||||
for the hashing to be quick.
|
||||
- Password hashing algorithms would not be suitable for this purpose as they're designed to
|
||||
prevent brute force attacks, considering that the same password could be shared across
|
||||
multiple sites by the user.
|
||||
- Moreover, password hash verification happens once per user session, so the delay involved
|
||||
is negligible, considering the security benefits it provides.
|
||||
While with API keys (assuming uniqueness of keys across the application), the delay
|
||||
involved in hashing (with the use of a password hashing algorithm) becomes significant,
|
||||
considering that it must be done per request.
|
||||
- Since we are the only ones generating API keys and are able to guarantee their uniqueness,
|
||||
a simple hash algorithm is sufficient for this purpose.
|
||||
|
||||
Hash algorithms considered:
|
||||
- Password hashing algorithms: Argon2id and PBKDF2
|
||||
- Simple hashing algorithms: HMAC-SHA256, HMAC-SHA512, BLAKE3
|
||||
|
||||
After benchmarking the simple hashing algorithms, we decided to go with the BLAKE3 keyed
|
||||
hashing algorithm, with a randomly generated key for the hash key.
|
||||
*/
|
||||
|
||||
HashedApiKey(
|
||||
blake3::keyed_hash(key, self.0.peek().as_bytes())
|
||||
.to_hex()
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn verify_hash(
|
||||
&self,
|
||||
key: &[u8; Self::HASH_KEY_LEN],
|
||||
stored_api_key: &HashedApiKey,
|
||||
) -> CustomResult<(), errors::ApiKeyError> {
|
||||
// Converting both hashes to `blake3::Hash` since it provides constant-time equality checks
|
||||
let provided_api_key_hash = blake3::keyed_hash(key, self.0.peek().as_bytes());
|
||||
let stored_api_key_hash = blake3::Hash::from_hex(&stored_api_key.0)
|
||||
.into_report()
|
||||
.change_context(errors::ApiKeyError::FailedToReadHashFromHex)?;
|
||||
|
||||
fp_utils::when(provided_api_key_hash != stored_api_key_hash, || {
|
||||
Err(errors::ApiKeyError::HashVerificationFailed).into_report()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn create_api_key(
|
||||
store: &dyn StorageInterface,
|
||||
api_key: api::CreateApiKeyRequest,
|
||||
merchant_id: String,
|
||||
) -> RouterResponse<api::CreateApiKeyResponse> {
|
||||
let hash_key = PlaintextApiKey::new_hash_key();
|
||||
let plaintext_api_key = PlaintextApiKey::new(consts::API_KEY_LENGTH);
|
||||
let api_key = storage::ApiKeyNew {
|
||||
key_id: PlaintextApiKey::new_key_id(),
|
||||
merchant_id,
|
||||
name: api_key.name,
|
||||
description: api_key.description,
|
||||
hash_key: Secret::from(hex::encode(hash_key)),
|
||||
hashed_api_key: plaintext_api_key.keyed_hash(&hash_key).into(),
|
||||
prefix: plaintext_api_key.prefix(),
|
||||
created_at: date_time::now(),
|
||||
expires_at: api_key.expiration.into(),
|
||||
last_used: None,
|
||||
};
|
||||
|
||||
let api_key = store
|
||||
.insert_api_key(api_key)
|
||||
.await
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Failed to insert new API key")?;
|
||||
|
||||
Ok(ApplicationResponse::Json(
|
||||
(api_key, plaintext_api_key).foreign_into(),
|
||||
))
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn retrieve_api_key(
|
||||
store: &dyn StorageInterface,
|
||||
key_id: &str,
|
||||
) -> RouterResponse<api::RetrieveApiKeyResponse> {
|
||||
let api_key = store
|
||||
.find_api_key_optional(key_id)
|
||||
.await
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError) // If retrieve failed
|
||||
.attach_printable("Failed to retrieve new API key")?
|
||||
.ok_or(report!(errors::ApiErrorResponse::ApiKeyNotFound))?; // If retrieve returned `None`
|
||||
|
||||
Ok(ApplicationResponse::Json(api_key.foreign_into()))
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn update_api_key(
|
||||
store: &dyn StorageInterface,
|
||||
key_id: &str,
|
||||
api_key: api::UpdateApiKeyRequest,
|
||||
) -> RouterResponse<api::RetrieveApiKeyResponse> {
|
||||
let api_key = store
|
||||
.update_api_key(key_id.to_owned(), api_key.foreign_into())
|
||||
.await
|
||||
.map_err(|err| err.to_not_found_response(errors::ApiErrorResponse::ApiKeyNotFound))?;
|
||||
|
||||
Ok(ApplicationResponse::Json(api_key.foreign_into()))
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn revoke_api_key(
|
||||
store: &dyn StorageInterface,
|
||||
key_id: &str,
|
||||
) -> RouterResponse<api::RevokeApiKeyResponse> {
|
||||
let revoked = store
|
||||
.revoke_api_key(key_id)
|
||||
.await
|
||||
.map_err(|err| err.to_not_found_response(errors::ApiErrorResponse::ApiKeyNotFound))?;
|
||||
|
||||
Ok(ApplicationResponse::Json(api::RevokeApiKeyResponse {
|
||||
key_id: key_id.to_owned(),
|
||||
revoked,
|
||||
}))
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn list_api_keys(
|
||||
store: &dyn StorageInterface,
|
||||
merchant_id: String,
|
||||
limit: Option<i64>,
|
||||
offset: Option<i64>,
|
||||
) -> RouterResponse<Vec<api::RetrieveApiKeyResponse>> {
|
||||
let api_keys = store
|
||||
.list_api_keys_by_merchant_id(&merchant_id, limit, offset)
|
||||
.await
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Failed to list merchant API keys")?;
|
||||
let api_keys = api_keys
|
||||
.into_iter()
|
||||
.map(ForeignInto::foreign_into)
|
||||
.collect();
|
||||
|
||||
Ok(ApplicationResponse::Json(api_keys))
|
||||
}
|
||||
|
||||
impl From<HashedApiKey> for storage::HashedApiKey {
|
||||
fn from(hashed_api_key: HashedApiKey) -> Self {
|
||||
hashed_api_key.0.into()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::unwrap_used)]
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_hashing_and_verification() {
|
||||
let plaintext_api_key = PlaintextApiKey::new(consts::API_KEY_LENGTH);
|
||||
let hash_key = PlaintextApiKey::new_hash_key();
|
||||
let hashed_api_key = plaintext_api_key.keyed_hash(&hash_key);
|
||||
|
||||
assert_ne!(
|
||||
plaintext_api_key.0.peek().as_bytes(),
|
||||
hashed_api_key.0.as_bytes()
|
||||
);
|
||||
|
||||
plaintext_api_key
|
||||
.verify_hash(&hash_key, &hashed_api_key)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
@ -390,3 +390,11 @@ pub enum WebhooksFlowError {
|
||||
#[error("Webhook not received by merchant")]
|
||||
NotReceivedByMerchant,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ApiKeyError {
|
||||
#[error("Failed to read API key hash from hexadecimal string")]
|
||||
FailedToReadHashFromHex,
|
||||
#[error("Failed to verify provided API key hash against stored API key hash")]
|
||||
HashVerificationFailed,
|
||||
}
|
||||
|
||||
@ -133,6 +133,8 @@ pub enum ApiErrorResponse {
|
||||
ResourceIdNotFound,
|
||||
#[error(error_type = ErrorType::ObjectNotFound, code = "HE_02", message = "Mandate does not exist in our records")]
|
||||
MandateNotFound,
|
||||
#[error(error_type = ErrorType::ObjectNotFound, code = "HE_02", message = "API Key does not exist in our records")]
|
||||
ApiKeyNotFound,
|
||||
#[error(error_type = ErrorType::ValidationError, code = "HE_03", message = "Return URL is not configured and not passed in payments request")]
|
||||
ReturnUrlUnavailable,
|
||||
#[error(error_type = ErrorType::ValidationError, code = "HE_03", message = "This refund is not possible through Hyperswitch. Please raise the refund through {connector} dashboard")]
|
||||
@ -231,7 +233,8 @@ impl actix_web::ResponseError for ApiErrorResponse {
|
||||
| Self::IncorrectConnectorNameGiven
|
||||
| Self::ResourceIdNotFound
|
||||
| Self::ConfigNotFound
|
||||
| Self::AddressNotFound => StatusCode::BAD_REQUEST, // 400
|
||||
| Self::AddressNotFound
|
||||
| Self::ApiKeyNotFound => StatusCode::BAD_REQUEST, // 400
|
||||
Self::DuplicateMerchantAccount
|
||||
| Self::DuplicateMerchantConnectorAccount
|
||||
| Self::DuplicatePaymentMethod
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
pub mod address;
|
||||
pub mod api_keys;
|
||||
pub mod cache;
|
||||
pub mod configs;
|
||||
pub mod connector_response;
|
||||
@ -35,23 +36,24 @@ pub trait StorageInterface:
|
||||
Send
|
||||
+ Sync
|
||||
+ dyn_clone::DynClone
|
||||
+ payment_attempt::PaymentAttemptInterface
|
||||
+ mandate::MandateInterface
|
||||
+ address::AddressInterface
|
||||
+ api_keys::ApiKeyInterface
|
||||
+ configs::ConfigInterface
|
||||
+ connector_response::ConnectorResponseInterface
|
||||
+ customers::CustomerInterface
|
||||
+ ephemeral_key::EphemeralKeyInterface
|
||||
+ events::EventInterface
|
||||
+ merchant_account::MerchantAccountInterface
|
||||
+ merchant_connector_account::MerchantConnectorAccountInterface
|
||||
+ merchant_connector_account::ConnectorAccessToken
|
||||
+ locker_mock_up::LockerMockUpInterface
|
||||
+ mandate::MandateInterface
|
||||
+ merchant_account::MerchantAccountInterface
|
||||
+ merchant_connector_account::ConnectorAccessToken
|
||||
+ merchant_connector_account::MerchantConnectorAccountInterface
|
||||
+ payment_attempt::PaymentAttemptInterface
|
||||
+ payment_intent::PaymentIntentInterface
|
||||
+ payment_method::PaymentMethodInterface
|
||||
+ process_tracker::ProcessTrackerInterface
|
||||
+ refund::RefundInterface
|
||||
+ queue::QueueInterface
|
||||
+ ephemeral_key::EphemeralKeyInterface
|
||||
+ connector_response::ConnectorResponseInterface
|
||||
+ refund::RefundInterface
|
||||
+ reverse_lookup::ReverseLookupInterface
|
||||
+ 'static
|
||||
{
|
||||
|
||||
138
crates/router/src/db/api_keys.rs
Normal file
138
crates/router/src/db/api_keys.rs
Normal file
@ -0,0 +1,138 @@
|
||||
use error_stack::IntoReport;
|
||||
|
||||
use super::{MockDb, Store};
|
||||
use crate::{
|
||||
connection::pg_connection,
|
||||
core::errors::{self, CustomResult},
|
||||
types::storage,
|
||||
};
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait ApiKeyInterface {
|
||||
async fn insert_api_key(
|
||||
&self,
|
||||
api_key: storage::ApiKeyNew,
|
||||
) -> CustomResult<storage::ApiKey, errors::StorageError>;
|
||||
|
||||
async fn update_api_key(
|
||||
&self,
|
||||
key_id: String,
|
||||
api_key: storage::ApiKeyUpdate,
|
||||
) -> CustomResult<storage::ApiKey, errors::StorageError>;
|
||||
|
||||
async fn revoke_api_key(&self, key_id: &str) -> CustomResult<bool, errors::StorageError>;
|
||||
|
||||
async fn find_api_key_optional(
|
||||
&self,
|
||||
key_id: &str,
|
||||
) -> CustomResult<Option<storage::ApiKey>, errors::StorageError>;
|
||||
|
||||
async fn list_api_keys_by_merchant_id(
|
||||
&self,
|
||||
merchant_id: &str,
|
||||
limit: Option<i64>,
|
||||
offset: Option<i64>,
|
||||
) -> CustomResult<Vec<storage::ApiKey>, errors::StorageError>;
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl ApiKeyInterface for Store {
|
||||
async fn insert_api_key(
|
||||
&self,
|
||||
api_key: storage::ApiKeyNew,
|
||||
) -> CustomResult<storage::ApiKey, errors::StorageError> {
|
||||
let conn = pg_connection(&self.master_pool).await;
|
||||
api_key
|
||||
.insert(&conn)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
.into_report()
|
||||
}
|
||||
|
||||
async fn update_api_key(
|
||||
&self,
|
||||
key_id: String,
|
||||
api_key: storage::ApiKeyUpdate,
|
||||
) -> CustomResult<storage::ApiKey, errors::StorageError> {
|
||||
let conn = pg_connection(&self.master_pool).await;
|
||||
storage::ApiKey::update_by_key_id(&conn, key_id, api_key)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
.into_report()
|
||||
}
|
||||
|
||||
async fn revoke_api_key(&self, key_id: &str) -> CustomResult<bool, errors::StorageError> {
|
||||
let conn = pg_connection(&self.master_pool).await;
|
||||
storage::ApiKey::revoke_by_key_id(&conn, key_id)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
.into_report()
|
||||
}
|
||||
|
||||
async fn find_api_key_optional(
|
||||
&self,
|
||||
key_id: &str,
|
||||
) -> CustomResult<Option<storage::ApiKey>, errors::StorageError> {
|
||||
let conn = pg_connection(&self.master_pool).await;
|
||||
storage::ApiKey::find_optional_by_key_id(&conn, key_id)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
.into_report()
|
||||
}
|
||||
|
||||
async fn list_api_keys_by_merchant_id(
|
||||
&self,
|
||||
merchant_id: &str,
|
||||
limit: Option<i64>,
|
||||
offset: Option<i64>,
|
||||
) -> CustomResult<Vec<storage::ApiKey>, errors::StorageError> {
|
||||
let conn = pg_connection(&self.master_pool).await;
|
||||
storage::ApiKey::find_by_merchant_id(&conn, merchant_id, limit, offset)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
.into_report()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl ApiKeyInterface for MockDb {
|
||||
async fn insert_api_key(
|
||||
&self,
|
||||
_api_key: storage::ApiKeyNew,
|
||||
) -> CustomResult<storage::ApiKey, errors::StorageError> {
|
||||
// [#172]: Implement function for `MockDb`
|
||||
Err(errors::StorageError::MockDbError)?
|
||||
}
|
||||
|
||||
async fn update_api_key(
|
||||
&self,
|
||||
_key_id: String,
|
||||
_api_key: storage::ApiKeyUpdate,
|
||||
) -> CustomResult<storage::ApiKey, errors::StorageError> {
|
||||
// [#172]: Implement function for `MockDb`
|
||||
Err(errors::StorageError::MockDbError)?
|
||||
}
|
||||
|
||||
async fn revoke_api_key(&self, _key_id: &str) -> CustomResult<bool, errors::StorageError> {
|
||||
// [#172]: Implement function for `MockDb`
|
||||
Err(errors::StorageError::MockDbError)?
|
||||
}
|
||||
|
||||
async fn find_api_key_optional(
|
||||
&self,
|
||||
_key_id: &str,
|
||||
) -> CustomResult<Option<storage::ApiKey>, errors::StorageError> {
|
||||
// [#172]: Implement function for `MockDb`
|
||||
Err(errors::StorageError::MockDbError)?
|
||||
}
|
||||
|
||||
async fn list_api_keys_by_merchant_id(
|
||||
&self,
|
||||
_merchant_id: &str,
|
||||
_limit: Option<i64>,
|
||||
_offset: Option<i64>,
|
||||
) -> CustomResult<Vec<storage::ApiKey>, errors::StorageError> {
|
||||
// [#172]: Implement function for `MockDb`
|
||||
Err(errors::StorageError::MockDbError)?
|
||||
}
|
||||
}
|
||||
@ -49,6 +49,7 @@ pub mod headers {
|
||||
pub const ACCEPT: &str = "Accept";
|
||||
pub const X_API_VERSION: &str = "X-ApiVersion";
|
||||
pub const DATE: &str = "Date";
|
||||
pub const X_MERCHANT_ID: &str = "X-Merchant-Id";
|
||||
}
|
||||
|
||||
pub mod pii {
|
||||
@ -95,7 +96,9 @@ pub fn mk_app(
|
||||
|
||||
#[cfg(feature = "olap")]
|
||||
{
|
||||
server_app = server_app.service(routes::MerchantAccount::server(state.clone()));
|
||||
server_app = server_app
|
||||
.service(routes::MerchantAccount::server(state.clone()))
|
||||
.service(routes::ApiKeys::server(state.clone()));
|
||||
}
|
||||
|
||||
#[cfg(feature = "stripe")]
|
||||
|
||||
@ -17,7 +17,10 @@ Our APIs accept and return JSON in the HTTP body, and return standard HTTP respo
|
||||
You can consume the APIs directly using your favorite HTTP/REST library.
|
||||
|
||||
We have a testing environment referred to "sandbox", which you can setup to test API calls without
|
||||
affecting production data. Currently, our sandbox environment is live while our production environment is under development and will be available soon. You can sign up on our Dashboard to get API keys to access Hyperswitch API.
|
||||
affecting production data.
|
||||
Currently, our sandbox environment is live while our production environment is under development
|
||||
and will be available soon.
|
||||
You can sign up on our Dashboard to get API keys to access Hyperswitch API.
|
||||
|
||||
### Environment
|
||||
|
||||
@ -32,13 +35,13 @@ Use the following base URLs when making requests to the APIs:
|
||||
|
||||
When you sign up on our [dashboard](https://app.hyperswitch.io) and create a merchant
|
||||
account, you are given a secret key (also referred as api-key) and a publishable key.
|
||||
You may authenticate all API requests with Hyperswitch server by providing the appropriate key in the
|
||||
request Authorization header.
|
||||
You may authenticate all API requests with Hyperswitch server by providing the appropriate key in
|
||||
the request Authorization header.
|
||||
|
||||
| Key | Description |
|
||||
|---------------|-----------------------------------------------------------------------------------------------|
|
||||
| Sandbox | Private key. Used to authenticate all API requests from your merchant server |
|
||||
| Production | Unique identifier for your account. Used to authenticate API requests from your app’s client |
|
||||
| Production | Unique identifier for your account. Used to authenticate API requests from your app's client |
|
||||
|
||||
Never share your secret api keys. Keep them guarded and secure.
|
||||
"#,
|
||||
@ -47,13 +50,14 @@ Never share your secret api keys. Keep them guarded and secure.
|
||||
(url = "https://sandbox.hyperswitch.io", description = "Sandbox Environment")
|
||||
),
|
||||
tags(
|
||||
(name = "Merchant Account"),// , description = "Create and manage merchant accounts"),
|
||||
(name = "Merchant Connector Account"),// , description = "Create and manage merchant connector accounts"),
|
||||
(name = "Payments"),// , description = "Create and manage one-time payments, recurring payments and mandates"),
|
||||
(name = "Refunds"),// , description = "Create and manage refunds for successful payments"),
|
||||
(name = "Mandates"),// , description = "Manage mandates"),
|
||||
(name = "Customers"),// , description = "Create and manage customers"),
|
||||
(name = "Payment Methods")// , description = "Create and manage payment methods of customers")
|
||||
(name = "Merchant Account", description = "Create and manage merchant accounts"),
|
||||
(name = "Merchant Connector Account", description = "Create and manage merchant connector accounts"),
|
||||
(name = "Payments", description = "Create and manage one-time payments, recurring payments and mandates"),
|
||||
(name = "Refunds", description = "Create and manage refunds for successful payments"),
|
||||
(name = "Mandates", description = "Manage mandates"),
|
||||
(name = "Customers", description = "Create and manage customers"),
|
||||
(name = "Payment Methods", description = "Create and manage payment methods of customers"),
|
||||
(name = "API Key", description = "Create and manage API Keys"),
|
||||
),
|
||||
paths(
|
||||
crate::routes::refunds::refunds_create,
|
||||
@ -92,6 +96,11 @@ Never share your secret api keys. Keep them guarded and secure.
|
||||
crate::routes::customers::customers_retrieve,
|
||||
crate::routes::customers::customers_update,
|
||||
crate::routes::customers::customers_delete,
|
||||
crate::routes::api_keys::api_key_create,
|
||||
crate::routes::api_keys::api_key_retrieve,
|
||||
crate::routes::api_keys::api_key_update,
|
||||
crate::routes::api_keys::api_key_revoke,
|
||||
crate::routes::api_keys::api_key_list,
|
||||
),
|
||||
components(schemas(
|
||||
crate::types::api::refunds::RefundRequest,
|
||||
@ -181,6 +190,12 @@ Never share your secret api keys. Keep them guarded and secure.
|
||||
crate::types::api::admin::MerchantConnectorId,
|
||||
crate::types::api::admin::MerchantDetails,
|
||||
crate::types::api::admin::WebhookDetails,
|
||||
crate::types::api::api_keys::ApiKeyExpiration,
|
||||
crate::types::api::api_keys::CreateApiKeyRequest,
|
||||
crate::types::api::api_keys::CreateApiKeyResponse,
|
||||
crate::types::api::api_keys::RetrieveApiKeyResponse,
|
||||
crate::types::api::api_keys::RevokeApiKeyResponse,
|
||||
crate::types::api::api_keys::UpdateApiKeyRequest
|
||||
))
|
||||
)]
|
||||
pub struct ApiDoc;
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
pub mod admin;
|
||||
pub mod api_keys;
|
||||
pub mod app;
|
||||
pub mod configs;
|
||||
pub mod customers;
|
||||
@ -13,7 +14,7 @@ pub mod refunds;
|
||||
pub mod webhooks;
|
||||
|
||||
pub use self::app::{
|
||||
AppState, Configs, Customers, EphemeralKey, Health, Mandates, MerchantAccount,
|
||||
ApiKeys, AppState, Configs, Customers, EphemeralKey, Health, Mandates, MerchantAccount,
|
||||
MerchantConnectorAccount, PaymentMethods, Payments, Payouts, Refunds, Webhooks,
|
||||
};
|
||||
#[cfg(feature = "stripe")]
|
||||
|
||||
209
crates/router/src/routes/api_keys.rs
Normal file
209
crates/router/src/routes/api_keys.rs
Normal file
@ -0,0 +1,209 @@
|
||||
use actix_web::{web, HttpRequest, Responder};
|
||||
use error_stack::{IntoReport, ResultExt};
|
||||
use router_env::{instrument, tracing, Flow};
|
||||
|
||||
use super::app::AppState;
|
||||
use crate::{
|
||||
core::{
|
||||
api_keys,
|
||||
errors::{self, RouterResult},
|
||||
},
|
||||
services::{api, authentication as auth},
|
||||
types::api as api_types,
|
||||
};
|
||||
|
||||
/// API Key - Create
|
||||
///
|
||||
/// Create a new API Key for accessing our APIs from your servers. The plaintext API Key will be
|
||||
/// displayed only once on creation, so ensure you store it securely.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api_keys",
|
||||
request_body= CreateApiKeyRequest,
|
||||
responses(
|
||||
(status = 200, description = "API Key created", body = CreateApiKeyResponse),
|
||||
(status = 400, description = "Invalid data")
|
||||
),
|
||||
tag = "API Key",
|
||||
operation_id = "Create an API Key"
|
||||
)]
|
||||
#[instrument(skip_all, fields(flow = ?Flow::ApiKeyCreate))]
|
||||
pub async fn api_key_create(
|
||||
state: web::Data<AppState>,
|
||||
req: HttpRequest,
|
||||
json_payload: web::Json<api_types::CreateApiKeyRequest>,
|
||||
) -> impl Responder {
|
||||
let payload = json_payload.into_inner();
|
||||
|
||||
api::server_wrap(
|
||||
state.get_ref(),
|
||||
&req,
|
||||
payload,
|
||||
|state, _, payload| async {
|
||||
let merchant_id = get_merchant_id_header(&req)?;
|
||||
api_keys::create_api_key(&*state.store, payload, merchant_id).await
|
||||
},
|
||||
&auth::AdminApiAuth,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// API Key - Retrieve
|
||||
///
|
||||
/// Retrieve information about the specified API Key.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api_keys/{key_id}",
|
||||
params (("key_id" = String, Path, description = "The unique identifier for the API Key")),
|
||||
responses(
|
||||
(status = 200, description = "API Key retrieved", body = RetrieveApiKeyResponse),
|
||||
(status = 404, description = "API Key not found")
|
||||
),
|
||||
tag = "API Key",
|
||||
operation_id = "Retrieve an API Key"
|
||||
)]
|
||||
#[instrument(skip_all, fields(flow = ?Flow::ApiKeyRetrieve))]
|
||||
pub async fn api_key_retrieve(
|
||||
state: web::Data<AppState>,
|
||||
req: HttpRequest,
|
||||
path: web::Path<String>,
|
||||
) -> impl Responder {
|
||||
let key_id = path.into_inner();
|
||||
|
||||
api::server_wrap(
|
||||
state.get_ref(),
|
||||
&req,
|
||||
&key_id,
|
||||
|state, _, key_id| api_keys::retrieve_api_key(&*state.store, key_id),
|
||||
&auth::AdminApiAuth,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// API Key - Update
|
||||
///
|
||||
/// Update information for the specified API Key.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api_keys/{key_id}",
|
||||
request_body = UpdateApiKeyRequest,
|
||||
params (("key_id" = String, Path, description = "The unique identifier for the API Key")),
|
||||
responses(
|
||||
(status = 200, description = "API Key updated", body = RetrieveApiKeyResponse),
|
||||
(status = 404, description = "API Key not found")
|
||||
),
|
||||
tag = "API Key",
|
||||
operation_id = "Update an API Key"
|
||||
)]
|
||||
#[instrument(skip_all, fields(flow = ?Flow::ApiKeyUpdate))]
|
||||
pub async fn api_key_update(
|
||||
state: web::Data<AppState>,
|
||||
req: HttpRequest,
|
||||
path: web::Path<String>,
|
||||
json_payload: web::Json<api_types::UpdateApiKeyRequest>,
|
||||
) -> impl Responder {
|
||||
let key_id = path.into_inner();
|
||||
let payload = json_payload.into_inner();
|
||||
|
||||
api::server_wrap(
|
||||
state.get_ref(),
|
||||
&req,
|
||||
(&key_id, payload),
|
||||
|state, _, (key_id, payload)| api_keys::update_api_key(&*state.store, key_id, payload),
|
||||
&auth::AdminApiAuth,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// API Key - Revoke
|
||||
///
|
||||
/// Revoke the specified API Key. Once revoked, the API Key can no longer be used for
|
||||
/// authenticating with our APIs.
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api_keys/{key_id}",
|
||||
params (("key_id" = String, Path, description = "The unique identifier for the API Key")),
|
||||
responses(
|
||||
(status = 200, description = "API Key revoked", body = RevokeApiKeyResponse),
|
||||
(status = 404, description = "API Key not found")
|
||||
),
|
||||
tag = "API Key",
|
||||
operation_id = "Revoke an API Key"
|
||||
)]
|
||||
#[instrument(skip_all, fields(flow = ?Flow::ApiKeyRevoke))]
|
||||
pub async fn api_key_revoke(
|
||||
state: web::Data<AppState>,
|
||||
req: HttpRequest,
|
||||
path: web::Path<String>,
|
||||
) -> impl Responder {
|
||||
let key_id = path.into_inner();
|
||||
|
||||
api::server_wrap(
|
||||
state.get_ref(),
|
||||
&req,
|
||||
&key_id,
|
||||
|state, _, key_id| api_keys::revoke_api_key(&*state.store, key_id),
|
||||
&auth::AdminApiAuth,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// API Key - List
|
||||
///
|
||||
/// List all API Keys associated with your merchant account.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api_keys/list",
|
||||
params(
|
||||
("limit" = Option<i64>, Query, description = "The maximum number of API Keys to include in the response"),
|
||||
("skip" = Option<i64>, Query, description = "The number of API Keys to skip when retrieving the list of API keys."),
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "List of API Keys retrieved successfully", body = Vec<RetrieveApiKeyResponse>),
|
||||
),
|
||||
tag = "API Key",
|
||||
operation_id = "List all API Keys associated with a merchant account"
|
||||
)]
|
||||
#[instrument(skip_all, fields(flow = ?Flow::ApiKeyList))]
|
||||
pub async fn api_key_list(
|
||||
state: web::Data<AppState>,
|
||||
req: HttpRequest,
|
||||
query: web::Query<api_types::ListApiKeyConstraints>,
|
||||
) -> impl Responder {
|
||||
let list_api_key_constraints = query.into_inner();
|
||||
let limit = list_api_key_constraints.limit;
|
||||
let offset = list_api_key_constraints.skip;
|
||||
|
||||
api::server_wrap(
|
||||
state.get_ref(),
|
||||
&req,
|
||||
(&req, limit, offset),
|
||||
|state, _, (req, limit, offset)| async move {
|
||||
let merchant_id = get_merchant_id_header(req)?;
|
||||
api_keys::list_api_keys(&*state.store, merchant_id, limit, offset).await
|
||||
},
|
||||
&auth::AdminApiAuth,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
fn get_merchant_id_header(req: &HttpRequest) -> RouterResult<String> {
|
||||
use crate::headers::X_MERCHANT_ID;
|
||||
|
||||
req.headers()
|
||||
.get(X_MERCHANT_ID)
|
||||
.ok_or_else(|| errors::ApiErrorResponse::InvalidRequestData {
|
||||
message: format!("Missing header: `{X_MERCHANT_ID}`"),
|
||||
})
|
||||
.into_report()?
|
||||
.to_str()
|
||||
.into_report()
|
||||
.change_context(errors::ApiErrorResponse::InvalidDataValue {
|
||||
field_name: X_MERCHANT_ID,
|
||||
})
|
||||
.attach_printable(
|
||||
"Failed to convert header value to string, \
|
||||
possibly contains non-printable or non-ASCII characters",
|
||||
)
|
||||
.map(|s| s.to_owned())
|
||||
}
|
||||
@ -1,8 +1,8 @@
|
||||
use actix_web::{web, Scope};
|
||||
|
||||
#[cfg(feature = "olap")]
|
||||
use super::admin::*;
|
||||
use super::health::*;
|
||||
#[cfg(feature = "olap")]
|
||||
use super::{admin::*, api_keys::*};
|
||||
#[cfg(any(feature = "olap", feature = "oltp"))]
|
||||
use super::{configs::*, customers::*, mandates::*, payments::*, payouts::*, refunds::*};
|
||||
#[cfg(feature = "oltp")]
|
||||
@ -334,3 +334,21 @@ impl Configs {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ApiKeys;
|
||||
|
||||
#[cfg(feature = "olap")]
|
||||
impl ApiKeys {
|
||||
pub fn server(state: AppState) -> Scope {
|
||||
web::scope("/api_keys")
|
||||
.app_data(web::Data::new(state))
|
||||
.service(web::resource("").route(web::post().to(api_key_create)))
|
||||
.service(web::resource("/list").route(web::get().to(api_key_list)))
|
||||
.service(
|
||||
web::resource("/{key_id}")
|
||||
.route(web::get().to(api_key_retrieve))
|
||||
.route(web::post().to(api_key_update))
|
||||
.route(web::delete().to(api_key_revoke)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
pub mod admin;
|
||||
pub mod api_keys;
|
||||
pub mod configs;
|
||||
pub mod customers;
|
||||
pub mod enums;
|
||||
@ -13,7 +14,8 @@ use std::{fmt::Debug, str::FromStr};
|
||||
use error_stack::{report, IntoReport, ResultExt};
|
||||
|
||||
pub use self::{
|
||||
admin::*, configs::*, customers::*, payment_methods::*, payments::*, refunds::*, webhooks::*,
|
||||
admin::*, api_keys::*, configs::*, customers::*, payment_methods::*, payments::*, refunds::*,
|
||||
webhooks::*,
|
||||
};
|
||||
use super::ErrorResponse;
|
||||
use crate::{
|
||||
|
||||
4
crates/router/src/types/api/api_keys.rs
Normal file
4
crates/router/src/types/api/api_keys.rs
Normal file
@ -0,0 +1,4 @@
|
||||
pub use api_models::api_keys::{
|
||||
ApiKeyExpiration, CreateApiKeyRequest, CreateApiKeyResponse, ListApiKeyConstraints,
|
||||
RetrieveApiKeyResponse, RevokeApiKeyResponse, UpdateApiKeyRequest,
|
||||
};
|
||||
@ -1,4 +1,5 @@
|
||||
pub mod address;
|
||||
pub mod api_keys;
|
||||
pub mod configs;
|
||||
pub mod connector_response;
|
||||
pub mod customers;
|
||||
@ -22,7 +23,8 @@ pub mod refund;
|
||||
pub mod kv;
|
||||
|
||||
pub use self::{
|
||||
address::*, configs::*, connector_response::*, customers::*, events::*, locker_mock_up::*,
|
||||
mandate::*, merchant_account::*, merchant_connector_account::*, payment_attempt::*,
|
||||
payment_intent::*, payment_method::*, process_tracker::*, refund::*, reverse_lookup::*,
|
||||
address::*, api_keys::*, configs::*, connector_response::*, customers::*, events::*,
|
||||
locker_mock_up::*, mandate::*, merchant_account::*, merchant_connector_account::*,
|
||||
payment_attempt::*, payment_intent::*, payment_method::*, process_tracker::*, refund::*,
|
||||
reverse_lookup::*,
|
||||
};
|
||||
|
||||
1
crates/router/src/types/storage/api_keys.rs
Normal file
1
crates/router/src/types/storage/api_keys.rs
Normal file
@ -0,0 +1 @@
|
||||
pub use storage_models::api_keys::{ApiKey, ApiKeyNew, ApiKeyUpdate, HashedApiKey};
|
||||
@ -398,3 +398,68 @@ impl From<F<api_models::payments::AddressDetails>> for F<storage_models::address
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl
|
||||
From<
|
||||
F<(
|
||||
storage_models::api_keys::ApiKey,
|
||||
crate::core::api_keys::PlaintextApiKey,
|
||||
)>,
|
||||
> for F<api_models::api_keys::CreateApiKeyResponse>
|
||||
{
|
||||
fn from(
|
||||
item: F<(
|
||||
storage_models::api_keys::ApiKey,
|
||||
crate::core::api_keys::PlaintextApiKey,
|
||||
)>,
|
||||
) -> Self {
|
||||
use masking::StrongSecret;
|
||||
|
||||
let (api_key, plaintext_api_key) = item.0;
|
||||
api_models::api_keys::CreateApiKeyResponse {
|
||||
key_id: api_key.key_id.clone(),
|
||||
merchant_id: api_key.merchant_id,
|
||||
name: api_key.name,
|
||||
description: api_key.description,
|
||||
api_key: StrongSecret::from(format!(
|
||||
"{}-{}",
|
||||
api_key.key_id,
|
||||
plaintext_api_key.peek().to_owned()
|
||||
)),
|
||||
created: api_key.created_at,
|
||||
expiration: api_key.expires_at.into(),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<F<storage_models::api_keys::ApiKey>> for F<api_models::api_keys::RetrieveApiKeyResponse> {
|
||||
fn from(item: F<storage_models::api_keys::ApiKey>) -> Self {
|
||||
let api_key = item.0;
|
||||
api_models::api_keys::RetrieveApiKeyResponse {
|
||||
key_id: api_key.key_id.clone(),
|
||||
merchant_id: api_key.merchant_id,
|
||||
name: api_key.name,
|
||||
description: api_key.description,
|
||||
prefix: format!("{}-{}", api_key.key_id, api_key.prefix).into(),
|
||||
created: api_key.created_at,
|
||||
expiration: api_key.expires_at.into(),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<F<api_models::api_keys::UpdateApiKeyRequest>>
|
||||
for F<storage_models::api_keys::ApiKeyUpdate>
|
||||
{
|
||||
fn from(item: F<api_models::api_keys::UpdateApiKeyRequest>) -> Self {
|
||||
let api_key = item.0;
|
||||
storage_models::api_keys::ApiKeyUpdate::Update {
|
||||
name: api_key.name,
|
||||
description: api_key.description,
|
||||
expires_at: api_key.expiration.map(Into::into),
|
||||
last_used: None,
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user