mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 17:30:23 +08:00
refactor: 拆分 system.rs 大文件,将 email_templates/proxy_errors/system_config 下沉到 shared 层
- 删除 1666 行的 admin/system/shared/system.rs,按职责拆分到独立模块 - 新增 handlers/shared/email_templates.rs 和 system_config_values.rs 存放跨层共用的模板/配置工具函数 - 新增 admin/system/shared/email_templates.rs 存放 admin 专用的模板操作逻辑 - 新增 admin/shared/proxy_errors.rs 存放 build_proxy_error_response - 清理 public/system_modules_helpers 中不属于 public 层的导出 - 新增架构测试守护模块归属边界
This commit is contained in:
216
apps/aether-gateway/src/handlers/shared/email_templates.rs
Normal file
216
apps/aether-gateway/src/handlers/shared/email_templates.rs
Normal file
@@ -0,0 +1,216 @@
|
||||
use super::system_config_string;
|
||||
use crate::{AppState, GatewayError};
|
||||
use serde_json::json;
|
||||
|
||||
pub(crate) struct AdminEmailTemplateDefinition {
|
||||
pub(crate) template_type: &'static str,
|
||||
pub(crate) name: &'static str,
|
||||
pub(crate) variables: &'static [&'static str],
|
||||
pub(crate) default_subject: &'static str,
|
||||
pub(crate) default_html: &'static str,
|
||||
}
|
||||
|
||||
const ADMIN_EMAIL_TEMPLATE_DEFINITIONS: &[AdminEmailTemplateDefinition] = &[
|
||||
AdminEmailTemplateDefinition {
|
||||
template_type: "verification",
|
||||
name: "注册验证码",
|
||||
variables: &["app_name", "code", "expire_minutes", "email"],
|
||||
default_subject: "验证码",
|
||||
default_html: r#"<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>验证码</title>
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0; background-color: #faf9f5; font-family: Georgia, 'Times New Roman', 'Songti SC', 'STSong', serif;">
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="background-color: #faf9f5; padding: 40px 20px;">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="max-width: 480px;">
|
||||
<tr>
|
||||
<td style="padding: 0 0 32px; text-align: center;">
|
||||
<div style="font-size: 13px; font-family: 'SF Mono', Monaco, 'Courier New', monospace; color: #6c695c; letter-spacing: 0.15em; text-transform: uppercase;">
|
||||
{{app_name}}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="background-color: #ffffff; border: 1px solid rgba(61, 57, 41, 0.1); border-radius: 6px;">
|
||||
<tr>
|
||||
<td style="padding: 48px 40px;">
|
||||
<h1 style="margin: 0 0 24px; font-size: 24px; font-weight: 500; color: #3d3929; text-align: center; letter-spacing: -0.02em;">
|
||||
验证码
|
||||
</h1>
|
||||
<p style="margin: 0 0 32px; font-size: 15px; color: #6c695c; line-height: 1.7; text-align: center;">
|
||||
您正在注册账户,请使用以下验证码完成验证。
|
||||
</p>
|
||||
<div style="background-color: #faf9f5; border: 1px solid rgba(61, 57, 41, 0.08); border-radius: 4px; padding: 32px 20px; text-align: center; margin-bottom: 32px;">
|
||||
<div style="font-size: 40px; font-weight: 500; color: #c96442; letter-spacing: 12px; font-family: 'SF Mono', Monaco, 'Courier New', monospace;">
|
||||
{{code}}
|
||||
</div>
|
||||
</div>
|
||||
<p style="margin: 0; font-size: 14px; color: #6c695c; line-height: 1.6; text-align: center;">
|
||||
验证码将在 <span style="color: #3d3929; font-weight: 500;">{{expire_minutes}} 分钟</span>后失效
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 32px 0 0; text-align: center;">
|
||||
<p style="margin: 0 0 8px; font-size: 12px; color: #6c695c;">
|
||||
如果这不是您的操作,请忽略此邮件。
|
||||
</p>
|
||||
<p style="margin: 0; font-size: 11px; color: rgba(108, 105, 92, 0.6);">
|
||||
此邮件由系统自动发送,请勿回复
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>"#,
|
||||
},
|
||||
AdminEmailTemplateDefinition {
|
||||
template_type: "password_reset",
|
||||
name: "找回密码",
|
||||
variables: &["app_name", "reset_link", "expire_minutes", "email"],
|
||||
default_subject: "密码重置",
|
||||
default_html: r#"<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>密码重置</title>
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0; background-color: #faf9f5; font-family: Georgia, 'Times New Roman', 'Songti SC', 'STSong', serif;">
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="background-color: #faf9f5; padding: 40px 20px;">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="max-width: 480px;">
|
||||
<tr>
|
||||
<td style="padding: 0 0 32px; text-align: center;">
|
||||
<div style="font-size: 13px; font-family: 'SF Mono', Monaco, 'Courier New', monospace; color: #6c695c; letter-spacing: 0.15em; text-transform: uppercase;">
|
||||
{{app_name}}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="background-color: #ffffff; border: 1px solid rgba(61, 57, 41, 0.1); border-radius: 6px;">
|
||||
<tr>
|
||||
<td style="padding: 48px 40px;">
|
||||
<h1 style="margin: 0 0 24px; font-size: 24px; font-weight: 500; color: #3d3929; text-align: center; letter-spacing: -0.02em;">
|
||||
重置密码
|
||||
</h1>
|
||||
<p style="margin: 0 0 32px; font-size: 15px; color: #6c695c; line-height: 1.7; text-align: center;">
|
||||
您正在重置账户密码,请点击下方按钮完成操作。
|
||||
</p>
|
||||
<div style="text-align: center; margin-bottom: 32px;">
|
||||
<a href="{{reset_link}}" style="display: inline-block; padding: 14px 36px; background-color: #c96442; color: #ffffff; text-decoration: none; border-radius: 4px; font-size: 15px; font-weight: 500; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;">
|
||||
重置密码
|
||||
</a>
|
||||
</div>
|
||||
<p style="margin: 0; font-size: 14px; color: #6c695c; line-height: 1.6; text-align: center;">
|
||||
链接将在 <span style="color: #3d3929; font-weight: 500;">{{expire_minutes}} 分钟</span>后失效
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 32px 0 0; text-align: center;">
|
||||
<p style="margin: 0 0 8px; font-size: 12px; color: #6c695c;">
|
||||
如果您没有请求重置密码,请忽略此邮件。
|
||||
</p>
|
||||
<p style="margin: 0; font-size: 11px; color: rgba(108, 105, 92, 0.6);">
|
||||
此邮件由系统自动发送,请勿回复
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>"#,
|
||||
},
|
||||
];
|
||||
|
||||
pub(crate) fn admin_email_template_definition(
|
||||
template_type: &str,
|
||||
) -> Option<&'static AdminEmailTemplateDefinition> {
|
||||
let normalized = template_type.trim();
|
||||
ADMIN_EMAIL_TEMPLATE_DEFINITIONS
|
||||
.iter()
|
||||
.find(|definition| definition.template_type == normalized)
|
||||
}
|
||||
|
||||
pub(crate) fn admin_email_template_subject_key(template_type: &str) -> String {
|
||||
format!("email_template_{template_type}_subject")
|
||||
}
|
||||
|
||||
pub(crate) fn admin_email_template_html_key(template_type: &str) -> String {
|
||||
format!("email_template_{template_type}_html")
|
||||
}
|
||||
|
||||
pub(crate) async fn read_admin_email_template_payload(
|
||||
state: &AppState,
|
||||
template_type: &str,
|
||||
) -> Result<Option<serde_json::Value>, GatewayError> {
|
||||
let Some(definition) = admin_email_template_definition(template_type) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let subject = state
|
||||
.read_system_config_json_value(&admin_email_template_subject_key(definition.template_type))
|
||||
.await?;
|
||||
let html = state
|
||||
.read_system_config_json_value(&admin_email_template_html_key(definition.template_type))
|
||||
.await?;
|
||||
let subject = system_config_string(subject.as_ref())
|
||||
.unwrap_or_else(|| definition.default_subject.to_string());
|
||||
let html =
|
||||
system_config_string(html.as_ref()).unwrap_or_else(|| definition.default_html.to_string());
|
||||
let is_custom = subject != definition.default_subject || html != definition.default_html;
|
||||
|
||||
Ok(Some(json!({
|
||||
"type": definition.template_type,
|
||||
"name": definition.name,
|
||||
"variables": definition.variables,
|
||||
"subject": subject,
|
||||
"html": html,
|
||||
"is_custom": is_custom,
|
||||
"default_subject": definition.default_subject,
|
||||
"default_html": definition.default_html,
|
||||
})))
|
||||
}
|
||||
|
||||
pub(crate) fn escape_admin_email_template_html(value: &str) -> String {
|
||||
value
|
||||
.replace('&', "&")
|
||||
.replace('<', "<")
|
||||
.replace('>', ">")
|
||||
.replace('\"', """)
|
||||
.replace('\'', "'")
|
||||
}
|
||||
|
||||
pub(crate) fn render_admin_email_template_html(
|
||||
template_html: &str,
|
||||
variables: &std::collections::BTreeMap<String, String>,
|
||||
) -> Result<String, GatewayError> {
|
||||
let mut rendered = template_html.to_string();
|
||||
for (key, value) in variables {
|
||||
let pattern = regex::Regex::new(&format!(r"\{{\{{\s*{}\s*\}}\}}", regex::escape(key)))
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?;
|
||||
rendered = pattern
|
||||
.replace_all(&rendered, escape_admin_email_template_html(value))
|
||||
.into_owned();
|
||||
}
|
||||
Ok(rendered)
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
mod admin_proxy;
|
||||
mod catalog;
|
||||
mod email_templates;
|
||||
mod external_models;
|
||||
mod normalize;
|
||||
mod payloads;
|
||||
mod request_utils;
|
||||
mod system_config_values;
|
||||
mod usage_stats;
|
||||
|
||||
pub(crate) use self::admin_proxy::{
|
||||
@@ -17,6 +19,11 @@ pub(crate) use self::catalog::{
|
||||
provider_catalog_key_supports_format, provider_key_health_summary,
|
||||
provider_key_status_snapshot_payload,
|
||||
};
|
||||
pub(crate) use self::email_templates::{
|
||||
admin_email_template_definition, admin_email_template_html_key,
|
||||
admin_email_template_subject_key, escape_admin_email_template_html,
|
||||
read_admin_email_template_payload, render_admin_email_template_html,
|
||||
};
|
||||
pub(crate) use self::external_models::OFFICIAL_EXTERNAL_MODEL_PROVIDERS;
|
||||
pub(crate) use self::normalize::{
|
||||
normalize_json_array, normalize_json_object, normalize_string_list,
|
||||
@@ -34,6 +41,9 @@ pub(crate) use self::request_utils::{
|
||||
sanitize_upstream_path_and_query, should_strip_forwarded_provider_credential_header,
|
||||
should_strip_forwarded_trusted_admin_header, strip_query_param, unix_secs_to_rfc3339,
|
||||
};
|
||||
pub(crate) use self::system_config_values::{
|
||||
module_available_from_env, system_config_bool, system_config_string,
|
||||
};
|
||||
pub(crate) use self::usage_stats::{
|
||||
admin_stats_bad_request_response, list_usage_for_optional_range, parse_bounded_u32, round_to,
|
||||
AdminStatsTimeRange, AdminStatsUsageFilter,
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
pub(crate) fn module_available_from_env(env_key: &str, default_available: bool) -> bool {
|
||||
match std::env::var(env_key) {
|
||||
Ok(value) => matches!(
|
||||
value.trim().to_ascii_lowercase().as_str(),
|
||||
"true" | "1" | "yes"
|
||||
),
|
||||
Err(_) => default_available,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn system_config_bool(value: Option<&serde_json::Value>, default: bool) -> bool {
|
||||
match value {
|
||||
Some(serde_json::Value::Bool(value)) => *value,
|
||||
Some(serde_json::Value::Number(value)) => {
|
||||
value.as_i64().map(|value| value != 0).unwrap_or(default)
|
||||
}
|
||||
Some(serde_json::Value::String(value)) => {
|
||||
match value.trim().to_ascii_lowercase().as_str() {
|
||||
"true" | "1" | "yes" | "on" => true,
|
||||
"false" | "0" | "no" | "off" => false,
|
||||
_ => default,
|
||||
}
|
||||
}
|
||||
_ => default,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn system_config_string(value: Option<&serde_json::Value>) -> Option<String> {
|
||||
match value {
|
||||
Some(serde_json::Value::String(value)) => {
|
||||
let value = value.trim();
|
||||
if value.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(value.to_string())
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user