From d43ddb5e065152cdd73b471ce78d23834282c6f0 Mon Sep 17 00:00:00 2001 From: Jet Pham Date: Thu, 5 Mar 2026 13:22:19 -0800 Subject: [PATCH] feat: sort by time --- mymx-sdk/src/lib.rs | 28 +--------------------------- mymx-server/src/main.rs | 16 ++-------------- 2 files changed, 3 insertions(+), 41 deletions(-) diff --git a/mymx-sdk/src/lib.rs b/mymx-sdk/src/lib.rs index d20763a..68a6f3c 100644 --- a/mymx-sdk/src/lib.rs +++ b/mymx-sdk/src/lib.rs @@ -1,4 +1,3 @@ -use chrono::Utc; use hmac::{Hmac, Mac}; use serde::Deserialize; use sha2::Sha256; @@ -13,8 +12,6 @@ pub enum Error { InvalidSignatureHeader, #[error("signature verification failed")] SignatureVerificationFailed, - #[error("timestamp too old (replay protection)")] - TimestampTooOld, #[error("failed to parse webhook body: {0}")] ParseError(#[from] serde_json::Error), #[error("HMAC error: {0}")] @@ -168,16 +165,9 @@ fn parse_signature_header(header: &str) -> Result<(i64, String), Error> { /// 2. Construct the signed payload: `"{timestamp}.{raw_body}"` /// 3. Compute HMAC-SHA256 with the webhook secret /// 4. Constant-time compare -/// 5. Reject timestamps older than 5 minutes pub fn verify_signature(raw_body: &str, signature_header: &str, secret: &str) -> Result<(), Error> { let (timestamp, expected_sig) = parse_signature_header(signature_header)?; - // Replay protection: reject timestamps older than 5 minutes - let now = Utc::now().timestamp(); - if now - timestamp > 300 { - return Err(Error::TimestampTooOld); - } - // Construct signed payload let payload = format!("{}.{}", timestamp, raw_body); @@ -213,6 +203,7 @@ pub fn handle_webhook( #[cfg(test)] mod tests { use super::*; + use chrono::Utc; #[test] fn test_parse_signature_header() { @@ -254,21 +245,4 @@ mod tests { assert_eq!(event.email.id, "e90fef93-9549-4e17-b86f-295c13089645"); } - #[test] - fn test_verify_signature_replay() { - let secret = "test_secret"; - let body = r#"{"test": true}"#; - let old_ts = Utc::now().timestamp() - 600; // 10 minutes ago - - let payload = format!("{}.{}", old_ts, body); - let mut mac = HmacSha256::new_from_slice(secret.as_bytes()).unwrap(); - mac.update(payload.as_bytes()); - let sig = hex::encode(mac.finalize().into_bytes()); - - let header = format!("t={},v1={}", old_ts, sig); - assert!(matches!( - verify_signature(body, &header, secret), - Err(Error::TimestampTooOld) - )); - } } diff --git a/mymx-server/src/main.rs b/mymx-server/src/main.rs index f3df830..9a64150 100644 --- a/mymx-server/src/main.rs +++ b/mymx-server/src/main.rs @@ -62,15 +62,6 @@ fn map_webhook_error(e: mymx_sdk::Error) -> (StatusCode, Json) { }), ) } - mymx_sdk::Error::TimestampTooOld => { - tracing::warn!("Webhook rejected: timestamp too old (replay protection)"); - ( - StatusCode::UNAUTHORIZED, - Json(ErrorResponse { - error: "timestamp too old".into(), - }), - ) - } mymx_sdk::Error::HmacError(msg) => { tracing::warn!("Webhook rejected: HMAC error: {}", msg); ( @@ -218,7 +209,7 @@ async fn health_handler(State(state): State) -> impl IntoResponse { async fn index_handler(State(state): State) -> Result, StatusCode> { let emails: Vec = sqlx::query_as::<_, EmailRow>( - "SELECT body, subject, from_address, received_at FROM emails", + "SELECT body, subject, from_address, received_at FROM emails ORDER BY received_at DESC", ) .fetch_all(&state.db) .await @@ -227,7 +218,7 @@ async fn index_handler(State(state): State) -> Result, St StatusCode::INTERNAL_SERVER_ERROR })?; - let mut rows: Vec<(String, String, String, DateTime)> = emails + let rows: Vec<(String, String, String, DateTime)> = emails .into_iter() .map(|email| { let mut hasher = Sha256::new(); @@ -239,9 +230,6 @@ async fn index_handler(State(state): State) -> Result, St }) .collect(); - // Sort alphabetically by hash - rows.sort_by(|a, b| a.0.cmp(&b.0)); - let table_rows: String = rows .iter() .map(|(hash, subject, from, received_at)| {