check paras
Some checks failed
Docker Build and Push / build (push) Has been cancelled

This commit is contained in:
tsuki 2025-08-15 22:32:07 +08:00
parent 6a3ce7e9d3
commit ac53b79bc3
7 changed files with 514 additions and 26 deletions

1
Cargo.lock generated
View File

@ -2436,6 +2436,7 @@ dependencies = [
"jsonwebtoken", "jsonwebtoken",
"lettre", "lettre",
"rdkafka", "rdkafka",
"regex",
"rustls", "rustls",
"sea-query", "sea-query",
"sea-query-binder", "sea-query-binder",

View File

@ -54,5 +54,6 @@ anyhow = "1.0.98"
thiserror = "2.0.12" thiserror = "2.0.12"
casbin = { version = "2.0", features = ["logging", "incremental","runtime-tokio"] } casbin = { version = "2.0", features = ["logging", "incremental","runtime-tokio"] }
sqlx-adapter = { version = "1.8.0", default-features = false, features = ["postgres", "runtime-tokio-native-tls"]} sqlx-adapter = { version = "1.8.0", default-features = false, features = ["postgres", "runtime-tokio-native-tls"]}
regex = "1.11.1"

View File

@ -19,13 +19,8 @@ impl ConfigMutation {
let configs = input let configs = input
.into_iter() .into_iter()
.map(|input| { .map(|input| (input.key, input.value.unwrap()))
( .collect::<HashMap<String, String>>();
input.key,
serde_json::to_value(input.value.unwrap()).unwrap(),
)
})
.collect::<HashMap<String, serde_json::Value>>();
config_manager.set_values(configs).await?; config_manager.set_values(configs).await?;
Ok("successed".to_string()) Ok("successed".to_string())

View File

@ -1,5 +1,5 @@
use crate::graphql::guards::*; use crate::graphql::guards::*;
use crate::graphql::types::{config::*, permission::*}; use crate::graphql::types::config::*;
use crate::services::config_manager::ConfigsManager; use crate::services::config_manager::ConfigsManager;
use async_graphql::{Context, Object, Result}; use async_graphql::{Context, Object, Result};
@ -20,4 +20,38 @@ impl ConfigQuery {
let configs = configs_service.get_settings_by_category("site").await?; let configs = configs_service.get_settings_by_category("site").await?;
Ok(configs) Ok(configs)
} }
#[graphql(guard = "RequireWritePermission::new(\"config\")")]
async fn validate_config(
&self,
ctx: &Context<'_>,
input: Vec<UpdateConfig>,
) -> Result<ConfigValidationResult> {
let configs_service = ctx.data::<ConfigsManager>()?;
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,
})
}
} }

View File

