Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions src/custom_extractors.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::sync::Arc;

use axum::{async_trait, extract::FromRequestParts, http::request::Parts};
use hyperswitch_masking::Secret;

use crate::{
app::TenantAppState,
Expand Down Expand Up @@ -57,3 +58,36 @@ impl FromRequestParts<Arc<GlobalAppState>> for TenantId {
Ok(Self(tenant_id))
}
}

/// Optionally reads `x-fingerprint-id` from request headers.
/// If present, the value must be exactly 20 alphanumeric (0-9 a-z A-Z) characters,
/// matching the format of server-generated fingerprint IDs.
#[derive(Debug)]
pub struct OptionalFingerprintId(pub Option<Secret<String>>);

#[async_trait]
impl FromRequestParts<Arc<GlobalAppState>> for OptionalFingerprintId {
type Rejection = ContainerError<ApiError>;

async fn from_request_parts(
parts: &mut Parts,
_state: &Arc<GlobalAppState>,
) -> Result<Self, Self::Rejection> {
let fingerprint_id = parts
.headers
.get(consts::X_FINGERPRINT_ID)
.and_then(|h| h.to_str().ok())
.map(|s| -> Result<Secret<String>, ContainerError<ApiError>> {
if s.len() != consts::ID_LENGTH || !s.chars().all(|c| c.is_ascii_alphanumeric()) {
Err(ContainerError::from(ApiError::ValidationError(
"x-fingerprint-id must be exactly 20 alphanumeric characters",
)))
} else {
Ok(Secret::new(s.to_string()))
}
})
.transpose()?;

Ok(Self(fingerprint_id))
}
}
5 changes: 3 additions & 2 deletions src/routes/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use axum::{error_handling::HandleErrorLayer, response::IntoResponse};
use self::types::Validation;
use crate::{
crypto::{hash_manager::managers::sha::Sha512, keymanager},
custom_extractors::TenantStateResolver,
custom_extractors::{OptionalFingerprintId, TenantStateResolver},
error::{self, ContainerError, ResultContainerExt},
logger,
storage::{FingerprintInterface, HashInterface, LockerInterface},
Expand Down Expand Up @@ -219,11 +219,12 @@ pub async fn retrieve_card(
/// `/cards/fingerprint` handling the creation and retrieval of card fingerprint
pub async fn get_or_insert_fingerprint(
TenantStateResolver(tenant_app_state): TenantStateResolver,
OptionalFingerprintId(fingerprint_id): OptionalFingerprintId,
Json(request): Json<types::FingerprintRequest>,
) -> Result<Json<types::FingerprintResponse>, ContainerError<error::ApiError>> {
let fingerprint = tenant_app_state
.db
.get_or_insert_fingerprint(request.data, request.key)
.get_or_insert_fingerprint(request.data, request.key, fingerprint_id)
.await?;

let response = Json(fingerprint.into());
Expand Down
1 change: 1 addition & 0 deletions src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ pub(crate) trait FingerprintInterface {
&self,
data: Secret<String>,
key: Secret<String>,
fingerprint_id: Option<Secret<String>>,
) -> Result<types::Fingerprint, ContainerError<Self::Error>>;
}

Expand Down
6 changes: 5 additions & 1 deletion src/storage/caching/fingerprint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,12 @@ where
&self,
data: Secret<String>,
key: Secret<String>,
fingerprint_id: Option<Secret<String>>,
) -> Result<types::Fingerprint, ContainerError<Self::Error>> {
let output = self.inner.get_or_insert_fingerprint(data, key).await?;
let output = self
.inner
.get_or_insert_fingerprint(data, key, fingerprint_id)
.await?;
self.cache_data::<types::Fingerprint>(
output.fingerprint_hash.clone().expose(),
output.clone(),
Expand Down
2 changes: 2 additions & 0 deletions src/storage/consts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ pub const ID_LENGTH: usize = 20;
pub const X_TENANT_ID: &str = "x-tenant-id";
/// Header key for request ID
pub const X_REQUEST_ID: &str = "x-request-id";
/// Header key for caller-supplied fingerprint ID (optional)
pub const X_FINGERPRINT_ID: &str = "x-fingerprint-id";
/// Header Constants
pub mod headers {
pub const CONTENT_TYPE: &str = "Content-Type";
Expand Down
40 changes: 30 additions & 10 deletions src/storage/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,7 @@ impl super::FingerprintInterface for Storage {
&self,
data: Secret<String>,
key: Secret<String>,
fingerprint_id: Option<Secret<String>>,
) -> Result<types::Fingerprint, ContainerError<Self::Error>> {
let algo = HmacSha512::<1>::new(key.map(|inner| inner.into_bytes()));

Expand All @@ -360,20 +361,39 @@ impl super::FingerprintInterface for Storage {
.find_by_fingerprint_hash(fingerprint_hash.clone())
.await?;
match output {
// Hash already exists: return the stored fingerprint regardless of any
// caller-supplied id — the hash is the canonical deduplication key.
Some(inner) => Ok(inner),
None => {
let id = fingerprint_id
.unwrap_or_else(|| utils::generate_nano_id(consts::ID_LENGTH).into());
let cloned_hash = fingerprint_hash.clone();
let mut conn = self.get_conn().await?;
let query = diesel::insert_into(types::Fingerprint::table()).values(
types::FingerprintTableNew {
fingerprint_hash,
fingerprint_id: utils::generate_nano_id(consts::ID_LENGTH).into(),
},
);

Ok(query
.get_result(&mut conn)
.await
.change_error(error::StorageError::InsertError)?)
let insert_result: Result<types::Fingerprint, diesel::result::Error> =
diesel::insert_into(types::Fingerprint::table())
.values(types::FingerprintTableNew {
fingerprint_hash,
fingerprint_id: id,
})
.get_result(&mut conn)
.await;

match insert_result {
Ok(inner) => Ok(inner),
// Race condition: a concurrent request inserted the same hash first.
// Re-read by hash and return the winner row.
Err(diesel::result::Error::DatabaseError(
diesel::result::DatabaseErrorKind::UniqueViolation,
_,
)) => self
.find_by_fingerprint_hash(cloned_hash)
.await?
.ok_or_else(|| {
ContainerError::from(error::FingerprintDBError::DBInsertError)
}),
Err(error) => Err(error).change_error(error::StorageError::InsertError)?,
}
}
}
}
Expand Down
Loading