use axum::{extract::{State, Path, Query, Json}, http::StatusCode}; use serde::Deserialize; use sqlx::Row; use crate::AppState; use super::ApiResponse; use crate::tcp::push_to_targets; use csm_protocol::{MessageType, UsbPolicyPayload, UsbDeviceRule}; #[derive(Debug, Deserialize)] pub struct UsbEventListParams { pub device_uid: Option, pub event_type: Option, pub page: Option, pub page_size: Option, } pub async fn list_events( State(state): State, Query(params): Query, ) -> Json> { let limit = params.page_size.unwrap_or(20).min(100); let offset = params.page.unwrap_or(1).saturating_sub(1) * limit; // Normalize empty strings to None let device_uid = params.device_uid.as_deref().filter(|s| !s.is_empty()).map(String::from); let event_type = params.event_type.as_deref().filter(|s| !s.is_empty()).map(String::from); let rows = sqlx::query( "SELECT id, device_uid, vendor_id, product_id, serial_number, device_name, event_type, event_time FROM usb_events WHERE 1=1 AND (? IS NULL OR device_uid = ?) AND (? IS NULL OR event_type = ?) ORDER BY event_time DESC LIMIT ? OFFSET ?" ) .bind(&device_uid).bind(&device_uid) .bind(&event_type).bind(&event_type) .bind(limit).bind(offset) .fetch_all(&state.db) .await; match rows { Ok(records) => { let items: Vec = records.iter().map(|r| serde_json::json!({ "id": r.get::("id"), "device_uid": r.get::("device_uid"), "vendor_id": r.get::, _>("vendor_id"), "product_id": r.get::, _>("product_id"), "serial_number": r.get::, _>("serial_number"), "device_name": r.get::, _>("device_name"), "event_type": r.get::("event_type"), "event_time": r.get::("event_time"), })).collect(); Json(ApiResponse::ok(serde_json::json!({ "events": items, "page": params.page.unwrap_or(1), "page_size": limit, }))) } Err(e) => Json(ApiResponse::internal_error("query usb events", e)), } } pub async fn list_policies( State(state): State, ) -> Json> { let rows = sqlx::query( "SELECT id, name, policy_type, target_group, rules, enabled, created_at, updated_at FROM usb_policies ORDER BY created_at DESC LIMIT 500" ) .fetch_all(&state.db) .await; match rows { Ok(records) => { let items: Vec = records.iter().map(|r| serde_json::json!({ "id": r.get::("id"), "name": r.get::("name"), "policy_type": r.get::("policy_type"), "target_group": r.get::, _>("target_group"), "rules": r.get::("rules"), "enabled": r.get::("enabled"), "created_at": r.get::("created_at"), "updated_at": r.get::("updated_at"), })).collect(); Json(ApiResponse::ok(serde_json::json!({ "policies": items, }))) } Err(e) => Json(ApiResponse::internal_error("query usb policies", e)), } } #[derive(Debug, Deserialize)] pub struct CreatePolicyRequest { pub name: String, pub policy_type: String, pub target_group: Option, pub rules: String, pub enabled: Option, } pub async fn create_policy( State(state): State, Json(body): Json, ) -> (StatusCode, Json>) { let enabled = body.enabled.unwrap_or(1); // Input validation if body.name.trim().is_empty() || body.name.len() > 100 { return (StatusCode::BAD_REQUEST, Json(ApiResponse::error("name must be 1-100 chars"))); } let result = sqlx::query( "INSERT INTO usb_policies (name, policy_type, target_group, rules, enabled) VALUES (?, ?, ?, ?, ?)" ) .bind(&body.name) .bind(&body.policy_type) .bind(&body.target_group) .bind(&body.rules) .bind(enabled) .execute(&state.db) .await; match result { Ok(r) => { let new_id = r.last_insert_rowid(); // Push USB policy to matching online clients if enabled == 1 { let payload = build_usb_policy_payload(&body.policy_type, true, &body.rules); let target_group = body.target_group.as_deref(); push_to_targets(&state.db, &state.clients, MessageType::UsbPolicyUpdate, &payload, "group", target_group).await; } (StatusCode::CREATED, Json(ApiResponse::ok(serde_json::json!({ "id": new_id, })))) } Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(ApiResponse::internal_error("create usb policy", e))), } } #[derive(Debug, Deserialize)] pub struct UpdatePolicyRequest { pub name: Option, pub policy_type: Option, pub target_group: Option, pub rules: Option, pub enabled: Option, } pub async fn update_policy( State(state): State, Path(id): Path, Json(body): Json, ) -> Json> { // Fetch existing policy let existing = sqlx::query("SELECT * FROM usb_policies WHERE id = ?") .bind(id) .fetch_optional(&state.db) .await; let existing = match existing { Ok(Some(row)) => row, Ok(None) => return Json(ApiResponse::error("Policy not found")), Err(e) => return Json(ApiResponse::internal_error("query usb policy", e)), }; let name = body.name.unwrap_or_else(|| existing.get::("name")); let policy_type = body.policy_type.unwrap_or_else(|| existing.get::("policy_type")); let target_group = body.target_group.or_else(|| existing.get::, _>("target_group")); let rules = body.rules.unwrap_or_else(|| existing.get::("rules")); let enabled = body.enabled.unwrap_or_else(|| existing.get::("enabled")); let result = sqlx::query( "UPDATE usb_policies SET name = ?, policy_type = ?, target_group = ?, rules = ?, enabled = ?, updated_at = datetime('now') WHERE id = ?" ) .bind(&name) .bind(&policy_type) .bind(&target_group) .bind(&rules) .bind(enabled) .bind(id) .execute(&state.db) .await; match result { Ok(_) => { // Push updated USB policy to matching online clients let payload = build_usb_policy_payload(&policy_type, enabled == 1, &rules); let target_group = target_group.as_deref(); push_to_targets(&state.db, &state.clients, MessageType::UsbPolicyUpdate, &payload, "group", target_group).await; Json(ApiResponse::ok(serde_json::json!({"updated": true}))) } Err(e) => Json(ApiResponse::internal_error("update usb policy", e)), } } pub async fn delete_policy( State(state): State, Path(id): Path, ) -> Json> { // Fetch existing policy to get target info for push let existing = sqlx::query("SELECT target_group FROM usb_policies WHERE id = ?") .bind(id) .fetch_optional(&state.db) .await; let target_group = match existing { Ok(Some(row)) => row.get::, _>("target_group"), _ => return Json(ApiResponse::error("Policy not found")), }; let result = sqlx::query("DELETE FROM usb_policies WHERE id = ?") .bind(id) .execute(&state.db) .await; match result { Ok(r) => { if r.rows_affected() > 0 { // Push disabled policy to clients let disabled = UsbPolicyPayload { policy_type: String::new(), enabled: false, rules: vec![], }; push_to_targets(&state.db, &state.clients, MessageType::UsbPolicyUpdate, &disabled, "group", target_group.as_deref()).await; Json(ApiResponse::ok(serde_json::json!({"deleted": true}))) } else { Json(ApiResponse::error("Policy not found")) } } Err(e) => Json(ApiResponse::internal_error("delete usb policy", e)), } } /// Build a UsbPolicyPayload from raw policy fields fn build_usb_policy_payload(policy_type: &str, enabled: bool, rules_json: &str) -> UsbPolicyPayload { let raw_rules: Vec = serde_json::from_str(rules_json).unwrap_or_default(); let rules: Vec = raw_rules.iter().map(|r| UsbDeviceRule { vendor_id: r.get("vendor_id").and_then(|v| v.as_str().map(String::from)), product_id: r.get("product_id").and_then(|v| v.as_str().map(String::from)), serial: r.get("serial").and_then(|v| v.as_str().map(String::from)), device_name: r.get("device_name").and_then(|v| v.as_str().map(String::from)), }).collect(); UsbPolicyPayload { policy_type: policy_type.to_string(), enabled, rules, } }