@ -71,13 +71,38 @@ pub mod output {
self.value_type == expected_type self.value_type == expected_type
} }
/// 清理配置值处理可能的JSON转义
fn clean_value(&self, value: &str) -> String {
// 首先尝试JSON解析处理可能的双重编码
if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(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<bool, String> { pub fn get_bool(&self) -> Result<bool, String> {
if self.value_type == "boolean" { if self.value_type == "boolean" {
self.value if let Some(v) = &self.value {
.as_ref() let cleaned_value = self.clean_value(v);
.and_then(|v| v.parse::<bool>().ok()) match cleaned_value.to_lowercase().as_str() {
.ok_or_else(|| "Invalid boolean value".to_string()) "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 { } else {
Err("Setting is not a boolean type".to_string()) Err("Setting is not a boolean type".to_string())
} }
@ -86,10 +111,14 @@ pub mod output {
/// 获取数字值 /// 获取数字值
pub fn get_number(&self) -> Result<f64, String> { pub fn get_number(&self) -> Result<f64, String> {
if self.value_type == "number" { if self.value_type == "number" {
self.value if let Some(v) = &self.value {
.as_ref() let cleaned_value = self.clean_value(v);
.and_then(|v| v.parse::<f64>().ok()) cleaned_value
.ok_or_else(|| "Invalid number value".to_string()) .parse::<f64>()
.map_err(|_| format!("Invalid number value: {}", v))
} else {
Err("No value set for number setting".to_string())
}
} else { } else {
Err("Setting is not a number type".to_string()) 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<String>,
}
} }
pub mod input { pub mod input {

View File

@ -1,11 +1,47 @@
use crate::models::config::Config; use crate::models::config::Config;
use crate::services::config_service::ConfigsService; use crate::services::config_service::ConfigsService;
use anyhow::Result; use anyhow::Result;
use regex::Regex;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::RwLock; 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 mod keys {
// 应用配置 // 应用配置
pub const SITE_NAME: &str = "site.name"; pub const SITE_NAME: &str = "site.name";
@ -195,16 +231,25 @@ impl ConfigsManager {
Ok(self.get_json(key).await?.unwrap_or(default)) Ok(self.get_json(key).await?.unwrap_or(default))
} }
/// 设置配置值 /// 设置配置值(带验证)
pub async fn set_value<T>(&self, key: &str, value: &T) -> Result<()> pub async fn set_value<T>(&self, key: &str, value: &T) -> Result<()>
where where
T: Serialize, 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 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 { let update_setting = crate::models::UpdateConfig {
key: key.to_string(), key: key.to_string(),
value: Some(serde_json::to_string(value)?), value: Some(value_str),
description: None, description: None,
category: None, category: None,
is_editable: None, is_editable: None,
@ -212,6 +257,8 @@ impl ConfigsManager {
self.configs_service self.configs_service
.update_setting(setting.id, update_setting, uuid::Uuid::nil()) .update_setting(setting.id, update_setting, uuid::Uuid::nil())
.await?; .await?;
} else {
return Err(anyhow::anyhow!("配置项不存在: {}", key));
} }
// 刷新缓存 // 刷新缓存
@ -220,11 +267,35 @@ impl ConfigsManager {
Ok(()) Ok(())
} }
/// 批量设置配置值 pub async fn set_values(&self, updates: HashMap<String, String>) -> Result<()> {
pub async fn set_values(&self, updates: HashMap<String, serde_json::Value>) -> Result<()> { let mut validation_errors = Vec::new();
let updates: Vec<(String, serde_json::Value)> = updates.into_iter().collect(); 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 self.configs_service
.batch_update_settings(updates, uuid::Uuid::nil()) .batch_update_settings(validated_updates, uuid::Uuid::nil())
.await?; .await?;
// 刷新缓存 // 刷新缓存
@ -266,4 +337,356 @@ impl ConfigsManager {
pub async fn get_setting_metadata(&self, key: &str) -> Result<Option<Config>> { pub async fn get_setting_metadata(&self, key: &str) -> Result<Option<Config>> {
self.get_cached_setting(key).await 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::<serde_json::Value>(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::<f64>().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::<serde_json::Value>(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::<u16>().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::<u64>().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::<u64>().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::<u32>().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::<u64>().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::<u16>().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()
}
}
}
}
} }

View File

@ -230,7 +230,7 @@ impl ConfigsService {
pub async fn batch_update_settings( pub async fn batch_update_settings(
&self, &self,
updates: Vec<(String, Value)>, updates: Vec<(String, String)>,
user_id: Uuid, user_id: Uuid,
) -> Result<Vec<Config>> { ) -> Result<Vec<Config>> {
let mut updated_settings = Vec::new(); let mut updated_settings = Vec::new();
@ -241,7 +241,6 @@ impl ConfigsService {
continue; // 跳过不可编辑的配置 continue; // 跳过不可编辑的配置
} }
let value_str = serde_json::to_string(&value)?;
let updated_setting = sqlx::query_as!( let updated_setting = sqlx::query_as!(
Config, Config,
r#" r#"
@ -253,7 +252,7 @@ impl ConfigsService {
updated_at as "updated_at: DateTime<Utc>", updated_at as "updated_at: DateTime<Utc>",
created_by, updated_by created_by, updated_by
"#, "#,
value_str, value,
user_id, user_id,
key key
) )