This commit is contained in:
parent
6a3ce7e9d3
commit
ac53b79bc3
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -2436,6 +2436,7 @@ dependencies = [
|
|||||||
"jsonwebtoken",
|
"jsonwebtoken",
|
||||||
"lettre",
|
"lettre",
|
||||||
"rdkafka",
|
"rdkafka",
|
||||||
|
"regex",
|
||||||
"rustls",
|
"rustls",
|
||||||
"sea-query",
|
"sea-query",
|
||||||
"sea-query-binder",
|
"sea-query-binder",
|
||||||
|
|||||||
@ -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"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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())
|
||||||
|
|||||||
@ -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,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user