use axum::{extract::{State, Path, Json}, http::StatusCode}; use serde::Deserialize; use sqlx::Row; use csm_protocol::MessageType; use crate::AppState; use crate::api::ApiResponse; use crate::tcp::push_to_targets; #[derive(Debug, Deserialize)] pub struct CreateRuleRequest { pub rule_type: String, // "block" | "allow" pub window_title: Option, pub window_class: Option, pub process_name: Option, pub target_type: Option, pub target_id: Option, } pub async fn list_rules(State(state): State) -> Json> { match sqlx::query("SELECT id, rule_type, window_title, window_class, process_name, target_type, target_id, enabled, created_at FROM popup_filter_rules ORDER BY created_at DESC") .fetch_all(&state.db).await { Ok(rows) => Json(ApiResponse::ok(serde_json::json!({"rules": rows.iter().map(|r| serde_json::json!({ "id": r.get::("id"), "rule_type": r.get::("rule_type"), "window_title": r.get::,_>("window_title"), "window_class": r.get::,_>("window_class"), "process_name": r.get::,_>("process_name"), "target_type": r.get::("target_type"), "target_id": r.get::,_>("target_id"), "enabled": r.get::("enabled"), "created_at": r.get::("created_at") })).collect::>()}))), Err(e) => Json(ApiResponse::internal_error("query popup filter rules", e)), } } pub async fn create_rule(State(state): State, Json(req): Json) -> (StatusCode, Json>) { let target_type = req.target_type.unwrap_or_else(|| "global".to_string()); // Validate inputs if !matches!(req.rule_type.as_str(), "block" | "allow") { return (StatusCode::BAD_REQUEST, Json(ApiResponse::error("rule_type must be 'block' or 'allow'"))); } if !matches!(target_type.as_str(), "global" | "device" | "group") { return (StatusCode::BAD_REQUEST, Json(ApiResponse::error("invalid target_type"))); } let has_filter = req.window_title.as_ref().map_or(false, |s| !s.is_empty()) || req.window_class.as_ref().map_or(false, |s| !s.is_empty()) || req.process_name.as_ref().map_or(false, |s| !s.is_empty()); if !has_filter { return (StatusCode::BAD_REQUEST, Json(ApiResponse::error("at least one filter (window_title/window_class/process_name) required"))); } match sqlx::query("INSERT INTO popup_filter_rules (rule_type, window_title, window_class, process_name, target_type, target_id) VALUES (?,?,?,?,?,?)") .bind(&req.rule_type).bind(&req.window_title).bind(&req.window_class).bind(&req.process_name).bind(&target_type).bind(&req.target_id) .execute(&state.db).await { Ok(r) => { let new_id = r.last_insert_rowid(); let rules = fetch_popup_rules_for_push(&state.db, &target_type, req.target_id.as_deref()).await; push_to_targets(&state.db, &state.clients, MessageType::PopupRules, &serde_json::json!({"rules": rules}), &target_type, req.target_id.as_deref()).await; (StatusCode::CREATED, Json(ApiResponse::ok(serde_json::json!({"id": new_id})))) } Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(ApiResponse::internal_error("create popup filter rule", e))), } } #[derive(Debug, Deserialize)] pub struct UpdateRuleRequest { pub window_title: Option, pub window_class: Option, pub process_name: Option, pub enabled: Option } pub async fn update_rule(State(state): State, Path(id): Path, Json(body): Json) -> Json> { let existing = sqlx::query("SELECT * FROM popup_filter_rules WHERE id = ?") .bind(id) .fetch_optional(&state.db) .await; let existing = match existing { Ok(Some(row)) => row, Ok(None) => return Json(ApiResponse::error("Not found")), Err(e) => return Json(ApiResponse::internal_error("query popup filter rule", e)), }; let window_title = body.window_title.or_else(|| existing.get::, _>("window_title")); let window_class = body.window_class.or_else(|| existing.get::, _>("window_class")); let process_name = body.process_name.or_else(|| existing.get::, _>("process_name")); let enabled = body.enabled.unwrap_or_else(|| existing.get::("enabled")); let result = sqlx::query("UPDATE popup_filter_rules SET window_title = ?, window_class = ?, process_name = ?, enabled = ? WHERE id = ?") .bind(&window_title) .bind(&window_class) .bind(&process_name) .bind(enabled) .bind(id) .execute(&state.db) .await; match result { Ok(r) if r.rows_affected() > 0 => { let target_type_val: String = existing.get("target_type"); let target_id_val: Option = existing.get("target_id"); let rules = fetch_popup_rules_for_push(&state.db, &target_type_val, target_id_val.as_deref()).await; push_to_targets(&state.db, &state.clients, MessageType::PopupRules, &serde_json::json!({"rules": rules}), &target_type_val, target_id_val.as_deref()).await; Json(ApiResponse::ok(())) } Ok(_) => Json(ApiResponse::error("Not found")), Err(e) => Json(ApiResponse::internal_error("update popup filter rule", e)), } } pub async fn delete_rule(State(state): State, Path(id): Path) -> Json> { let existing = sqlx::query("SELECT target_type, target_id FROM popup_filter_rules WHERE id = ?") .bind(id).fetch_optional(&state.db).await; let (target_type, target_id) = match existing { Ok(Some(row)) => (row.get::("target_type"), row.get::, _>("target_id")), _ => return Json(ApiResponse::error("Not found")), }; match sqlx::query("DELETE FROM popup_filter_rules WHERE id=?").bind(id).execute(&state.db).await { Ok(r) if r.rows_affected() > 0 => { let rules = fetch_popup_rules_for_push(&state.db, &target_type, target_id.as_deref()).await; push_to_targets(&state.db, &state.clients, MessageType::PopupRules, &serde_json::json!({"rules": rules}), &target_type, target_id.as_deref()).await; Json(ApiResponse::ok(())) } _ => Json(ApiResponse::error("Not found")), } } pub async fn list_stats(State(state): State) -> Json> { match sqlx::query("SELECT device_uid, blocked_count, date FROM popup_block_stats ORDER BY date DESC LIMIT 30") .fetch_all(&state.db).await { Ok(rows) => Json(ApiResponse::ok(serde_json::json!({"stats": rows.iter().map(|r| serde_json::json!({ "device_uid": r.get::("device_uid"), "blocked_count": r.get::("blocked_count"), "date": r.get::("date") })).collect::>()}))), Err(e) => Json(ApiResponse::internal_error("query popup block stats", e)), } } async fn fetch_popup_rules_for_push( db: &sqlx::SqlitePool, target_type: &str, target_id: Option<&str>, ) -> Vec { let query = match target_type { "device" => sqlx::query( "SELECT id, rule_type, window_title, window_class, process_name FROM popup_filter_rules WHERE enabled = 1 AND (target_type = 'global' OR (target_type = 'device' AND target_id = ?))" ).bind(target_id), _ => sqlx::query( "SELECT id, rule_type, window_title, window_class, process_name FROM popup_filter_rules WHERE enabled = 1 AND target_type = 'global'" ), }; query.fetch_all(db).await .map(|rows| rows.iter().map(|r| serde_json::json!({ "id": r.get::("id"), "rule_type": r.get::("rule_type"), "window_title": r.get::,_>("window_title"), "window_class": r.get::,_>("window_class"), "process_name": r.get::,_>("process_name"), })).collect()) .unwrap_or_default() }