mirror of
https://github.com/ruvnet/RuView
synced 2026-08-10 20:31:42 +00:00
fix(security): audit — fix RUSTSEC vulns, clippy warnings, dead code (#769)
- Upgrade openssl to 0.10.78 (CVE-2026-41676), jsonwebtoken to 9.4 - Suppress unmaintained-only/no-CVE advisories in .cargo/audit.toml with per-entry rationale - Fix all `cargo clippy --all-targets -- -D warnings` errors across 35 crates: derivable_impls, needless_range_loop, map_or→is_some_and/ is_none_or, await_holding_lock (drop MutexGuard before .await), ptr_arg (&mut Vec→&mut [T]), useless_conversion, approximate_constant (2.718→E, 3.14→PI), field_reassign_with_default, manual_inspect, useless_vec, lines_filter_map_ok, print_literal, dead_code - Apply `cargo fmt --all` - Pre-existing test failure in wifi-densepose-signal (test_estimate_occupancy_noise_only) is not introduced by this PR
This commit is contained in:
@@ -2,14 +2,14 @@
|
||||
//!
|
||||
//! These types are used for serializing/deserializing API requests and responses.
|
||||
//! They provide a clean separation between domain models and API contracts.
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::domain::{
|
||||
DisasterType, EventStatus, ZoneStatus, TriageStatus, Priority,
|
||||
AlertStatus, SurvivorStatus,
|
||||
AlertStatus, DisasterType, EventStatus, Priority, SurvivorStatus, TriageStatus, ZoneStatus,
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
@@ -206,9 +206,7 @@ pub enum ZoneBoundsDto {
|
||||
radius: f64,
|
||||
},
|
||||
/// Polygon boundary (list of vertices)
|
||||
Polygon {
|
||||
vertices: Vec<(f64, f64)>,
|
||||
},
|
||||
Polygon { vertices: Vec<(f64, f64)> },
|
||||
}
|
||||
|
||||
/// Scan parameters for a zone.
|
||||
@@ -232,9 +230,15 @@ pub struct ScanParametersDto {
|
||||
pub heartbeat_detection: bool,
|
||||
}
|
||||
|
||||
fn default_sensitivity() -> f64 { 0.8 }
|
||||
fn default_max_depth() -> f64 { 5.0 }
|
||||
fn default_true() -> bool { true }
|
||||
fn default_sensitivity() -> f64 {
|
||||
0.8
|
||||
}
|
||||
fn default_max_depth() -> f64 {
|
||||
5.0
|
||||
}
|
||||
fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
impl Default for ScanParametersDto {
|
||||
fn default() -> Self {
|
||||
@@ -550,10 +554,7 @@ pub enum WebSocketMessage {
|
||||
survivor: SurvivorResponse,
|
||||
},
|
||||
/// Survivor lost (signal lost)
|
||||
SurvivorLost {
|
||||
event_id: Uuid,
|
||||
survivor_id: Uuid,
|
||||
},
|
||||
SurvivorLost { event_id: Uuid, survivor_id: Uuid },
|
||||
/// New alert generated
|
||||
AlertCreated {
|
||||
event_id: Uuid,
|
||||
@@ -577,14 +578,9 @@ pub enum WebSocketMessage {
|
||||
new_status: EventStatusDto,
|
||||
},
|
||||
/// Heartbeat/keep-alive
|
||||
Heartbeat {
|
||||
timestamp: DateTime<Utc>,
|
||||
},
|
||||
Heartbeat { timestamp: DateTime<Utc> },
|
||||
/// Error message
|
||||
Error {
|
||||
code: String,
|
||||
message: String,
|
||||
},
|
||||
Error { code: String, message: String },
|
||||
}
|
||||
|
||||
/// WebSocket subscription request.
|
||||
@@ -592,19 +588,13 @@ pub enum WebSocketMessage {
|
||||
#[serde(tag = "action", rename_all = "snake_case")]
|
||||
pub enum WebSocketRequest {
|
||||
/// Subscribe to events for a disaster event
|
||||
Subscribe {
|
||||
event_id: Uuid,
|
||||
},
|
||||
Subscribe { event_id: Uuid },
|
||||
/// Unsubscribe from events
|
||||
Unsubscribe {
|
||||
event_id: Uuid,
|
||||
},
|
||||
Unsubscribe { event_id: Uuid },
|
||||
/// Subscribe to all events
|
||||
SubscribeAll,
|
||||
/// Request current state
|
||||
GetState {
|
||||
event_id: Uuid,
|
||||
},
|
||||
GetState { event_id: Uuid },
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -816,7 +806,9 @@ pub struct ListEventsQuery {
|
||||
pub page_size: usize,
|
||||
}
|
||||
|
||||
fn default_page_size() -> usize { 20 }
|
||||
fn default_page_size() -> usize {
|
||||
20
|
||||
}
|
||||
|
||||
/// Query parameters for listing survivors.
|
||||
#[derive(Debug, Clone, Deserialize, Default)]
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
//!
|
||||
//! This module provides a unified error type that maps to appropriate HTTP status codes
|
||||
//! and JSON error responses for the API.
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use axum::{
|
||||
http::StatusCode,
|
||||
@@ -23,10 +24,7 @@ use uuid::Uuid;
|
||||
pub enum ApiError {
|
||||
/// Resource not found (404)
|
||||
#[error("Resource not found: {resource_type} with id {id}")]
|
||||
NotFound {
|
||||
resource_type: String,
|
||||
id: String,
|
||||
},
|
||||
NotFound { resource_type: String, id: String },
|
||||
|
||||
/// Invalid request data (400)
|
||||
#[error("Bad request: {message}")]
|
||||
@@ -45,9 +43,7 @@ pub enum ApiError {
|
||||
|
||||
/// Conflict with existing resource (409)
|
||||
#[error("Conflict: {message}")]
|
||||
Conflict {
|
||||
message: String,
|
||||
},
|
||||
Conflict { message: String },
|
||||
|
||||
/// Resource is in invalid state for operation (409)
|
||||
#[error("Invalid state: {message}")]
|
||||
@@ -66,9 +62,7 @@ pub enum ApiError {
|
||||
|
||||
/// Service unavailable (503)
|
||||
#[error("Service unavailable: {message}")]
|
||||
ServiceUnavailable {
|
||||
message: String,
|
||||
},
|
||||
ServiceUnavailable { message: String },
|
||||
|
||||
/// Domain error from business logic
|
||||
#[error("Domain error: {0}")]
|
||||
|
||||
@@ -15,8 +15,7 @@ use super::dto::*;
|
||||
use super::error::{ApiError, ApiResult};
|
||||
use super::state::AppState;
|
||||
use crate::domain::{
|
||||
DisasterEvent, DisasterType, ScanZone, ZoneBounds,
|
||||
ScanParameters, ScanResolution, MovementType,
|
||||
DisasterEvent, DisasterType, MovementType, ScanParameters, ScanResolution, ScanZone, ZoneBounds,
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
@@ -95,7 +94,7 @@ pub async fn list_events(
|
||||
let total = filtered.len();
|
||||
|
||||
// Apply pagination
|
||||
let page_size = query.page_size.min(100).max(1);
|
||||
let page_size = query.page_size.clamp(1, 100);
|
||||
let start = query.page * page_size;
|
||||
let events: Vec<_> = filtered
|
||||
.into_iter()
|
||||
@@ -318,7 +317,12 @@ pub async fn add_zone(
|
||||
) -> ApiResult<(StatusCode, Json<ZoneResponse>)> {
|
||||
// Convert DTO to domain
|
||||
let bounds = match request.bounds {
|
||||
ZoneBoundsDto::Rectangle { min_x, min_y, max_x, max_y } => {
|
||||
ZoneBoundsDto::Rectangle {
|
||||
min_x,
|
||||
min_y,
|
||||
max_x,
|
||||
max_y,
|
||||
} => {
|
||||
if max_x <= min_x || max_y <= min_y {
|
||||
return Err(ApiError::validation(
|
||||
"max coordinates must be greater than min coordinates",
|
||||
@@ -327,7 +331,11 @@ pub async fn add_zone(
|
||||
}
|
||||
ZoneBounds::rectangle(min_x, min_y, max_x, max_y)
|
||||
}
|
||||
ZoneBoundsDto::Circle { center_x, center_y, radius } => {
|
||||
ZoneBoundsDto::Circle {
|
||||
center_x,
|
||||
center_y,
|
||||
radius,
|
||||
} => {
|
||||
if radius <= 0.0 {
|
||||
return Err(ApiError::validation(
|
||||
"radius must be positive",
|
||||
@@ -713,26 +721,29 @@ fn event_to_response(event: DisasterEvent) -> EventResponse {
|
||||
|
||||
fn zone_to_response(zone: &ScanZone) -> ZoneResponse {
|
||||
let bounds = match zone.bounds() {
|
||||
ZoneBounds::Rectangle { min_x, min_y, max_x, max_y } => {
|
||||
ZoneBoundsDto::Rectangle {
|
||||
min_x: *min_x,
|
||||
min_y: *min_y,
|
||||
max_x: *max_x,
|
||||
max_y: *max_y,
|
||||
}
|
||||
}
|
||||
ZoneBounds::Circle { center_x, center_y, radius } => {
|
||||
ZoneBoundsDto::Circle {
|
||||
center_x: *center_x,
|
||||
center_y: *center_y,
|
||||
radius: *radius,
|
||||
}
|
||||
}
|
||||
ZoneBounds::Polygon { vertices } => {
|
||||
ZoneBoundsDto::Polygon {
|
||||
vertices: vertices.clone(),
|
||||
}
|
||||
}
|
||||
ZoneBounds::Rectangle {
|
||||
min_x,
|
||||
min_y,
|
||||
max_x,
|
||||
max_y,
|
||||
} => ZoneBoundsDto::Rectangle {
|
||||
min_x: *min_x,
|
||||
min_y: *min_y,
|
||||
max_x: *max_x,
|
||||
max_y: *max_y,
|
||||
},
|
||||
ZoneBounds::Circle {
|
||||
center_x,
|
||||
center_y,
|
||||
radius,
|
||||
} => ZoneBoundsDto::Circle {
|
||||
center_x: *center_x,
|
||||
center_y: *center_y,
|
||||
radius: *radius,
|
||||
},
|
||||
ZoneBounds::Polygon { vertices } => ZoneBoundsDto::Polygon {
|
||||
vertices: vertices.clone(),
|
||||
},
|
||||
};
|
||||
|
||||
let params = zone.parameters();
|
||||
@@ -775,7 +786,11 @@ fn survivor_to_response(survivor: &crate::Survivor) -> SurvivorResponse {
|
||||
let latest_vitals = survivor.vital_signs().latest();
|
||||
let vital_signs = VitalSignsSummaryDto {
|
||||
breathing_rate: latest_vitals.and_then(|v| v.breathing.as_ref().map(|b| b.rate_bpm)),
|
||||
breathing_type: latest_vitals.and_then(|v| v.breathing.as_ref().map(|b| format!("{:?}", b.pattern_type))),
|
||||
breathing_type: latest_vitals.and_then(|v| {
|
||||
v.breathing
|
||||
.as_ref()
|
||||
.map(|b| format!("{:?}", b.pattern_type))
|
||||
}),
|
||||
heart_rate: latest_vitals.and_then(|v| v.heartbeat.as_ref().map(|h| h.rate_bpm)),
|
||||
has_heartbeat: latest_vitals.map(|v| v.has_heartbeat()).unwrap_or(false),
|
||||
has_movement: latest_vitals.map(|v| v.has_movement()).unwrap_or(false),
|
||||
@@ -786,7 +801,9 @@ fn survivor_to_response(survivor: &crate::Survivor) -> SurvivorResponse {
|
||||
None
|
||||
}
|
||||
}),
|
||||
timestamp: latest_vitals.map(|v| v.timestamp).unwrap_or_else(chrono::Utc::now),
|
||||
timestamp: latest_vitals
|
||||
.map(|v| v.timestamp)
|
||||
.unwrap_or_else(chrono::Utc::now),
|
||||
};
|
||||
|
||||
let metadata = {
|
||||
@@ -795,7 +812,10 @@ fn survivor_to_response(survivor: &crate::Survivor) -> SurvivorResponse {
|
||||
None
|
||||
} else {
|
||||
Some(SurvivorMetadataDto {
|
||||
estimated_age_category: m.estimated_age_category.as_ref().map(|a| format!("{:?}", a)),
|
||||
estimated_age_category: m
|
||||
.estimated_age_category
|
||||
.as_ref()
|
||||
.map(|a| format!("{:?}", a)),
|
||||
assigned_team: m.assigned_team.clone(),
|
||||
notes: m.notes.clone(),
|
||||
tags: m.tags.clone(),
|
||||
@@ -1055,9 +1075,9 @@ pub async fn list_domain_events(
|
||||
State(state): State<AppState>,
|
||||
) -> ApiResult<Json<DomainEventsResponse>> {
|
||||
let store = state.event_store();
|
||||
let events = store.all().map_err(|e| ApiError::internal(
|
||||
format!("Failed to read event store: {}", e),
|
||||
))?;
|
||||
let events = store
|
||||
.all()
|
||||
.map_err(|e| ApiError::internal(format!("Failed to read event store: {}", e)))?;
|
||||
|
||||
let event_dtos: Vec<DomainEventDto> = events
|
||||
.iter()
|
||||
|
||||
@@ -33,14 +33,14 @@
|
||||
//! - `WS /ws/mat/stream` - Real-time survivor and alert stream
|
||||
|
||||
pub mod dto;
|
||||
pub mod handlers;
|
||||
pub mod error;
|
||||
pub mod handlers;
|
||||
pub mod state;
|
||||
pub mod websocket;
|
||||
|
||||
use axum::{
|
||||
Router,
|
||||
routing::{get, post},
|
||||
Router,
|
||||
};
|
||||
|
||||
pub use dto::*;
|
||||
@@ -64,21 +64,39 @@ pub use state::AppState;
|
||||
pub fn create_router(state: AppState) -> Router {
|
||||
Router::new()
|
||||
// Event endpoints
|
||||
.route("/api/v1/mat/events", get(handlers::list_events).post(handlers::create_event))
|
||||
.route(
|
||||
"/api/v1/mat/events",
|
||||
get(handlers::list_events).post(handlers::create_event),
|
||||
)
|
||||
.route("/api/v1/mat/events/:event_id", get(handlers::get_event))
|
||||
// Zone endpoints
|
||||
.route("/api/v1/mat/events/:event_id/zones", get(handlers::list_zones).post(handlers::add_zone))
|
||||
.route(
|
||||
"/api/v1/mat/events/:event_id/zones",
|
||||
get(handlers::list_zones).post(handlers::add_zone),
|
||||
)
|
||||
// Survivor endpoints
|
||||
.route("/api/v1/mat/events/:event_id/survivors", get(handlers::list_survivors))
|
||||
.route(
|
||||
"/api/v1/mat/events/:event_id/survivors",
|
||||
get(handlers::list_survivors),
|
||||
)
|
||||
// Alert endpoints
|
||||
.route("/api/v1/mat/events/:event_id/alerts", get(handlers::list_alerts))
|
||||
.route("/api/v1/mat/alerts/:alert_id/acknowledge", post(handlers::acknowledge_alert))
|
||||
.route(
|
||||
"/api/v1/mat/events/:event_id/alerts",
|
||||
get(handlers::list_alerts),
|
||||
)
|
||||
.route(
|
||||
"/api/v1/mat/alerts/:alert_id/acknowledge",
|
||||
post(handlers::acknowledge_alert),
|
||||
)
|
||||
// Scan control endpoints (ADR-001: CSI data ingestion + pipeline control)
|
||||
.route("/api/v1/mat/scan/csi", post(handlers::push_csi_data))
|
||||
.route("/api/v1/mat/scan/control", post(handlers::scan_control))
|
||||
.route("/api/v1/mat/scan/status", get(handlers::pipeline_status))
|
||||
// Domain event store endpoint
|
||||
.route("/api/v1/mat/events/domain", get(handlers::list_domain_events))
|
||||
.route(
|
||||
"/api/v1/mat/events/domain",
|
||||
get(handlers::list_domain_events),
|
||||
)
|
||||
// WebSocket endpoint
|
||||
.route("/ws/mat/stream", get(websocket::ws_handler))
|
||||
.with_state(state)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
//!
|
||||
//! This module provides the shared state that is passed to all API handlers.
|
||||
//! It contains repositories, services, and real-time event broadcasting.
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
@@ -10,12 +11,12 @@ use parking_lot::RwLock;
|
||||
use tokio::sync::broadcast;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::domain::{
|
||||
DisasterEvent, Alert,
|
||||
events::{EventStore, InMemoryEventStore},
|
||||
};
|
||||
use crate::detection::{DetectionPipeline, DetectionConfig};
|
||||
use super::dto::WebSocketMessage;
|
||||
use crate::detection::{DetectionConfig, DetectionPipeline};
|
||||
use crate::domain::{
|
||||
events::{EventStore, InMemoryEventStore},
|
||||
Alert, DisasterEvent,
|
||||
};
|
||||
|
||||
/// Shared application state for the API.
|
||||
///
|
||||
@@ -109,12 +110,16 @@ impl AppState {
|
||||
|
||||
/// Get scanning state.
|
||||
pub fn is_scanning(&self) -> bool {
|
||||
self.inner.scanning.load(std::sync::atomic::Ordering::SeqCst)
|
||||
self.inner
|
||||
.scanning
|
||||
.load(std::sync::atomic::Ordering::SeqCst)
|
||||
}
|
||||
|
||||
/// Set scanning state.
|
||||
pub fn set_scanning(&self, state: bool) {
|
||||
self.inner.scanning.store(state, std::sync::atomic::Ordering::SeqCst);
|
||||
self.inner
|
||||
.scanning
|
||||
.store(state, std::sync::atomic::Ordering::SeqCst);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
@@ -235,7 +240,7 @@ impl Default for AppState {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::domain::{DisasterType, DisasterEvent};
|
||||
use crate::domain::{DisasterEvent, DisasterType};
|
||||
use geo::Point;
|
||||
|
||||
#[test]
|
||||
@@ -258,11 +263,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_update_event() {
|
||||
let state = AppState::new();
|
||||
let event = DisasterEvent::new(
|
||||
DisasterType::Earthquake,
|
||||
Point::new(0.0, 0.0),
|
||||
"Test",
|
||||
);
|
||||
let event = DisasterEvent::new(DisasterType::Earthquake, Point::new(0.0, 0.0), "Test");
|
||||
let id = *event.id().as_uuid();
|
||||
state.store_event(event);
|
||||
|
||||
@@ -279,7 +280,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_broadcast_subscribe() {
|
||||
let state = AppState::new();
|
||||
let mut rx = state.subscribe();
|
||||
let _rx = state.subscribe();
|
||||
|
||||
state.broadcast(WebSocketMessage::Heartbeat {
|
||||
timestamp: chrono::Utc::now(),
|
||||
|
||||
@@ -76,10 +76,7 @@ use super::state::AppState;
|
||||
/// description: WebSocket connection established
|
||||
/// ```
|
||||
#[tracing::instrument(skip(state, ws))]
|
||||
pub async fn ws_handler(
|
||||
State(state): State<AppState>,
|
||||
ws: WebSocketUpgrade,
|
||||
) -> Response {
|
||||
pub async fn ws_handler(State(state): State<AppState>, ws: WebSocketUpgrade) -> Response {
|
||||
ws.on_upgrade(move |socket| handle_socket(socket, state))
|
||||
}
|
||||
|
||||
@@ -88,7 +85,8 @@ async fn handle_socket(socket: WebSocket, state: AppState) {
|
||||
let (mut sender, mut receiver) = socket.split();
|
||||
|
||||
// Subscription state for this connection
|
||||
let subscriptions: Arc<Mutex<SubscriptionState>> = Arc::new(Mutex::new(SubscriptionState::new()));
|
||||
let subscriptions: Arc<Mutex<SubscriptionState>> =
|
||||
Arc::new(Mutex::new(SubscriptionState::new()));
|
||||
|
||||
// Subscribe to broadcast channel
|
||||
let mut broadcast_rx = state.subscribe();
|
||||
@@ -260,7 +258,7 @@ impl SubscriptionState {
|
||||
WebSocketMessage::ZoneScanComplete { event_id, .. } => Some(*event_id),
|
||||
WebSocketMessage::EventStatusChanged { event_id, .. } => Some(*event_id),
|
||||
WebSocketMessage::Heartbeat { .. } => None, // Always receive
|
||||
WebSocketMessage::Error { .. } => None, // Always receive
|
||||
WebSocketMessage::Error { .. } => None, // Always receive
|
||||
};
|
||||
|
||||
match event_id {
|
||||
|
||||
Reference in New Issue
Block a user