From ac53b79bc3493daa7bce30a08744e7e177d650ab Mon Sep 17 00:00:00 2001 From: tsuki Date: Fri, 15 Aug 2025 22:32:07 +0800 Subject: [PATCH] check paras --- Cargo.lock | 1 + Cargo.toml | 1 + src/graphql/mutations/config.rs | 9 +- src/graphql/queries/config.rs | 36 ++- src/graphql/types/config.rs | 51 +++- src/services/config_manager.rs | 437 +++++++++++++++++++++++++++++++- src/services/config_service.rs | 5 +- 7 files changed, 514 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3b628bb..6109b29 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2436,6 +2436,7 @@ dependencies = [ "jsonwebtoken", "lettre", "rdkafka", + "regex", "rustls", "sea-query", "sea-query-binder", diff --git a/Cargo.toml b/Cargo.toml index 47b95d6..0335acc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,5 +54,6 @@ anyhow = "1.0.98" thiserror = "2.0.12" casbin = { version = "2.0", features = ["logging", "incremental","runtime-tokio"] } sqlx-adapter = { version = "1.8.0", default-features = false, features = ["postgres", "runtime-tokio-native-tls"]} +regex = "1.11.1" diff --git a/src/graphql/mutations/config.rs b/src/graphql/mutations/config.rs index 7e9221c..1ce0029 100644 --- a/src/graphql/mutations/config.rs +++ b/src/graphql/mutations/config.rs @@ -19,13 +19,8 @@ impl ConfigMutation { let configs = input .into_iter() - .map(|input| { - ( - input.key, - serde_json::to_value(input.value.unwrap()).unwrap(), - ) - }) - .collect::>(); + .map(|input| (input.key, input.value.unwrap())) + .collect::>(); config_manager.set_values(configs).await?; Ok("successed".to_string()) diff --git a/src/graphql/queries/config.rs b/src/graphql/queries/config.rs index a293d3b..665f75e 100644 --- a/src/graphql/queries/config.rs +++ b/src/graphql/queries/config.rs @@ -1,5 +1,5 @@ use crate::graphql::guards::*; -use crate::graphql::types::{config::*, permission::*}; +use crate::graphql::types::config::*; use crate::services::config_manager::ConfigsManager; use async_graphql::{Context, Object, Result}; @@ -20,4 +20,38 @@ impl ConfigQuery { let configs = configs_service.get_settings_by_category("site").await?; Ok(configs) } + + #[graphql(guard = "RequireWritePermission::new(\"config\")")] + async fn validate_config( + &self, + ctx: &Context<'_>, + input: Vec, + ) -> Result { + let configs_service = ctx.data::()?; + let mut errors = vec![]; + + for config in input { + println!("config: {:?}", config); + if let Some(value) = &config.value { + if let Some(existing_config) = + configs_service.get_setting_metadata(&config.key).await? + { + if let Err(e) = configs_service.validate_config( + &config.key, + value, + &existing_config.value_type, + ) { + errors.push(format!("{}: {}", config.key, e.to_string())); + } + } else { + errors.push(format!("{}: 配置项不存在", config.key)); + } + } + } + + Ok(ConfigValidationResult { + is_valid: errors.is_empty(), + errors, + }) + } } diff --git a/src/graphql/types/config.rs b/src/graphql/types/config.rs index b999b48..395f6f9 100644 --- a/src/graphql/types/config.rs +++ b/src/graphql/types/config.rs @@ -71,13 +71,38 @@ pub mod output { self.value_type == expected_type } + /// 清理配置值,处理可能的JSON转义 + fn clean_value(&self, value: &str) -> String { + // 首先尝试JSON解析,处理可能的双重编码 + if let Ok(json_value) = serde_json::from_str::(value) { + if let Some(string_value) = json_value.as_str() { + return string_value.to_string(); + } + // 如果不是字符串,返回JSON值的字符串表示 + return json_value.to_string(); + } + + // 如果JSON解析失败,进行基本的引号清理 + value + .trim() + .trim_matches('"') + .trim_matches('\'') + .to_string() + } + /// 获取布尔值 pub fn get_bool(&self) -> Result { if self.value_type == "boolean" { - self.value - .as_ref() - .and_then(|v| v.parse::().ok()) - .ok_or_else(|| "Invalid boolean value".to_string()) + if let Some(v) = &self.value { + let cleaned_value = self.clean_value(v); + match cleaned_value.to_lowercase().as_str() { + "true" | "1" | "yes" => Ok(true), + "false" | "0" | "no" => Ok(false), + _ => Err(format!("Invalid boolean value: {}", v)), + } + } else { + Err("No value set for boolean setting".to_string()) + } } else { Err("Setting is not a boolean type".to_string()) } @@ -86,10 +111,14 @@ pub mod output { /// 获取数字值 pub fn get_number(&self) -> Result { if self.value_type == "number" { - self.value - .as_ref() - .and_then(|v| v.parse::().ok()) - .ok_or_else(|| "Invalid number value".to_string()) + if let Some(v) = &self.value { + let cleaned_value = self.clean_value(v); + cleaned_value + .parse::() + .map_err(|_| format!("Invalid number value: {}", v)) + } else { + Err("No value set for number setting".to_string()) + } } else { Err("Setting is not a number type".to_string()) } @@ -136,6 +165,12 @@ pub mod output { } } } + + #[derive(Debug, Clone, Serialize, Deserialize, SimpleObject)] + pub struct ConfigValidationResult { + pub is_valid: bool, + pub errors: Vec, + } } pub mod input { diff --git a/src/services/config_manager.rs b/src/services/config_manager.rs index 5b65d74..6c28109 100644 --- a/src/services/config_manager.rs +++ b/src/services/config_manager.rs @@ -1,11 +1,47 @@ use crate::models::config::Config; use crate::services::config_service::ConfigsService; use anyhow::Result; +use regex::Regex; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::sync::Arc; use tokio::sync::RwLock; +/// 配置验证错误类型 +#[derive(Debug, Clone, PartialEq)] +pub enum ValidationError { + /// 无效的格式 + InvalidFormat(String), + /// 不支持的类型 + UnsupportedType(String), + /// 无效的值 + InvalidValue(String), + /// 无效的范围 + InvalidRange(String), + /// 必填字段 + RequiredField(String), + /// 类型不匹配 + InvalidType(String), + /// 内部错误 + InternalError(String), +} + +impl std::fmt::Display for ValidationError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ValidationError::InvalidFormat(msg) => write!(f, "格式错误: {}", msg), + ValidationError::UnsupportedType(msg) => write!(f, "不支持的类型: {}", msg), + ValidationError::InvalidValue(msg) => write!(f, "无效的值: {}", msg), + ValidationError::InvalidRange(msg) => write!(f, "范围错误: {}", msg), + ValidationError::RequiredField(msg) => write!(f, "必填字段: {}", msg), + ValidationError::InvalidType(msg) => write!(f, "类型错误: {}", msg), + ValidationError::InternalError(msg) => write!(f, "内部错误: {}", msg), + } + } +} + +impl std::error::Error for ValidationError {} + pub mod keys { // 应用配置 pub const SITE_NAME: &str = "site.name"; @@ -195,16 +231,25 @@ impl ConfigsManager { Ok(self.get_json(key).await?.unwrap_or(default)) } - /// 设置配置值 + /// 设置配置值(带验证) pub async fn set_value(&self, key: &str, value: &T) -> Result<()> where T: Serialize, { - // 更新数据库 + let value_str = serde_json::to_string(value)?; + + // 获取现有配置以确定类型 if let Some(setting) = self.configs_service.get_config_by_key(key).await? { + // 执行验证 + if let Err(validation_error) = + self.validate_config(key, &value_str, &setting.value_type) + { + return Err(anyhow::anyhow!("配置验证失败: {}", validation_error)); + } + let update_setting = crate::models::UpdateConfig { key: key.to_string(), - value: Some(serde_json::to_string(value)?), + value: Some(value_str), description: None, category: None, is_editable: None, @@ -212,6 +257,8 @@ impl ConfigsManager { self.configs_service .update_setting(setting.id, update_setting, uuid::Uuid::nil()) .await?; + } else { + return Err(anyhow::anyhow!("配置项不存在: {}", key)); } // 刷新缓存 @@ -220,11 +267,35 @@ impl ConfigsManager { Ok(()) } - /// 批量设置配置值 - pub async fn set_values(&self, updates: HashMap) -> Result<()> { - let updates: Vec<(String, serde_json::Value)> = updates.into_iter().collect(); + pub async fn set_values(&self, updates: HashMap) -> Result<()> { + let mut validation_errors = Vec::new(); + let mut validated_updates = Vec::new(); + + for (key, value) in &updates { + if let Some(setting) = self.configs_service.get_config_by_key(key).await? { + if let Err(validation_error) = + self.validate_config(key, &value, &setting.value_type) + { + validation_errors.push(format!("{}: {}", key, validation_error)); + } else { + validated_updates.push((key.clone(), value.clone())); + } + } else { + validation_errors.push(format!("{}: 配置项不存在", key)); + } + } + + // 如果有验证错误,返回错误 + if !validation_errors.is_empty() { + return Err(anyhow::anyhow!( + "配置验证失败:\n{}", + validation_errors.join("\n") + )); + } + + // 执行批量更新 self.configs_service - .batch_update_settings(updates, uuid::Uuid::nil()) + .batch_update_settings(validated_updates, uuid::Uuid::nil()) .await?; // 刷新缓存 @@ -266,4 +337,356 @@ impl ConfigsManager { pub async fn get_setting_metadata(&self, key: &str) -> Result> { self.get_cached_setting(key).await } + + /// 验证配置是否有效 + pub fn validate_config( + &self, + key: &str, + value: &str, + value_type: &str, + ) -> Result<(), ValidationError> { + // 基础类型验证 + self.validate_basic_type(value, value_type)?; + + // 特定配置项的业务逻辑验证 + self.validate_business_logic(key, value, value_type)?; + + Ok(()) + } + + /// 清理配置值,处理可能的JSON转义 + fn clean_config_value(&self, value: &str) -> String { + // 首先尝试JSON解析,处理可能的双重编码 + if let Ok(json_value) = serde_json::from_str::(value) { + if let Some(string_value) = json_value.as_str() { + return string_value.to_string(); + } + // 如果不是字符串,返回JSON值的字符串表示 + return json_value.to_string(); + } + + // 如果JSON解析失败,进行基本的引号清理 + value + .trim() + .trim_matches('"') + .trim_matches('\'') + .to_string() + } + + /// 验证基础数据类型 + fn validate_basic_type(&self, value: &str, value_type: &str) -> Result<(), ValidationError> { + let cleaned_value = self.clean_config_value(value); + + match value_type { + "string" => { + // 字符串类型:检查长度限制 + if cleaned_value.len() > 10000 { + return Err(ValidationError::InvalidFormat( + "字符串长度超过10000字符限制".to_string(), + )); + } + } + "number" => { + // 数字类型:尝试解析为浮点数 + cleaned_value.parse::().map_err(|_| { + ValidationError::InvalidFormat(format!("无效的数字格式: {}", value)) + })?; + } + "boolean" => { + // 布尔类型:检查是否为有效的布尔值 + match cleaned_value.to_lowercase().as_str() { + "true" | "false" | "1" | "0" | "yes" | "no" => {} + _ => { + return Err(ValidationError::InvalidFormat(format!( + "无效的布尔值: {}。有效值: true, false, 1, 0, yes, no", + value + ))) + } + } + } + "json" => { + // JSON类型:验证JSON格式 + serde_json::from_str::(value).map_err(|e| { + ValidationError::InvalidFormat(format!("无效的JSON格式: {}", e)) + })?; + } + _ => { + return Err(ValidationError::UnsupportedType(format!( + "不支持的配置类型: {}", + value_type + ))); + } + } + + Ok(()) + } + + /// 验证特定配置项的业务逻辑 + fn validate_business_logic( + &self, + key: &str, + value: &str, + value_type: &str, + ) -> Result<(), ValidationError> { + match key { + // 网站URL验证 + keys::SITE_URL => { + if value_type == "string" && !value.is_empty() { + if !value.is_empty() { + self.validate_url(value)?; + } + } + } + + // 邮箱配置验证 + keys::EMAIL_SMTP_FROM_EMAIL => { + if value_type == "string" && !value.is_empty() { + if !value.is_empty() { + self.validate_email(value)?; + } + } + } + + // SMTP端口验证 + keys::EMAIL_SMTP_PORT => { + if value_type == "number" { + let port = value.parse::().map_err(|_| { + ValidationError::InvalidRange("SMTP端口必须是0-65535之间的整数".to_string()) + })?; + if port == 0 { + return Err(ValidationError::InvalidRange("SMTP端口不能为0".to_string())); + } + } + } + + // 日志级别验证 + keys::LOGGING_LEVEL => { + if value_type == "string" { + match value.to_lowercase().as_str() { + "trace" | "debug" | "info" | "warn" | "error" => {} + _ => { + return Err(ValidationError::InvalidValue( + "日志级别必须是以下之一: trace, debug, info, warn, error" + .to_string(), + )) + } + } + } + } + + // 缓存TTL验证 + keys::CACHE_TTL => { + if value_type == "number" { + let ttl = value.parse::().map_err(|_| { + ValidationError::InvalidRange("缓存TTL必须是正整数".to_string()) + })?; + if ttl < 60 { + return Err(ValidationError::InvalidRange( + "缓存TTL不能小于60秒".to_string(), + )); + } + if ttl > 86400 { + return Err(ValidationError::InvalidRange( + "缓存TTL不能超过86400秒(24小时)".to_string(), + )); + } + } + } + + // 缓存最大大小验证 + keys::CACHE_MAX_SIZE => { + if value_type == "number" { + let size = value.parse::().map_err(|_| { + ValidationError::InvalidRange("缓存最大大小必须是正整数".to_string()) + })?; + if size < 1 { + return Err(ValidationError::InvalidRange( + "缓存最大大小不能小于1".to_string(), + )); + } + if size > 10000 { + return Err(ValidationError::InvalidRange( + "缓存最大大小不能超过10000".to_string(), + )); + } + } + } + + // 日志文件数量验证 + keys::LOGGING_MAX_FILES => { + if value_type == "number" { + let count = value.parse::().map_err(|_| { + ValidationError::InvalidRange("日志文件数量必须是正整数".to_string()) + })?; + if count < 1 || count > 100 { + return Err(ValidationError::InvalidRange( + "日志文件数量必须在1-100之间".to_string(), + )); + } + } + } + + // 日志文件大小验证 + keys::LOGGING_MAX_FILE_SIZE => { + if value_type == "number" { + // 如果是数字类型,验证是否为有效的正整数(字节数) + let size = value.parse::().map_err(|_| { + ValidationError::InvalidRange( + "日志文件大小必须是正整数(字节数)".to_string(), + ) + })?; + if size < 1024 { + return Err(ValidationError::InvalidRange( + "日志文件大小不能小于1024字节".to_string(), + )); + } + if size > 1073741824 { + // 1GB + return Err(ValidationError::InvalidRange( + "日志文件大小不能超过1GB".to_string(), + )); + } + } else if value_type == "string" && !value.is_empty() { + if !value.is_empty() { + self.validate_file_size(value)?; + } + } + } + + _ => { + // 对于其他配置项,执行通用验证 + self.validate_common_rules(key, value, value_type)?; + } + } + + Ok(()) + } + + /// 验证URL格式 + fn validate_url(&self, url: &str) -> Result<(), ValidationError> { + // let url_regex = Regex::new(r"^https?://[^\s/$.?#]$") + // .map_err(|_| ValidationError::InternalError("URL正则表达式编译失败".to_string()))?; + + // if !url_regex.is_match(url) { + // return Err(ValidationError::InvalidFormat( + // "无效的URL格式,必须以http://或https://开头".to_string(), + // )); + // } + + Ok(()) + } + + /// 验证邮箱格式 + fn validate_email(&self, email: &str) -> Result<(), ValidationError> { + let email_regex = Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$") + .map_err(|_| ValidationError::InternalError("邮箱正则表达式编译失败".to_string()))?; + + if !email_regex.is_match(email) { + return Err(ValidationError::InvalidFormat("无效的邮箱格式".to_string())); + } + + Ok(()) + } + + /// 验证文件大小格式 (如: "10MB", "1GB") + fn validate_file_size(&self, size_str: &str) -> Result<(), ValidationError> { + let size_regex = Regex::new(r"^(\d+(?:\.\d+)?)(B|KB|MB|GB|TB)$").map_err(|_| { + ValidationError::InternalError("文件大小正则表达式编译失败".to_string()) + })?; + + if !size_regex.is_match(size_str) { + return Err(ValidationError::InvalidFormat( + "无效的文件大小格式,应为数字+单位(B/KB/MB/GB/TB),如: 10MB".to_string(), + )); + } + + Ok(()) + } + + /// 通用验证规则 + fn validate_common_rules( + &self, + key: &str, + value: &str, + value_type: &str, + ) -> Result<(), ValidationError> { + // 检查空值规则 + if value.is_empty() && value_type == "string" { + // 某些关键配置不能为空 + match key { + keys::SITE_NAME | keys::EMAIL_SMTP_HOST | keys::EMAIL_SMTP_USER => { + return Err(ValidationError::RequiredField(format!( + "配置项 {} 不能为空", + key + ))); + } + _ => {} + } + } + + // 检查布尔开关类型配置 + if key.starts_with("switch.") || key.contains("open_") { + if value_type != "boolean" { + return Err(ValidationError::InvalidType(format!( + "开关类配置 {} 必须是布尔类型", + key + ))); + } + } + + // 检查数字范围配置 + if key.contains("port") && value_type == "number" { + let port = value.parse::().map_err(|_| { + ValidationError::InvalidRange("端口必须是0-65535之间的整数".to_string()) + })?; + if port == 0 { + return Err(ValidationError::InvalidRange("端口不能为0".to_string())); + } + } + + Ok(()) + } + + /// 批量验证配置 + pub fn validate_configs( + &self, + configs: &[(String, String, String)], + ) -> Result<(), Vec<(String, ValidationError)>> { + let mut errors = Vec::new(); + + for (key, value, value_type) in configs { + if let Err(e) = self.validate_config(key, value, value_type) { + errors.push((key.clone(), e)); + } + } + + if errors.is_empty() { + Ok(()) + } else { + Err(errors) + } + } + + /// 获取配置验证规则说明 + pub fn get_validation_rules(&self, key: &str) -> String { + match key { + keys::SITE_URL => "必须是有效的HTTP或HTTPS URL格式".to_string(), + keys::EMAIL_SMTP_FROM_EMAIL => "必须是有效的邮箱格式".to_string(), + keys::EMAIL_SMTP_PORT => "必须是1-65535之间的数字".to_string(), + keys::LOGGING_LEVEL => "必须是以下之一: trace, debug, info, warn, error".to_string(), + keys::CACHE_TTL => "必须是60-86400之间的数字(秒)".to_string(), + keys::CACHE_MAX_SIZE => "必须是1-10000之间的数字".to_string(), + keys::LOGGING_MAX_FILES => "必须是1-100之间的数字".to_string(), + keys::LOGGING_MAX_FILE_SIZE => "必须是文件大小格式,如: 10MB, 1GB".to_string(), + _ => { + if key.starts_with("switch.") || key.contains("open_") { + "必须是布尔类型 (true/false)".to_string() + } else if key.contains("port") { + "必须是1-65535之间的端口号".to_string() + } else { + "请参考配置文档".to_string() + } + } + } + } } diff --git a/src/services/config_service.rs b/src/services/config_service.rs index 23b6774..c555a95 100644 --- a/src/services/config_service.rs +++ b/src/services/config_service.rs @@ -230,7 +230,7 @@ impl ConfigsService { pub async fn batch_update_settings( &self, - updates: Vec<(String, Value)>, + updates: Vec<(String, String)>, user_id: Uuid, ) -> Result> { let mut updated_settings = Vec::new(); @@ -241,7 +241,6 @@ impl ConfigsService { continue; // 跳过不可编辑的配置 } - let value_str = serde_json::to_string(&value)?; let updated_setting = sqlx::query_as!( Config, r#" @@ -253,7 +252,7 @@ impl ConfigsService { updated_at as "updated_at: DateTime", created_by, updated_by "#, - value_str, + value, user_id, key )