From 5339b7323d0ea3a729ac7a39a86ffc766bca91ee Mon Sep 17 00:00:00 2001 From: Endowed992 Date: Sun, 21 Jun 2026 21:50:08 +0000 Subject: [PATCH 1/2] feat(error-handling): restructure error handling module with enhanced types and sanitization --- backend/ERROR_HANDLING_README.md | 2 +- backend/src/error/code.rs | 51 ++++ backend/src/error/conversions.rs | 76 ++++++ backend/src/error/mod.rs | 9 + backend/src/error/response.rs | 72 +++++ backend/src/error/sanitize.rs | 70 +++++ backend/src/{error.rs => error/types.rs} | 324 ++--------------------- docs/ERROR_HANDLING_IMPLEMENTATION.md | 2 +- 8 files changed, 309 insertions(+), 297 deletions(-) create mode 100644 backend/src/error/code.rs create mode 100644 backend/src/error/conversions.rs create mode 100644 backend/src/error/mod.rs create mode 100644 backend/src/error/response.rs create mode 100644 backend/src/error/sanitize.rs rename backend/src/{error.rs => error/types.rs} (54%) diff --git a/backend/ERROR_HANDLING_README.md b/backend/ERROR_HANDLING_README.md index 44fb749d..7869abbf 100644 --- a/backend/ERROR_HANDLING_README.md +++ b/backend/ERROR_HANDLING_README.md @@ -190,7 +190,7 @@ match service.create_product(request).await { ## Files Modified/Created ### Modified -- `backend/src/error.rs` - Enhanced error types and sanitization +- `backend/src/error/` - Enhanced error types and sanitization - `backend/src/middleware.rs` - Added error_handler module - `backend/src/main.rs` - Integrated error monitoring and middleware diff --git a/backend/src/error/code.rs b/backend/src/error/code.rs new file mode 100644 index 00000000..02c6736d --- /dev/null +++ b/backend/src/error/code.rs @@ -0,0 +1,51 @@ +use serde::{Deserialize, Serialize}; + +/// Standardized error codes for programmatic handling +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum ErrorCode { + // Authentication & Authorization (1000-1099) + Unauthorized = 1000, + InvalidCredentials = 1001, + TokenExpired = 1002, + TokenInvalid = 1003, + InsufficientPermissions = 1004, + + // Validation Errors (1100-1199) + ValidationFailed = 1100, + InvalidInput = 1101, + MissingRequiredField = 1102, + InvalidFormat = 1103, + ValueOutOfRange = 1104, + + // Resource Errors (1200-1299) + ResourceNotFound = 1200, + ResourceAlreadyExists = 1201, + ResourceConflict = 1202, + ResourceDeleted = 1203, + + // Rate Limiting (1300-1399) + RateLimitExceeded = 1300, + QuotaExceeded = 1301, + + // Database Errors (1400-1499) + DatabaseError = 1400, + DatabaseConnectionFailed = 1401, + DatabaseQueryFailed = 1402, + DatabaseConstraintViolation = 1403, + + // External Service Errors (1500-1599) + ExternalServiceError = 1500, + BlockchainError = 1501, + PaymentServiceError = 1502, + + // Internal Errors (1600-1699) + InternalServerError = 1600, + ConfigurationError = 1601, + CryptographyError = 1602, + + // Business Logic Errors (1700-1799) + BusinessRuleViolation = 1700, + InvalidStateTransition = 1701, + OperationNotAllowed = 1702, +} diff --git a/backend/src/error/conversions.rs b/backend/src/error/conversions.rs new file mode 100644 index 00000000..8b7ff3db --- /dev/null +++ b/backend/src/error/conversions.rs @@ -0,0 +1,76 @@ +use super::AppError; + +impl From for AppError { + fn from(err: sqlx::Error) -> Self { + tracing::error!(error = ?err, "Database error"); + AppError::Database(err) + } +} + +impl From for AppError { + fn from(err: serde_json::Error) -> Self { + tracing::debug!(error = %err, "JSON parsing error"); + AppError::BadRequest("Invalid JSON format".to_string()) + } +} + +impl From for AppError { + fn from(err: bcrypt::BcryptError) -> Self { + tracing::error!(error = ?err, "Password hashing error"); + AppError::Cryptography("Password operation failed".to_string()) + } +} + +impl From for AppError { + fn from(err: chrono::ParseError) -> Self { + tracing::debug!(error = %err, "Date parsing error"); + AppError::Validation("Invalid date format".to_string()) + } +} + +impl From for AppError { + fn from(err: std::net::AddrParseError) -> Self { + tracing::debug!(error = %err, "Address parsing error"); + AppError::Validation("Invalid network address".to_string()) + } +} + +impl From for AppError { + fn from(err: config::ConfigError) -> Self { + tracing::error!(error = ?err, "Configuration error"); + AppError::Configuration("Configuration error".to_string()) + } +} + +impl From for AppError { + fn from(err: jsonwebtoken::errors::Error) -> Self { + use jsonwebtoken::errors::ErrorKind; + + tracing::debug!(error = ?err, "JWT error"); + + match err.kind() { + ErrorKind::ExpiredSignature => AppError::TokenExpired, + ErrorKind::InvalidToken + | ErrorKind::InvalidSignature + | ErrorKind::InvalidAlgorithm + | ErrorKind::Base64(_) + | ErrorKind::Json(_) + | ErrorKind::Utf8(_) => AppError::TokenInvalid, + _ => AppError::Unauthorized, + } + } +} + +impl From for AppError { + fn from(err: uuid::Error) -> Self { + tracing::debug!(error = %err, "UUID parsing error"); + AppError::Validation("Invalid ID format".to_string()) + } +} + +impl From for AppError { + fn from(err: redis::RedisError) -> Self { + tracing::error!(error = ?err, "Redis error"); + AppError::Internal("Cache service error".to_string()) + } +} diff --git a/backend/src/error/mod.rs b/backend/src/error/mod.rs new file mode 100644 index 00000000..f4867311 --- /dev/null +++ b/backend/src/error/mod.rs @@ -0,0 +1,9 @@ +pub mod code; +pub mod conversions; +pub mod response; +pub mod sanitize; +pub mod types; + +pub use code::ErrorCode; +pub use response::ErrorResponse; +pub use types::AppError; diff --git a/backend/src/error/response.rs b/backend/src/error/response.rs new file mode 100644 index 00000000..7e0fdbc7 --- /dev/null +++ b/backend/src/error/response.rs @@ -0,0 +1,72 @@ +use axum::{ + http::StatusCode, + response::{IntoResponse, Response}, + Json, +}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use super::code::ErrorCode; + +/// Standardized error response structure +#[derive(Debug, Serialize, Deserialize)] +pub struct ErrorResponse { + /// HTTP status code + pub status: u16, + + /// Standardized error code for programmatic handling + pub code: ErrorCode, + + /// User-friendly error message (sanitized) + pub message: String, + + /// Unique correlation ID for tracking and debugging + pub correlation_id: String, + + /// Optional additional details (only in development) + #[serde(skip_serializing_if = "Option::is_none")] + pub details: Option, +} + +impl ErrorResponse { + /// Create a new error response with correlation ID + pub fn new(status: StatusCode, code: ErrorCode, message: String) -> Self { + Self { + status: status.as_u16(), + code, + message, + correlation_id: Uuid::new_v4().to_string(), + details: None, + } + } + + /// Add details (only included in development mode) + pub fn with_details(mut self, details: String) -> Self { + if cfg!(debug_assertions) { + self.details = Some(details); + } + self + } +} + +#[cfg(test)] +mod tests { + use super::*; + use axum::http::StatusCode; + + #[test] + fn test_error_response_excludes_details_in_release() { + let response = ErrorResponse::new( + StatusCode::INTERNAL_SERVER_ERROR, + ErrorCode::InternalServerError, + "Test error".to_string(), + ) + .with_details("Sensitive internal details".to_string()); + + #[cfg(not(debug_assertions))] + assert!(response.details.is_none()); + + #[cfg(debug_assertions)] + assert!(response.details.is_some()); + } +} diff --git a/backend/src/error/sanitize.rs b/backend/src/error/sanitize.rs new file mode 100644 index 00000000..113257a9 --- /dev/null +++ b/backend/src/error/sanitize.rs @@ -0,0 +1,70 @@ +use regex::Regex; + +/// Sanitize database errors to prevent information disclosure +pub fn sanitize_database_error(err: &sqlx::Error) -> String { + match err { + sqlx::Error::RowNotFound => "Record not found".to_string(), + sqlx::Error::ColumnNotFound(_) => "Database schema error".to_string(), + sqlx::Error::Database(_) => "Database constraint violation".to_string(), + sqlx::Error::PoolTimedOut => "Database connection timeout".to_string(), + sqlx::Error::PoolClosed => "Database connection closed".to_string(), + _ => "Database operation failed".to_string(), + } +} + +/// Sanitize user messages to prevent information disclosure +pub fn sanitize_message(msg: &str) -> String { + let msg = Regex::new(r"(/[a-zA-Z0-9_\-./]+)") + .unwrap() + .replace_all(msg, "[path]"); + + let msg = Regex::new(r"(?i)(SELECT|INSERT|UPDATE|DELETE|FROM|WHERE|JOIN)") + .unwrap() + .replace_all(&msg, "[sql]"); + + let msg = Regex::new(r"(postgres|mysql|mongodb)://[^\s]+") + .unwrap() + .replace_all(&msg, "[connection]"); + + let msg = Regex::new(r"([a-zA-Z0-9_-]{32,})") + .unwrap() + .replace_all(&msg, "[token]"); + + msg.to_string() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_sanitize_message_removes_paths() { + let msg = "Error in /usr/local/app/src/main.rs"; + let sanitized = sanitize_message(msg); + assert!(sanitized.contains("[path]")); + assert!(!sanitized.contains("/usr/local")); + } + + #[test] + fn test_sanitize_message_removes_sql() { + let msg = "Error in SELECT * FROM users WHERE id = 1"; + let sanitized = sanitize_message(msg); + assert!(sanitized.contains("[sql]")); + assert!(!sanitized.contains("SELECT")); + } + + #[test] + fn test_sanitize_message_removes_connection_strings() { + let msg = "Failed to connect to postgres://user:pass@localhost:5432/db"; + let sanitized = sanitize_message(msg); + assert!(sanitized.contains("[connection]")); + assert!(!sanitized.contains("postgres://")); + } + + #[test] + fn test_sanitize_message_removes_tokens() { + let msg = "Invalid token: abc123def456ghi789jkl012mno345pqr678"; + let sanitized = sanitize_message(msg); + assert!(sanitized.contains("[token]")); + } +} diff --git a/backend/src/error.rs b/backend/src/error/types.rs similarity index 54% rename from backend/src/error.rs rename to backend/src/error/types.rs index 3a51ee61..78264973 100644 --- a/backend/src/error.rs +++ b/backend/src/error/types.rs @@ -3,166 +3,75 @@ use axum::{ response::{IntoResponse, Response}, Json, }; -use serde::{Deserialize, Serialize}; -use serde_json::json; use uuid::Uuid; -/// Standardized error codes for programmatic handling -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] -#[serde(rename_all = "SCREAMING_SNAKE_CASE")] -pub enum ErrorCode { - // Authentication & Authorization (1000-1099) - Unauthorized = 1000, - InvalidCredentials = 1001, - TokenExpired = 1002, - TokenInvalid = 1003, - InsufficientPermissions = 1004, - - // Validation Errors (1100-1199) - ValidationFailed = 1100, - InvalidInput = 1101, - MissingRequiredField = 1102, - InvalidFormat = 1103, - ValueOutOfRange = 1104, - - // Resource Errors (1200-1299) - ResourceNotFound = 1200, - ResourceAlreadyExists = 1201, - ResourceConflict = 1202, - ResourceDeleted = 1203, - - // Rate Limiting (1300-1399) - RateLimitExceeded = 1300, - QuotaExceeded = 1301, - - // Database Errors (1400-1499) - DatabaseError = 1400, - DatabaseConnectionFailed = 1401, - DatabaseQueryFailed = 1402, - DatabaseConstraintViolation = 1403, - - // External Service Errors (1500-1599) - ExternalServiceError = 1500, - BlockchainError = 1501, - PaymentServiceError = 1502, - - // Internal Errors (1600-1699) - InternalServerError = 1600, - ConfigurationError = 1601, - CryptographyError = 1602, - - // Business Logic Errors (1700-1799) - BusinessRuleViolation = 1700, - InvalidStateTransition = 1701, - OperationNotAllowed = 1702, -} +use super::code::ErrorCode; +use super::response::ErrorResponse; +use super::sanitize::{sanitize_database_error, sanitize_message}; /// Application error types with detailed context #[derive(Debug, thiserror::Error)] pub enum AppError { #[error("Database error")] Database(#[source] sqlx::Error), - + #[error("Authentication failed")] Unauthorized, - + #[error("Invalid credentials")] InvalidCredentials, - + #[error("Token expired")] TokenExpired, - + #[error("Token invalid")] TokenInvalid, - + #[error("Insufficient permissions")] Forbidden(String), - + #[error("Resource not found")] NotFound(String), - + #[error("Resource already exists")] AlreadyExists(String), - + #[error("Validation error")] Validation(String), - + #[error("Rate limit exceeded")] RateLimit, - + #[error("Quota exceeded")] QuotaExceeded, - + #[error("Internal server error")] Internal(String), - + #[error("Bad request")] BadRequest(String), - + #[error("Configuration error")] Configuration(String), - + #[error("Blockchain error")] Blockchain(String), - + #[error("External service error")] ExternalService(String), - + #[error("Business rule violation")] BusinessRule(String), - + #[error("Cryptography error")] Cryptography(String), } -/// Standardized error response structure -#[derive(Debug, Serialize, Deserialize)] -pub struct ErrorResponse { - /// HTTP status code - pub status: u16, - - /// Standardized error code for programmatic handling - pub code: ErrorCode, - - /// User-friendly error message (sanitized) - pub message: String, - - /// Unique correlation ID for tracking and debugging - pub correlation_id: String, - - /// Optional additional details (only in development) - #[serde(skip_serializing_if = "Option::is_none")] - pub details: Option, -} - -impl ErrorResponse { - /// Create a new error response with correlation ID - pub fn new(status: StatusCode, code: ErrorCode, message: String) -> Self { - Self { - status: status.as_u16(), - code, - message, - correlation_id: Uuid::new_v4().to_string(), - details: None, - } - } - - /// Add details (only included in development mode) - pub fn with_details(mut self, details: String) -> Self { - // Only include details in development mode - if cfg!(debug_assertions) { - self.details = Some(details); - } - self - } -} - impl IntoResponse for AppError { fn into_response(self) -> Response { let correlation_id = Uuid::new_v4().to_string(); - + let (status, code, message, log_details) = match self { - // Database Errors - Sanitize all database details AppError::Database(ref err) => { let details = sanitize_database_error(err); tracing::error!( @@ -177,8 +86,6 @@ impl IntoResponse for AppError { Some(details), ) } - - // Authentication Errors AppError::Unauthorized => { tracing::warn!( correlation_id = %correlation_id, @@ -191,7 +98,6 @@ impl IntoResponse for AppError { None, ) } - AppError::InvalidCredentials => { tracing::warn!( correlation_id = %correlation_id, @@ -204,7 +110,6 @@ impl IntoResponse for AppError { None, ) } - AppError::TokenExpired => { tracing::info!( correlation_id = %correlation_id, @@ -217,7 +122,6 @@ impl IntoResponse for AppError { None, ) } - AppError::TokenInvalid => { tracing::warn!( correlation_id = %correlation_id, @@ -230,8 +134,6 @@ impl IntoResponse for AppError { None, ) } - - // Authorization Errors AppError::Forbidden(ref msg) => { tracing::warn!( correlation_id = %correlation_id, @@ -245,8 +147,6 @@ impl IntoResponse for AppError { Some(msg.clone()), ) } - - // Resource Errors AppError::NotFound(ref msg) => { tracing::debug!( correlation_id = %correlation_id, @@ -260,7 +160,6 @@ impl IntoResponse for AppError { None, ) } - AppError::AlreadyExists(ref msg) => { tracing::debug!( correlation_id = %correlation_id, @@ -274,8 +173,6 @@ impl IntoResponse for AppError { None, ) } - - // Validation Errors AppError::Validation(ref msg) => { tracing::debug!( correlation_id = %correlation_id, @@ -289,7 +186,6 @@ impl IntoResponse for AppError { None, ) } - AppError::BadRequest(ref msg) => { tracing::debug!( correlation_id = %correlation_id, @@ -303,8 +199,6 @@ impl IntoResponse for AppError { None, ) } - - // Rate Limiting AppError::RateLimit => { tracing::warn!( correlation_id = %correlation_id, @@ -317,7 +211,6 @@ impl IntoResponse for AppError { None, ) } - AppError::QuotaExceeded => { tracing::warn!( correlation_id = %correlation_id, @@ -330,8 +223,6 @@ impl IntoResponse for AppError { None, ) } - - // Internal Errors - Never expose internal details AppError::Internal(ref msg) => { tracing::error!( correlation_id = %correlation_id, @@ -345,7 +236,6 @@ impl IntoResponse for AppError { Some(msg.clone()), ) } - AppError::Configuration(ref msg) => { tracing::error!( correlation_id = %correlation_id, @@ -359,7 +249,6 @@ impl IntoResponse for AppError { Some(msg.clone()), ) } - AppError::Cryptography(ref msg) => { tracing::error!( correlation_id = %correlation_id, @@ -373,8 +262,6 @@ impl IntoResponse for AppError { Some(msg.clone()), ) } - - // External Service Errors AppError::Blockchain(ref msg) => { tracing::error!( correlation_id = %correlation_id, @@ -388,7 +275,6 @@ impl IntoResponse for AppError { Some(msg.clone()), ) } - AppError::ExternalService(ref msg) => { tracing::error!( correlation_id = %correlation_id, @@ -402,8 +288,6 @@ impl IntoResponse for AppError { Some(msg.clone()), ) } - - // Business Logic Errors AppError::BusinessRule(ref msg) => { tracing::info!( correlation_id = %correlation_id, @@ -421,8 +305,7 @@ impl IntoResponse for AppError { let mut response = ErrorResponse::new(status, code, message); response.correlation_id = correlation_id; - - // Only add details in debug mode + if let Some(details) = log_details { response = response.with_details(details); } @@ -431,169 +314,20 @@ impl IntoResponse for AppError { } } -/// Sanitize database errors to prevent information disclosure -fn sanitize_database_error(err: &sqlx::Error) -> String { - match err { - sqlx::Error::RowNotFound => "Record not found".to_string(), - sqlx::Error::ColumnNotFound(_) => "Database schema error".to_string(), - sqlx::Error::Database(_) => "Database constraint violation".to_string(), - sqlx::Error::PoolTimedOut => "Database connection timeout".to_string(), - sqlx::Error::PoolClosed => "Database connection closed".to_string(), - _ => "Database operation failed".to_string(), - } -} - -/// Sanitize user messages to prevent information disclosure -fn sanitize_message(msg: &str) -> String { - // Remove potential file paths - let msg = regex::Regex::new(r"(/[a-zA-Z0-9_\-./]+)") - .unwrap() - .replace_all(msg, "[path]"); - - // Remove potential SQL fragments - let msg = regex::Regex::new(r"(?i)(SELECT|INSERT|UPDATE|DELETE|FROM|WHERE|JOIN)") - .unwrap() - .replace_all(&msg, "[sql]"); - - // Remove potential connection strings - let msg = regex::Regex::new(r"(postgres|mysql|mongodb)://[^\s]+") - .unwrap() - .replace_all(&msg, "[connection]"); - - // Remove potential API keys or tokens - let msg = regex::Regex::new(r"([a-zA-Z0-9_-]{32,})") - .unwrap() - .replace_all(&msg, "[token]"); - - msg.to_string() -} - -// Convert from common error types -impl From for AppError { - fn from(err: sqlx::Error) -> Self { - // Log the actual error internally but don't expose it - tracing::error!(error = ?err, "Database error"); - AppError::Database(err) - } -} - -impl From for AppError { - fn from(err: serde_json::Error) -> Self { - tracing::debug!(error = %err, "JSON parsing error"); - AppError::BadRequest("Invalid JSON format".to_string()) - } -} - -impl From for AppError { - fn from(err: bcrypt::BcryptError) -> Self { - tracing::error!(error = ?err, "Password hashing error"); - AppError::Cryptography("Password operation failed".to_string()) - } -} - -impl From for AppError { - fn from(err: chrono::ParseError) -> Self { - tracing::debug!(error = %err, "Date parsing error"); - AppError::Validation("Invalid date format".to_string()) - } -} - -impl From for AppError { - fn from(err: std::net::AddrParseError) -> Self { - tracing::debug!(error = %err, "Address parsing error"); - AppError::Validation("Invalid network address".to_string()) - } -} - -impl From for AppError { - fn from(err: config::ConfigError) -> Self { - tracing::error!(error = ?err, "Configuration error"); - AppError::Configuration("Configuration error".to_string()) - } -} - -impl From for AppError { - fn from(err: jsonwebtoken::errors::Error) -> Self { - use jsonwebtoken::errors::ErrorKind; - - tracing::debug!(error = ?err, "JWT error"); - - match err.kind() { - ErrorKind::ExpiredSignature => AppError::TokenExpired, - ErrorKind::InvalidToken - | ErrorKind::InvalidSignature - | ErrorKind::InvalidAlgorithm - | ErrorKind::Base64(_) - | ErrorKind::Json(_) - | ErrorKind::Utf8(_) => AppError::TokenInvalid, - _ => AppError::Unauthorized, - } - } -} - -impl From for AppError { - fn from(err: uuid::Error) -> Self { - tracing::debug!(error = %err, "UUID parsing error"); - AppError::Validation("Invalid ID format".to_string()) - } -} - -impl From for AppError { - fn from(err: redis::RedisError) -> Self { - tracing::error!(error = ?err, "Redis error"); - AppError::Internal("Cache service error".to_string()) - } -} - #[cfg(test)] mod tests { use super::*; + use axum::http::StatusCode; #[test] - fn test_sanitize_message_removes_paths() { - let msg = "Error in /usr/local/app/src/main.rs"; - let sanitized = sanitize_message(msg); - assert!(sanitized.contains("[path]")); - assert!(!sanitized.contains("/usr/local")); - } - - #[test] - fn test_sanitize_message_removes_sql() { - let msg = "Error in SELECT * FROM users WHERE id = 1"; - let sanitized = sanitize_message(msg); - assert!(sanitized.contains("[sql]")); - assert!(!sanitized.contains("SELECT")); - } - - #[test] - fn test_sanitize_message_removes_connection_strings() { - let msg = "Failed to connect to postgres://user:pass@localhost:5432/db"; - let sanitized = sanitize_message(msg); - assert!(sanitized.contains("[connection]")); - assert!(!sanitized.contains("postgres://")); - } - - #[test] - fn test_sanitize_message_removes_tokens() { - let msg = "Invalid token: abc123def456ghi789jkl012mno345pqr678"; - let sanitized = sanitize_message(msg); - assert!(sanitized.contains("[token]")); + fn test_app_error_validation_into_response() { + let response = AppError::Validation("Invalid input".to_string()).into_response(); + assert_eq!(response.status(), StatusCode::BAD_REQUEST); } #[test] - fn test_error_response_excludes_details_in_release() { - let response = ErrorResponse::new( - StatusCode::INTERNAL_SERVER_ERROR, - ErrorCode::InternalServerError, - "Test error".to_string(), - ).with_details("Sensitive internal details".to_string()); - - // In release mode, details should be None - #[cfg(not(debug_assertions))] - assert!(response.details.is_none()); - - // In debug mode, details should be present - #[cfg(debug_assertions)] - assert!(response.details.is_some()); + fn test_app_error_forbidden_into_response() { + let response = AppError::Forbidden("no access".to_string()).into_response(); + assert_eq!(response.status(), StatusCode::FORBIDDEN); } } diff --git a/docs/ERROR_HANDLING_IMPLEMENTATION.md b/docs/ERROR_HANDLING_IMPLEMENTATION.md index a97bb130..647139a5 100644 --- a/docs/ERROR_HANDLING_IMPLEMENTATION.md +++ b/docs/ERROR_HANDLING_IMPLEMENTATION.md @@ -8,7 +8,7 @@ This document describes the comprehensive error handling and sanitization implem ### Components -1. **Error Types** (`backend/src/error.rs`) +1. **Error Types** (`backend/src/error/mod.rs`) - Standardized error codes for programmatic handling - Comprehensive error variants covering all failure scenarios - Automatic error conversions from common library errors From 781c7f81d683fb0742f7a262f893467c02f2deb3 Mon Sep 17 00:00:00 2001 From: Endowed992 Date: Sun, 21 Jun 2026 22:23:27 +0000 Subject: [PATCH 2/2] feat(routes): refactor and modularize route handling for improved organization --- backend/src/routes.rs | 166 +++++---------------------- backend/src/routes/admin.rs | 53 +++++++++ backend/src/routes/carbon.rs | 30 +++++ backend/src/routes/collaboration.rs | 14 +++ backend/src/routes/health.rs | 8 ++ backend/src/routes/key_management.rs | 11 ++ backend/src/routes/monitoring.rs | 15 +++ backend/src/routes/public.rs | 33 ++++++ 8 files changed, 194 insertions(+), 136 deletions(-) create mode 100644 backend/src/routes/admin.rs create mode 100644 backend/src/routes/carbon.rs create mode 100644 backend/src/routes/collaboration.rs create mode 100644 backend/src/routes/health.rs create mode 100644 backend/src/routes/key_management.rs create mode 100644 backend/src/routes/monitoring.rs create mode 100644 backend/src/routes/public.rs diff --git a/backend/src/routes.rs b/backend/src/routes.rs index 259863ce..22b14aa7 100644 --- a/backend/src/routes.rs +++ b/backend/src/routes.rs @@ -1,149 +1,43 @@ -use axum::{Router, routing::{get, post, put, delete}, middleware}; -use super::AppState; -use crate::models::UserRole; -use crate::middleware::auth::{jwt_auth, api_key_auth, require_role, require_admin}; +use axum::Router; +use crate::AppState; pub mod analytics; +pub mod admin; +pub mod carbon; +pub mod collaboration; +pub mod health; +pub mod key_management; +pub mod monitoring; +pub mod public; pub fn api_routes() -> Router { Router::new() - .nest("/api/v1", public_api_routes()) - .nest("/api/v1/admin", admin_api_routes()) - .nest("/api/v1/analytics", analytics_routes()) - .nest("/api/v1/carbon", carbon_routes()) - .nest("/api/v1/keys", key_management_routes()) - .nest("/api/v1/monitoring", monitoring_routes()) - .nest("/api/v1/collaboration", collaboration_routes()) + .nest("/api/v1", public::router()) + .nest("/api/v1/admin", admin::router()) + .nest("/api/v1/analytics", analytics::routes()) + .nest("/api/v1/carbon", carbon::router()) + .nest("/api/v1/keys", key_management::router()) + .nest("/api/v1/monitoring", monitoring::router()) + .nest("/api/v1/collaboration", collaboration::router()) } - -fn public_api_routes() -> Router { - Router::new() - .route("/products", get(crate::handlers::product::list_products)) - .route("/products/:id", get(crate::handlers::product::get_product)) - .route("/events", get(crate::handlers::event::list_events)) - .route("/events/:id", get(crate::handlers::event::get_event)) - .route("/recalls", get(crate::handlers::recall::list_recalls)) - .route("/recalls/:id", get(crate::handlers::recall::get_recall)) - .route("/recalls/:id/affected", get(crate::handlers::recall::list_affected_items)) - .route("/stats", get(crate::handlers::stats::get_stats)) - .route("/transactions", get(crate::handlers::financial::list_transactions)) - .route("/transactions/:id", get(crate::handlers::financial::get_transaction)) - .route("/compliance/check", post(crate::handlers::compliance::check_compliance) - .layer(middleware::from_fn(require_role(vec![UserRole::Inspector, UserRole::Administrator])))) - .route("/compliance/report/:product_id", get(crate::handlers::compliance::get_compliance_report) - .layer(middleware::from_fn(require_role(vec![UserRole::Auditor, UserRole::Administrator])))) - .route("/audit/report", get(crate::handlers::compliance::generate_audit_report) - .layer(middleware::from_fn(require_role(vec![UserRole::Auditor, UserRole::Administrator])))) - .layer(middleware::from_fn(api_key_auth)) - .layer(middleware::from_fn(crate::middleware::rate_limit::rate_limit_middleware)) -} - -fn admin_api_routes() -> Router { - Router::new() - .route("/products", post(crate::handlers::product::create_product)) - .route("/products/:id", put(crate::handlers::product::update_product).delete(crate::handlers::product::delete_product)) - .route("/events", post(crate::handlers::event::create_event) - .layer(middleware::from_fn(require_role(vec![UserRole::Supplier, UserRole::Carrier, UserRole::Administrator])))) - .route("/recalls", post(crate::handlers::recall::create_recall) - .layer(middleware::from_fn(require_role(vec![UserRole::Inspector, UserRole::Administrator])))) - .route("/recalls/:id/notify", post(crate::handlers::recall::notify_recall) - .layer(middleware::from_fn(require_role(vec![UserRole::Inspector, UserRole::Administrator])))) - .route("/recalls/:id/effectiveness", post(crate::handlers::recall::update_effectiveness) - .layer(middleware::from_fn(require_role(vec![UserRole::Inspector, UserRole::Administrator])))) - .route("/transactions", post(crate::handlers::financial::create_transaction) - .layer(middleware::from_fn(require_role(vec![UserRole::Supplier, UserRole::Administrator])))) - .route("/invoices", post(crate::handlers::financial::create_invoice) - .layer(middleware::from_fn(require_role(vec![UserRole::Supplier, UserRole::Administrator])))) - .route("/financing/request", post(crate::handlers::financial::request_financing) - .layer(middleware::from_fn(require_role(vec![UserRole::Supplier, UserRole::Administrator])))) - .route("/users", post(crate::handlers::user::create_user)) - .route("/users/me", get(crate::handlers::user::get_current_user)) - .route("/auth/login", post(crate::handlers::auth::login)) - .route("/auth/register", post(crate::handlers::auth::register)) - .layer(middleware::from_fn(require_admin)) - .layer(middleware::from_fn(jwt_auth)) - .layer(middleware::from_fn(crate::middleware::rate_limit::rate_limit_middleware)) -} - -// Public routes that don't require authentication pub fn health_routes() -> Router { - Router::new() - .route("/health", get(crate::handlers::health::health_check)) - .route("/health/db", get(crate::handlers::health::db_health_check)) + health::router() } -fn analytics_routes() -> Router { - Router::new() - .route("/dashboard", get(crate::routes::analytics::dashboard)) - .route("/products/:id", get(crate::routes::analytics::product_analytics)) - .route("/events", get(crate::routes::analytics::event_analytics)) - .route("/users", get(crate::routes::analytics::user_analytics)) - .route("/export", get(crate::routes::analytics::export)) - .layer(middleware::from_fn(require_role(vec![UserRole::Auditor, UserRole::Administrator]))) - .layer(middleware::from_fn(jwt_auth)) - .layer(middleware::from_fn(crate::middleware::rate_limit::rate_limit_middleware)) -} +#[cfg(test)] +mod tests { + use super::*; -fn key_management_routes() -> Router { - Router::new() - .route("/", get(crate::handlers::api_keys::list_keys).post(crate::handlers::api_keys::create_key)) - .route("/:id/revoke", post(crate::handlers::api_keys::revoke_key)) - .route("/:id/rotate", post(crate::handlers::api_keys::rotate_key)) - .layer(middleware::from_fn(jwt_auth)) - .layer(middleware::from_fn(crate::middleware::rate_limit::rate_limit_middleware)) -} - -fn carbon_routes() -> Router { - Router::new() - // Footprint - .route("/footprint/calculate", post(crate::handlers::carbon::calculate_footprint)) - .route("/footprint/preview", post(crate::handlers::carbon::preview_footprint)) - .route("/footprint/:product_id", get(crate::handlers::carbon::list_footprints)) - // Credits - .route("/credits", get(crate::handlers::carbon::list_credits)) - .route("/credits/:id", get(crate::handlers::carbon::get_credit)) - .route("/credits/generate", post(crate::handlers::carbon::generate_credit)) - .route("/credits/retire", post(crate::handlers::carbon::retire_credit)) - // Marketplace - .route("/market", get(crate::handlers::carbon::market_summary)) - .route("/market/trades", get(crate::handlers::carbon::list_trades)) - .route("/market/list", post(crate::handlers::carbon::list_credit_for_sale)) - .route("/market/purchase", post(crate::handlers::carbon::purchase_credit)) - // Verification - .route("/verify", post(crate::handlers::carbon::request_verification)) - .route("/verify/:credit_id", get(crate::handlers::carbon::list_verifications)) - // Reports - .route("/reports", get(crate::handlers::carbon::list_reports).post(crate::handlers::carbon::generate_report)) - // Dashboard and Supplier Scoring - .route("/dashboard", get(crate::handlers::carbon::get_sustainability_dashboard)) - .route("/supplier-score/:supplier_address", get(crate::handlers::carbon::get_supplier_score)) - .layer(middleware::from_fn(jwt_auth)) - - .layer(middleware::from_fn(crate::middleware::rate_limit::rate_limit_middleware)) -} + #[test] + fn api_routes_compiles() { + let router = api_routes(); + let _ = router; + } -fn monitoring_routes() -> Router { - Router::new() - .route("/dashboard", get(crate::handlers::monitoring::get_dashboard)) - .route("/errors", get(crate::handlers::monitoring::get_error_stats)) - .route("/errors/recent", get(crate::handlers::monitoring::get_recent_errors)) - .route("/performance", get(crate::handlers::monitoring::get_performance_metrics)) - .route("/infrastructure", get(crate::handlers::monitoring::get_infrastructure_metrics)) - .route("/alerts/check", post(crate::handlers::monitoring::check_alerts)) - .layer(middleware::from_fn(require_role(vec![UserRole::Auditor, UserRole::Administrator]))) - .layer(middleware::from_fn(jwt_auth)) - .layer(middleware::from_fn(crate::middleware::rate_limit::rate_limit_middleware)) + #[test] + fn health_routes_compiles() { + let router = health_routes(); + let _ = router; + } } - -fn collaboration_routes() -> Router { - Router::new() - .route("/share", post(crate::handlers::collaboration::share_product)) - .route("/shares/:product_id", get(crate::handlers::collaboration::list_shares)) - .route("/requests", post(crate::handlers::collaboration::create_collaboration_request)) - .route("/requests/:id", put(crate::handlers::collaboration::update_collaboration_request)) - .route("/audit/:entity_type/:entity_id", get(crate::handlers::collaboration::list_audit_trail)) - .layer(middleware::from_fn(jwt_auth)) - .layer(middleware::from_fn(crate::middleware::rate_limit::rate_limit_middleware)) -} - diff --git a/backend/src/routes/admin.rs b/backend/src/routes/admin.rs new file mode 100644 index 00000000..189d7b84 --- /dev/null +++ b/backend/src/routes/admin.rs @@ -0,0 +1,53 @@ +use axum::{Router, routing::{get, post, put}, middleware}; +use crate::{AppState, models::UserRole, middleware::auth::{jwt_auth, require_role, require_admin}}; + +pub fn router() -> Router { + Router::new() + .route("/products", post(crate::handlers::product::create_product)) + .route( + "/products/:id", + put(crate::handlers::product::update_product).delete(crate::handlers::product::delete_product), + ) + .route( + "/events", + post(crate::handlers::event::create_event) + .layer(middleware::from_fn(require_role(vec![UserRole::Supplier, UserRole::Carrier, UserRole::Administrator]))), + ) + .route( + "/recalls", + post(crate::handlers::recall::create_recall) + .layer(middleware::from_fn(require_role(vec![UserRole::Inspector, UserRole::Administrator]))), + ) + .route( + "/recalls/:id/notify", + post(crate::handlers::recall::notify_recall) + .layer(middleware::from_fn(require_role(vec![UserRole::Inspector, UserRole::Administrator]))), + ) + .route( + "/recalls/:id/effectiveness", + post(crate::handlers::recall::update_effectiveness) + .layer(middleware::from_fn(require_role(vec![UserRole::Inspector, UserRole::Administrator]))), + ) + .route( + "/transactions", + post(crate::handlers::financial::create_transaction) + .layer(middleware::from_fn(require_role(vec![UserRole::Supplier, UserRole::Administrator]))), + ) + .route( + "/invoices", + post(crate::handlers::financial::create_invoice) + .layer(middleware::from_fn(require_role(vec![UserRole::Supplier, UserRole::Administrator]))), + ) + .route( + "/financing/request", + post(crate::handlers::financial::request_financing) + .layer(middleware::from_fn(require_role(vec![UserRole::Supplier, UserRole::Administrator]))), + ) + .route("/users", post(crate::handlers::user::create_user)) + .route("/users/me", get(crate::handlers::user::get_current_user)) + .route("/auth/login", post(crate::handlers::auth::login)) + .route("/auth/register", post(crate::handlers::auth::register)) + .layer(middleware::from_fn(require_admin)) + .layer(middleware::from_fn(jwt_auth)) + .layer(middleware::from_fn(crate::middleware::rate_limit::rate_limit_middleware)) +} diff --git a/backend/src/routes/carbon.rs b/backend/src/routes/carbon.rs new file mode 100644 index 00000000..d3eb4350 --- /dev/null +++ b/backend/src/routes/carbon.rs @@ -0,0 +1,30 @@ +use axum::{Router, routing::{get, post}, middleware}; +use crate::{AppState, middleware::auth::jwt_auth}; + +pub fn router() -> Router { + Router::new() + // Footprint + .route("/footprint/calculate", post(crate::handlers::carbon::calculate_footprint)) + .route("/footprint/preview", post(crate::handlers::carbon::preview_footprint)) + .route("/footprint/:product_id", get(crate::handlers::carbon::list_footprints)) + // Credits + .route("/credits", get(crate::handlers::carbon::list_credits)) + .route("/credits/:id", get(crate::handlers::carbon::get_credit)) + .route("/credits/generate", post(crate::handlers::carbon::generate_credit)) + .route("/credits/retire", post(crate::handlers::carbon::retire_credit)) + // Marketplace + .route("/market", get(crate::handlers::carbon::market_summary)) + .route("/market/trades", get(crate::handlers::carbon::list_trades)) + .route("/market/list", post(crate::handlers::carbon::list_credit_for_sale)) + .route("/market/purchase", post(crate::handlers::carbon::purchase_credit)) + // Verification + .route("/verify", post(crate::handlers::carbon::request_verification)) + .route("/verify/:credit_id", get(crate::handlers::carbon::list_verifications)) + // Reports + .route("/reports", get(crate::handlers::carbon::list_reports).post(crate::handlers::carbon::generate_report)) + // Dashboard and Supplier Scoring + .route("/dashboard", get(crate::handlers::carbon::get_sustainability_dashboard)) + .route("/supplier-score/:supplier_address", get(crate::handlers::carbon::get_supplier_score)) + .layer(middleware::from_fn(jwt_auth)) + .layer(middleware::from_fn(crate::middleware::rate_limit::rate_limit_middleware)) +} diff --git a/backend/src/routes/collaboration.rs b/backend/src/routes/collaboration.rs new file mode 100644 index 00000000..2b21e856 --- /dev/null +++ b/backend/src/routes/collaboration.rs @@ -0,0 +1,14 @@ +use axum::{Router, routing::{get, post, put}, middleware}; +use crate::middleware::auth::jwt_auth; +use crate::AppState; + +pub fn router() -> Router { + Router::new() + .route("/share", post(crate::handlers::collaboration::share_product)) + .route("/shares/:product_id", get(crate::handlers::collaboration::list_shares)) + .route("/requests", post(crate::handlers::collaboration::create_collaboration_request)) + .route("/requests/:id", put(crate::handlers::collaboration::update_collaboration_request)) + .route("/audit/:entity_type/:entity_id", get(crate::handlers::collaboration::list_audit_trail)) + .layer(middleware::from_fn(jwt_auth)) + .layer(middleware::from_fn(crate::middleware::rate_limit::rate_limit_middleware)) +} diff --git a/backend/src/routes/health.rs b/backend/src/routes/health.rs new file mode 100644 index 00000000..fdcd178a --- /dev/null +++ b/backend/src/routes/health.rs @@ -0,0 +1,8 @@ +use axum::{Router, routing::get}; +use crate::AppState; + +pub fn router() -> Router { + Router::new() + .route("/health", get(crate::handlers::health::health_check)) + .route("/health/db", get(crate::handlers::health::db_health_check)) +} diff --git a/backend/src/routes/key_management.rs b/backend/src/routes/key_management.rs new file mode 100644 index 00000000..b289a2ea --- /dev/null +++ b/backend/src/routes/key_management.rs @@ -0,0 +1,11 @@ +use axum::{Router, routing::{get, post}, middleware}; +use crate::{AppState, middleware::auth::jwt_auth}; + +pub fn router() -> Router { + Router::new() + .route("/", get(crate::handlers::api_keys::list_keys).post(crate::handlers::api_keys::create_key)) + .route("/:id/revoke", post(crate::handlers::api_keys::revoke_key)) + .route("/:id/rotate", post(crate::handlers::api_keys::rotate_key)) + .layer(middleware::from_fn(jwt_auth)) + .layer(middleware::from_fn(crate::middleware::rate_limit::rate_limit_middleware)) +} diff --git a/backend/src/routes/monitoring.rs b/backend/src/routes/monitoring.rs new file mode 100644 index 00000000..d0e79da9 --- /dev/null +++ b/backend/src/routes/monitoring.rs @@ -0,0 +1,15 @@ +use axum::{Router, routing::{get, post}, middleware}; +use crate::{AppState, models::UserRole, middleware::auth::{jwt_auth, require_role}}; + +pub fn router() -> Router { + Router::new() + .route("/dashboard", get(crate::handlers::monitoring::get_dashboard)) + .route("/errors", get(crate::handlers::monitoring::get_error_stats)) + .route("/errors/recent", get(crate::handlers::monitoring::get_recent_errors)) + .route("/performance", get(crate::handlers::monitoring::get_performance_metrics)) + .route("/infrastructure", get(crate::handlers::monitoring::get_infrastructure_metrics)) + .route("/alerts/check", post(crate::handlers::monitoring::check_alerts)) + .layer(middleware::from_fn(require_role(vec![UserRole::Auditor, UserRole::Administrator]))) + .layer(middleware::from_fn(jwt_auth)) + .layer(middleware::from_fn(crate::middleware::rate_limit::rate_limit_middleware)) +} diff --git a/backend/src/routes/public.rs b/backend/src/routes/public.rs new file mode 100644 index 00000000..03b6a49d --- /dev/null +++ b/backend/src/routes/public.rs @@ -0,0 +1,33 @@ +use axum::{Router, routing::{get, post}, middleware}; +use crate::{AppState, models::UserRole, middleware::auth::{api_key_auth, require_role}}; + +pub fn router() -> Router { + Router::new() + .route("/products", get(crate::handlers::product::list_products)) + .route("/products/:id", get(crate::handlers::product::get_product)) + .route("/events", get(crate::handlers::event::list_events)) + .route("/events/:id", get(crate::handlers::event::get_event)) + .route("/recalls", get(crate::handlers::recall::list_recalls)) + .route("/recalls/:id", get(crate::handlers::recall::get_recall)) + .route("/recalls/:id/affected", get(crate::handlers::recall::list_affected_items)) + .route("/stats", get(crate::handlers::stats::get_stats)) + .route("/transactions", get(crate::handlers::financial::list_transactions)) + .route("/transactions/:id", get(crate::handlers::financial::get_transaction)) + .route( + "/compliance/check", + post(crate::handlers::compliance::check_compliance) + .layer(middleware::from_fn(require_role(vec![UserRole::Inspector, UserRole::Administrator]))), + ) + .route( + "/compliance/report/:product_id", + get(crate::handlers::compliance::get_compliance_report) + .layer(middleware::from_fn(require_role(vec![UserRole::Auditor, UserRole::Administrator]))), + ) + .route( + "/audit/report", + get(crate::handlers::compliance::generate_audit_report) + .layer(middleware::from_fn(require_role(vec![UserRole::Auditor, UserRole::Administrator]))), + ) + .layer(middleware::from_fn(api_key_auth)) + .layer(middleware::from_fn(crate::middleware::rate_limit::rate_limit_middleware)) +}