feat: 新增管理后台前端项目及安全加固
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
refactor(saas): 重构认证中间件与限流策略
- 登录限流调整为5次/分钟/IP
- 注册限流调整为3次/小时/IP
- GET请求不计入限流
fix(saas): 修复调度器时间戳处理
- 使用NOW()替代文本时间戳
- 兼容TEXT和TIMESTAMPTZ列类型
feat(saas): 实现环境变量插值
- 支持${ENV_VAR}语法解析
- 数据库密码支持环境变量注入
chore: 新增前端管理界面
- 基于React+Ant Design Pro
- 包含路由守卫/错误边界
- 对接58个API端点
docs: 更新安全加固文档
- 新增密钥管理规范
- 记录P0安全项审计结果
- 补充TLS终止说明
test: 完善配置解析单元测试
- 新增环境变量插值测试用例
This commit is contained in:
@@ -212,7 +212,8 @@ impl SaaSConfig {
|
||||
|
||||
let mut config = if config_path.exists() {
|
||||
let content = std::fs::read_to_string(&config_path)?;
|
||||
toml::from_str(&content)?
|
||||
let interpolated = interpolate_env_vars(&content);
|
||||
toml::from_str(&interpolated)?
|
||||
} else {
|
||||
tracing::warn!("Config file {:?} not found, using defaults", config_path);
|
||||
SaaSConfig::default()
|
||||
@@ -291,3 +292,71 @@ impl SaaSConfig {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 替换 TOML 配置文件中的 `${ENV_VAR}` 模式为环境变量值
|
||||
/// 未设置的环境变量保留原文,后续数据库连接或 JWT 初始化时会报明确错误
|
||||
fn interpolate_env_vars(content: &str) -> String {
|
||||
let mut result = String::with_capacity(content.len());
|
||||
let bytes = content.as_bytes();
|
||||
let mut i = 0;
|
||||
while i < bytes.len() {
|
||||
if i + 1 < bytes.len() && bytes[i] == b'$' && bytes[i + 1] == b'{' {
|
||||
let start = i + 2;
|
||||
let mut end = start;
|
||||
while end < bytes.len()
|
||||
&& (bytes[end].is_ascii_alphanumeric() || bytes[end] == b'_')
|
||||
{
|
||||
end += 1;
|
||||
}
|
||||
if end < bytes.len() && bytes[end] == b'}' {
|
||||
let var_name = std::str::from_utf8(&bytes[start..end]).unwrap_or("");
|
||||
match std::env::var(var_name) {
|
||||
Ok(val) => {
|
||||
tracing::debug!("Config: ${{{}}} → resolved ({} bytes)", var_name, val.len());
|
||||
result.push_str(&val);
|
||||
}
|
||||
Err(_) => {
|
||||
tracing::warn!("Config: ${{{}}} not set, keeping placeholder", var_name);
|
||||
result.push_str(&format!("${{{}}}", var_name));
|
||||
}
|
||||
}
|
||||
i = end + 1;
|
||||
} else {
|
||||
result.push(bytes[i] as char);
|
||||
i += 1;
|
||||
}
|
||||
} else {
|
||||
result.push(bytes[i] as char);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_interpolate_env_vars_resolves() {
|
||||
std::env::set_var("TEST_ZCLAW_DB_PW", "mypassword");
|
||||
let input = "url = \"postgres://user:${TEST_ZCLAW_DB_PW}@localhost/db\"";
|
||||
let result = interpolate_env_vars(input);
|
||||
assert_eq!(result, "url = \"postgres://user:mypassword@localhost/db\"");
|
||||
std::env::remove_var("TEST_ZCLAW_DB_PW");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_interpolate_env_vars_missing_keeps_placeholder() {
|
||||
let input = "url = \"postgres://user:${NONEXISTENT_VAR_12345}@localhost/db\"";
|
||||
let result = interpolate_env_vars(input);
|
||||
assert_eq!(result, "url = \"postgres://user:${NONEXISTENT_VAR_12345}@localhost/db\"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_interpolate_env_vars_no_placeholders() {
|
||||
let input = "host = \"0.0.0.0\"\nport = 8080";
|
||||
let result = interpolate_env_vars(input);
|
||||
assert_eq!(result, input);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user