- 添加项目基础结构:Cargo.toml、.gitignore、设备UID和密钥文件 - 实现前端Vue3项目结构:路由、登录页面、设备管理页面 - 添加核心协议定义(crates/protocol):设备状态、资产、USB事件等 - 实现客户端监控模块:系统状态收集、资产收集 - 实现服务端基础API和插件系统 - 添加数据库迁移脚本:设备管理、资产跟踪、告警系统等 - 实现前端设备状态展示和基本交互 - 添加使用时长统计和水印功能插件
156 lines
8.1 KiB
Rust
156 lines
8.1 KiB
Rust
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<String>,
|
|
pub window_class: Option<String>,
|
|
pub process_name: Option<String>,
|
|
pub target_type: Option<String>,
|
|
pub target_id: Option<String>,
|
|
}
|
|
|
|
pub async fn list_rules(State(state): State<AppState>) -> Json<ApiResponse<serde_json::Value>> {
|
|
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::<i64,_>("id"), "rule_type": r.get::<String,_>("rule_type"),
|
|
"window_title": r.get::<Option<String>,_>("window_title"),
|
|
"window_class": r.get::<Option<String>,_>("window_class"),
|
|
"process_name": r.get::<Option<String>,_>("process_name"),
|
|
"target_type": r.get::<String,_>("target_type"), "target_id": r.get::<Option<String>,_>("target_id"),
|
|
"enabled": r.get::<bool,_>("enabled"), "created_at": r.get::<String,_>("created_at")
|
|
})).collect::<Vec<_>>()}))),
|
|
Err(e) => Json(ApiResponse::internal_error("query popup filter rules", e)),
|
|
}
|
|
}
|
|
|
|
pub async fn create_rule(State(state): State<AppState>, Json(req): Json<CreateRuleRequest>) -> (StatusCode, Json<ApiResponse<serde_json::Value>>) {
|
|
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<String>, pub window_class: Option<String>, pub process_name: Option<String>, pub enabled: Option<bool> }
|
|
|
|
pub async fn update_rule(State(state): State<AppState>, Path(id): Path<i64>, Json(body): Json<UpdateRuleRequest>) -> Json<ApiResponse<()>> {
|
|
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::<Option<String>, _>("window_title"));
|
|
let window_class = body.window_class.or_else(|| existing.get::<Option<String>, _>("window_class"));
|
|
let process_name = body.process_name.or_else(|| existing.get::<Option<String>, _>("process_name"));
|
|
let enabled = body.enabled.unwrap_or_else(|| existing.get::<bool, _>("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<String> = 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<AppState>, Path(id): Path<i64>) -> Json<ApiResponse<()>> {
|
|
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::<String, _>("target_type"), row.get::<Option<String>, _>("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<AppState>) -> Json<ApiResponse<serde_json::Value>> {
|
|
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::<String,_>("device_uid"), "blocked_count": r.get::<i32,_>("blocked_count"),
|
|
"date": r.get::<String,_>("date")
|
|
})).collect::<Vec<_>>()}))),
|
|
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<serde_json::Value> {
|
|
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::<i64,_>("id"), "rule_type": r.get::<String,_>("rule_type"),
|
|
"window_title": r.get::<Option<String>,_>("window_title"),
|
|
"window_class": r.get::<Option<String>,_>("window_class"),
|
|
"process_name": r.get::<Option<String>,_>("process_name"),
|
|
})).collect())
|
|
.unwrap_or_default()
|
|
}
|