mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 09:50:21 +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:
@@ -8,7 +8,7 @@ pub(crate) use self::api_keys::maybe_build_local_admin_api_keys_response;
|
|||||||
pub(crate) use self::ldap::maybe_build_local_admin_ldap_response;
|
pub(crate) use self::ldap::maybe_build_local_admin_ldap_response;
|
||||||
pub(crate) use self::oauth_config::{
|
pub(crate) use self::oauth_config::{
|
||||||
build_admin_oauth_provider_payload, build_admin_oauth_supported_types_payload,
|
build_admin_oauth_provider_payload, build_admin_oauth_supported_types_payload,
|
||||||
build_admin_oauth_upsert_record, build_proxy_error_response,
|
build_admin_oauth_upsert_record,
|
||||||
};
|
};
|
||||||
pub(crate) use self::oauth_routes::maybe_build_local_admin_oauth_response;
|
pub(crate) use self::oauth_routes::maybe_build_local_admin_oauth_response;
|
||||||
pub(crate) use self::security::maybe_build_local_admin_security_response;
|
pub(crate) use self::security::maybe_build_local_admin_security_response;
|
||||||
|
|||||||
@@ -3,12 +3,7 @@ use crate::AppState;
|
|||||||
use aether_data::repository::oauth_providers::{
|
use aether_data::repository::oauth_providers::{
|
||||||
EncryptedSecretUpdate, UpsertOAuthProviderConfigRecord,
|
EncryptedSecretUpdate, UpsertOAuthProviderConfigRecord,
|
||||||
};
|
};
|
||||||
use axum::{
|
use axum::http;
|
||||||
body::Body,
|
|
||||||
http,
|
|
||||||
response::{IntoResponse, Response},
|
|
||||||
Json,
|
|
||||||
};
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
@@ -70,26 +65,6 @@ pub(crate) fn build_admin_oauth_provider_payload(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn build_proxy_error_response(
|
|
||||||
status: http::StatusCode,
|
|
||||||
error_type: &str,
|
|
||||||
message: impl Into<String>,
|
|
||||||
details: Option<serde_json::Value>,
|
|
||||||
) -> Response<Body> {
|
|
||||||
let message = message.into();
|
|
||||||
let mut error = serde_json::Map::new();
|
|
||||||
error.insert("type".to_string(), json!(error_type));
|
|
||||||
error.insert("message".to_string(), json!(message));
|
|
||||||
if let Some(details) = details {
|
|
||||||
error.insert("details".to_string(), details);
|
|
||||||
}
|
|
||||||
(
|
|
||||||
status,
|
|
||||||
Json(json!({ "error": serde_json::Value::Object(error) })),
|
|
||||||
)
|
|
||||||
.into_response()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn admin_oauth_provider_type_from_path(request_path: &str) -> Option<String> {
|
pub(crate) fn admin_oauth_provider_type_from_path(request_path: &str) -> Option<String> {
|
||||||
let provider_type = request_path.strip_prefix("/api/admin/oauth/providers/")?;
|
let provider_type = request_path.strip_prefix("/api/admin/oauth/providers/")?;
|
||||||
(!provider_type.is_empty() && !provider_type.contains('/')).then_some(provider_type.to_string())
|
(!provider_type.is_empty() && !provider_type.contains('/')).then_some(provider_type.to_string())
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
use super::oauth_config::{
|
use super::oauth_config::{
|
||||||
admin_oauth_provider_type_from_path, admin_oauth_test_provider_type_from_path,
|
admin_oauth_provider_type_from_path, admin_oauth_test_provider_type_from_path,
|
||||||
build_admin_oauth_provider_payload, build_admin_oauth_supported_types_payload,
|
build_admin_oauth_provider_payload, build_admin_oauth_supported_types_payload,
|
||||||
build_admin_oauth_upsert_record, build_proxy_error_response, AdminOAuthProviderUpsertRequest,
|
build_admin_oauth_upsert_record, AdminOAuthProviderUpsertRequest,
|
||||||
};
|
};
|
||||||
use crate::control::GatewayPublicRequestContext;
|
use crate::control::GatewayPublicRequestContext;
|
||||||
use crate::handlers::admin::shared::attach_admin_audit_response;
|
use crate::handlers::admin::shared::{attach_admin_audit_response, build_proxy_error_response};
|
||||||
use crate::{AppState, GatewayError};
|
use crate::{AppState, GatewayError};
|
||||||
use axum::{
|
use axum::{
|
||||||
body::{Body, Bytes},
|
body::{Body, Bytes},
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
use crate::async_task;
|
use crate::async_task;
|
||||||
use crate::control::GatewayPublicRequestContext;
|
use crate::control::GatewayPublicRequestContext;
|
||||||
use crate::handlers::admin::shared::{attach_admin_audit_response, query_param_value};
|
use crate::handlers::admin::shared::{
|
||||||
|
attach_admin_audit_response, build_proxy_error_response, query_param_value,
|
||||||
|
};
|
||||||
use crate::{AppState, GatewayError};
|
use crate::{AppState, GatewayError};
|
||||||
use aether_data_contracts::repository::video_tasks::{VideoTaskQueryFilter, VideoTaskStatus};
|
use aether_data_contracts::repository::video_tasks::{VideoTaskQueryFilter, VideoTaskStatus};
|
||||||
use axum::{
|
use axum::{
|
||||||
@@ -11,7 +13,6 @@ use axum::{
|
|||||||
};
|
};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
use super::super::super::auth::build_proxy_error_response;
|
|
||||||
use super::builders::{
|
use super::builders::{
|
||||||
admin_video_task_detail_id_from_path, admin_video_task_nested_id_from_path,
|
admin_video_task_detail_id_from_path, admin_video_task_nested_id_from_path,
|
||||||
admin_video_task_status_name, admin_video_task_timestamp, build_admin_video_task_list_item,
|
admin_video_task_status_name, admin_video_task_timestamp, build_admin_video_task_list_item,
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ pub(crate) mod pool_admin;
|
|||||||
pub(crate) mod shared;
|
pub(crate) mod shared;
|
||||||
pub(crate) mod write;
|
pub(crate) mod write;
|
||||||
|
|
||||||
use super::auth::build_proxy_error_response;
|
|
||||||
|
|
||||||
mod crud;
|
mod crud;
|
||||||
mod delete_task;
|
mod delete_task;
|
||||||
mod models;
|
mod models;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use super::super::build_proxy_error_response;
|
use crate::handlers::admin::shared::build_proxy_error_response;
|
||||||
use axum::{
|
use axum::{
|
||||||
body::Body,
|
body::Body,
|
||||||
http,
|
http,
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
mod paths;
|
mod paths;
|
||||||
mod payloads;
|
mod payloads;
|
||||||
|
mod proxy_errors;
|
||||||
|
|
||||||
pub(crate) use self::paths::*;
|
pub(crate) use self::paths::*;
|
||||||
pub(crate) use self::payloads::*;
|
pub(crate) use self::payloads::*;
|
||||||
|
pub(crate) use self::proxy_errors::build_proxy_error_response;
|
||||||
pub(crate) use crate::handlers::shared::{
|
pub(crate) use crate::handlers::shared::{
|
||||||
attach_admin_audit_response, build_admin_provider_key_response,
|
attach_admin_audit_response, build_admin_provider_key_response,
|
||||||
decrypt_catalog_secret_with_fallbacks, default_provider_key_status_snapshot,
|
decrypt_catalog_secret_with_fallbacks, default_provider_key_status_snapshot,
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
use axum::{
|
||||||
|
body::Body,
|
||||||
|
http,
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
Json,
|
||||||
|
};
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
pub(crate) fn build_proxy_error_response(
|
||||||
|
status: http::StatusCode,
|
||||||
|
error_type: &str,
|
||||||
|
message: impl Into<String>,
|
||||||
|
details: Option<serde_json::Value>,
|
||||||
|
) -> Response<Body> {
|
||||||
|
let message = message.into();
|
||||||
|
let mut error = serde_json::Map::new();
|
||||||
|
error.insert("type".to_string(), json!(error_type));
|
||||||
|
error.insert("message".to_string(), json!(message));
|
||||||
|
if let Some(details) = details {
|
||||||
|
error.insert("details".to_string(), details);
|
||||||
|
}
|
||||||
|
(
|
||||||
|
status,
|
||||||
|
Json(json!({ "error": serde_json::Value::Object(error) })),
|
||||||
|
)
|
||||||
|
.into_response()
|
||||||
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
use super::super::super::auth::build_proxy_error_response;
|
|
||||||
use super::shared::{
|
use super::shared::{
|
||||||
admin_adaptive_adjustment_items, admin_adaptive_dispatcher_not_found_response,
|
admin_adaptive_adjustment_items, admin_adaptive_dispatcher_not_found_response,
|
||||||
admin_adaptive_effective_limit, admin_adaptive_find_key, admin_adaptive_key_id_from_path,
|
admin_adaptive_effective_limit, admin_adaptive_find_key, admin_adaptive_key_id_from_path,
|
||||||
@@ -6,7 +5,9 @@ use super::shared::{
|
|||||||
admin_adaptive_load_candidate_keys,
|
admin_adaptive_load_candidate_keys,
|
||||||
};
|
};
|
||||||
use crate::control::GatewayPublicRequestContext;
|
use crate::control::GatewayPublicRequestContext;
|
||||||
use crate::handlers::admin::shared::{query_param_value, unix_secs_to_rfc3339};
|
use crate::handlers::admin::shared::{
|
||||||
|
build_proxy_error_response, query_param_value, unix_secs_to_rfc3339,
|
||||||
|
};
|
||||||
use crate::{AppState, GatewayError};
|
use crate::{AppState, GatewayError};
|
||||||
use axum::{
|
use axum::{
|
||||||
body::{Body, Bytes},
|
body::{Body, Bytes},
|
||||||
|
|||||||
@@ -1,20 +1,18 @@
|
|||||||
use super::ADMIN_AWS_REGIONS;
|
use super::ADMIN_AWS_REGIONS;
|
||||||
use crate::control::GatewayPublicRequestContext;
|
use crate::control::GatewayPublicRequestContext;
|
||||||
use crate::handlers::admin::auth::build_proxy_error_response;
|
|
||||||
use crate::handlers::admin::shared::attach_admin_audit_response;
|
use crate::handlers::admin::shared::attach_admin_audit_response;
|
||||||
|
use crate::handlers::admin::shared::build_proxy_error_response;
|
||||||
use crate::handlers::admin::system::shared::{
|
use crate::handlers::admin::system::shared::{
|
||||||
apply_admin_system_config_update, apply_admin_system_settings_update,
|
|
||||||
admin_system_config_key_from_path, admin_system_email_template_preview_type_from_path,
|
admin_system_config_key_from_path, admin_system_email_template_preview_type_from_path,
|
||||||
admin_system_email_template_reset_type_from_path, admin_system_email_template_type_from_path,
|
admin_system_email_template_reset_type_from_path, admin_system_email_template_type_from_path,
|
||||||
build_admin_api_formats_payload, build_admin_system_check_update_payload,
|
apply_admin_email_template_update, apply_admin_system_config_update,
|
||||||
build_admin_system_config_detail_payload, build_admin_system_config_export_payload,
|
apply_admin_system_settings_update, build_admin_api_formats_payload,
|
||||||
build_admin_system_configs_payload, build_admin_system_settings_payload,
|
|
||||||
build_admin_system_stats_payload, build_admin_system_users_export_payload,
|
|
||||||
current_aether_version, delete_admin_system_config, is_admin_system_configs_root,
|
|
||||||
is_admin_system_email_templates_root,
|
|
||||||
};
|
|
||||||
use crate::handlers::public::{
|
|
||||||
build_admin_email_template_payload, build_admin_email_templates_payload,
|
build_admin_email_template_payload, build_admin_email_templates_payload,
|
||||||
|
build_admin_system_check_update_payload, build_admin_system_config_detail_payload,
|
||||||
|
build_admin_system_config_export_payload, build_admin_system_configs_payload,
|
||||||
|
build_admin_system_settings_payload, build_admin_system_stats_payload,
|
||||||
|
build_admin_system_users_export_payload, current_aether_version, delete_admin_system_config,
|
||||||
|
is_admin_system_configs_root, is_admin_system_email_templates_root,
|
||||||
preview_admin_email_template, reset_admin_email_template,
|
preview_admin_email_template, reset_admin_email_template,
|
||||||
};
|
};
|
||||||
use crate::{AppState, GatewayError};
|
use crate::{AppState, GatewayError};
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use crate::handlers::admin::system::shared::{
|
|||||||
build_admin_module_validation_result, build_admin_modules_status_payload,
|
build_admin_module_validation_result, build_admin_modules_status_payload,
|
||||||
AdminSetModuleEnabledRequest,
|
AdminSetModuleEnabledRequest,
|
||||||
};
|
};
|
||||||
use crate::handlers::public::module_available_from_env;
|
use crate::handlers::shared::module_available_from_env;
|
||||||
use crate::{AppState, GatewayError};
|
use crate::{AppState, GatewayError};
|
||||||
use axum::{
|
use axum::{
|
||||||
body::{Body, Bytes},
|
body::{Body, Bytes},
|
||||||
|
|||||||
@@ -0,0 +1,224 @@
|
|||||||
|
use crate::handlers::shared::{
|
||||||
|
admin_email_template_definition, admin_email_template_html_key,
|
||||||
|
admin_email_template_subject_key, read_admin_email_template_payload,
|
||||||
|
render_admin_email_template_html, system_config_string,
|
||||||
|
};
|
||||||
|
use crate::{AppState, GatewayError};
|
||||||
|
use axum::body::Bytes;
|
||||||
|
use axum::http;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
pub(crate) async fn build_admin_email_templates_payload(
|
||||||
|
state: &AppState,
|
||||||
|
) -> Result<serde_json::Value, GatewayError> {
|
||||||
|
let mut templates = Vec::new();
|
||||||
|
for template_type in ["verification", "password_reset"] {
|
||||||
|
if let Some(payload) = read_admin_email_template_payload(state, template_type).await? {
|
||||||
|
let mut payload = payload;
|
||||||
|
if let Some(object) = payload.as_object_mut() {
|
||||||
|
object.remove("default_subject");
|
||||||
|
object.remove("default_html");
|
||||||
|
}
|
||||||
|
templates.push(payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(json!({ "templates": templates }))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn build_admin_email_template_payload(
|
||||||
|
state: &AppState,
|
||||||
|
template_type: &str,
|
||||||
|
) -> Result<Result<serde_json::Value, (http::StatusCode, serde_json::Value)>, GatewayError> {
|
||||||
|
let Some(payload) = read_admin_email_template_payload(state, template_type).await? else {
|
||||||
|
return Ok(Err((
|
||||||
|
http::StatusCode::NOT_FOUND,
|
||||||
|
json!({ "detail": format!("模板类型 '{template_type}' 不存在") }),
|
||||||
|
)));
|
||||||
|
};
|
||||||
|
Ok(Ok(payload))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn apply_admin_email_template_update(
|
||||||
|
state: &AppState,
|
||||||
|
template_type: &str,
|
||||||
|
request_body: &Bytes,
|
||||||
|
) -> Result<Result<serde_json::Value, (http::StatusCode, serde_json::Value)>, GatewayError> {
|
||||||
|
let Some(definition) = admin_email_template_definition(template_type) else {
|
||||||
|
return Ok(Err((
|
||||||
|
http::StatusCode::NOT_FOUND,
|
||||||
|
json!({ "detail": format!("模板类型 '{template_type}' 不存在") }),
|
||||||
|
)));
|
||||||
|
};
|
||||||
|
let payload = match serde_json::from_slice::<serde_json::Value>(request_body) {
|
||||||
|
Ok(serde_json::Value::Object(payload)) => payload,
|
||||||
|
_ => {
|
||||||
|
return Ok(Err((
|
||||||
|
http::StatusCode::BAD_REQUEST,
|
||||||
|
json!({ "detail": "请求数据验证失败" }),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let subject = match payload.get("subject") {
|
||||||
|
Some(serde_json::Value::String(value)) => Some(value.clone()),
|
||||||
|
Some(serde_json::Value::Null) | None => None,
|
||||||
|
Some(_) => {
|
||||||
|
return Ok(Err((
|
||||||
|
http::StatusCode::BAD_REQUEST,
|
||||||
|
json!({ "detail": "请求数据验证失败" }),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let html = match payload.get("html") {
|
||||||
|
Some(serde_json::Value::String(value)) => Some(value.clone()),
|
||||||
|
Some(serde_json::Value::Null) | None => None,
|
||||||
|
Some(_) => {
|
||||||
|
return Ok(Err((
|
||||||
|
http::StatusCode::BAD_REQUEST,
|
||||||
|
json!({ "detail": "请求数据验证失败" }),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if subject.is_none() && html.is_none() {
|
||||||
|
return Ok(Err((
|
||||||
|
http::StatusCode::BAD_REQUEST,
|
||||||
|
json!({ "detail": "请提供 subject 或 html" }),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let subject_key = admin_email_template_subject_key(definition.template_type);
|
||||||
|
let html_key = admin_email_template_html_key(definition.template_type);
|
||||||
|
|
||||||
|
if let Some(subject) = subject {
|
||||||
|
if subject.is_empty() {
|
||||||
|
let _ = state.delete_system_config_value(&subject_key).await?;
|
||||||
|
} else {
|
||||||
|
let _ = state
|
||||||
|
.upsert_system_config_json_value(&subject_key, &json!(subject), None)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(html) = html {
|
||||||
|
if html.is_empty() {
|
||||||
|
let _ = state.delete_system_config_value(&html_key).await?;
|
||||||
|
} else {
|
||||||
|
let _ = state
|
||||||
|
.upsert_system_config_json_value(&html_key, &json!(html), None)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Ok(json!({ "message": "模板保存成功" })))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn preview_admin_email_template(
|
||||||
|
state: &AppState,
|
||||||
|
template_type: &str,
|
||||||
|
request_body: Option<&Bytes>,
|
||||||
|
) -> Result<Result<serde_json::Value, (http::StatusCode, serde_json::Value)>, GatewayError> {
|
||||||
|
let Some(definition) = admin_email_template_definition(template_type) else {
|
||||||
|
return Ok(Err((
|
||||||
|
http::StatusCode::NOT_FOUND,
|
||||||
|
json!({ "detail": format!("模板类型 '{template_type}' 不存在") }),
|
||||||
|
)));
|
||||||
|
};
|
||||||
|
|
||||||
|
let payload = match request_body {
|
||||||
|
Some(bytes) => match serde_json::from_slice::<serde_json::Value>(bytes) {
|
||||||
|
Ok(serde_json::Value::Object(payload)) => payload,
|
||||||
|
Ok(serde_json::Value::Null) => serde_json::Map::new(),
|
||||||
|
_ => {
|
||||||
|
return Ok(Err((
|
||||||
|
http::StatusCode::BAD_REQUEST,
|
||||||
|
json!({ "detail": "请求数据验证失败" }),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => serde_json::Map::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let resolved = read_admin_email_template_payload(state, definition.template_type)
|
||||||
|
.await?
|
||||||
|
.expect("validated template type should exist");
|
||||||
|
let resolved_html = resolved["html"].as_str().unwrap_or(definition.default_html);
|
||||||
|
let html = payload
|
||||||
|
.get("html")
|
||||||
|
.and_then(|value| value.as_str())
|
||||||
|
.filter(|value| !value.is_empty())
|
||||||
|
.unwrap_or(resolved_html);
|
||||||
|
|
||||||
|
let email_app_name = state
|
||||||
|
.read_system_config_json_value("email_app_name")
|
||||||
|
.await?;
|
||||||
|
let smtp_from_name = state
|
||||||
|
.read_system_config_json_value("smtp_from_name")
|
||||||
|
.await?;
|
||||||
|
let app_name = system_config_string(email_app_name.as_ref())
|
||||||
|
.or_else(|| system_config_string(smtp_from_name.as_ref()))
|
||||||
|
.unwrap_or_else(|| "Aether".to_string());
|
||||||
|
|
||||||
|
let mut defaults = std::collections::BTreeMap::new();
|
||||||
|
defaults.insert("app_name".to_string(), app_name);
|
||||||
|
defaults.insert("code".to_string(), "123456".to_string());
|
||||||
|
defaults.insert("expire_minutes".to_string(), "30".to_string());
|
||||||
|
defaults.insert("email".to_string(), "example@example.com".to_string());
|
||||||
|
defaults.insert(
|
||||||
|
"reset_link".to_string(),
|
||||||
|
"https://example.com/reset?token=abc123".to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let preview_variables = definition
|
||||||
|
.variables
|
||||||
|
.iter()
|
||||||
|
.map(|key| {
|
||||||
|
let value = payload
|
||||||
|
.get(*key)
|
||||||
|
.map(|value| match value {
|
||||||
|
serde_json::Value::String(value) => value.clone(),
|
||||||
|
serde_json::Value::Null => "None".to_string(),
|
||||||
|
_ => value.to_string(),
|
||||||
|
})
|
||||||
|
.or_else(|| defaults.get(*key).cloned())
|
||||||
|
.unwrap_or_else(|| format!("{{{{{key}}}}}"));
|
||||||
|
((*key).to_string(), value)
|
||||||
|
})
|
||||||
|
.collect::<std::collections::BTreeMap<_, _>>();
|
||||||
|
|
||||||
|
let rendered_html = render_admin_email_template_html(html, &preview_variables)?;
|
||||||
|
|
||||||
|
Ok(Ok(json!({
|
||||||
|
"html": rendered_html,
|
||||||
|
"variables": preview_variables,
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn reset_admin_email_template(
|
||||||
|
state: &AppState,
|
||||||
|
template_type: &str,
|
||||||
|
) -> Result<Result<serde_json::Value, (http::StatusCode, serde_json::Value)>, GatewayError> {
|
||||||
|
let Some(definition) = admin_email_template_definition(template_type) else {
|
||||||
|
return Ok(Err((
|
||||||
|
http::StatusCode::NOT_FOUND,
|
||||||
|
json!({ "detail": format!("模板类型 '{template_type}' 不存在") }),
|
||||||
|
)));
|
||||||
|
};
|
||||||
|
|
||||||
|
let _ = state
|
||||||
|
.delete_system_config_value(&admin_email_template_subject_key(definition.template_type))
|
||||||
|
.await?;
|
||||||
|
let _ = state
|
||||||
|
.delete_system_config_value(&admin_email_template_html_key(definition.template_type))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(Ok(json!({
|
||||||
|
"message": "模板已重置为默认值",
|
||||||
|
"template": {
|
||||||
|
"type": definition.template_type,
|
||||||
|
"name": definition.name,
|
||||||
|
"subject": definition.default_subject,
|
||||||
|
"html": definition.default_html,
|
||||||
|
}
|
||||||
|
})))
|
||||||
|
}
|
||||||
@@ -1,9 +1,14 @@
|
|||||||
mod configs;
|
mod configs;
|
||||||
|
mod email_templates;
|
||||||
mod modules;
|
mod modules;
|
||||||
mod paths;
|
mod paths;
|
||||||
mod settings;
|
mod settings;
|
||||||
|
|
||||||
pub(crate) use self::configs::*;
|
pub(crate) use self::configs::*;
|
||||||
|
pub(crate) use self::email_templates::{
|
||||||
|
apply_admin_email_template_update, build_admin_email_template_payload,
|
||||||
|
build_admin_email_templates_payload, preview_admin_email_template, reset_admin_email_template,
|
||||||
|
};
|
||||||
pub(crate) use self::modules::*;
|
pub(crate) use self::modules::*;
|
||||||
pub(crate) use self::paths::*;
|
pub(crate) use self::paths::*;
|
||||||
pub(crate) use self::settings::*;
|
pub(crate) use self::settings::*;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::handlers::public::{module_available_from_env, system_config_bool};
|
use crate::handlers::shared::{module_available_from_env, system_config_bool};
|
||||||
use crate::{AppState, GatewayError};
|
use crate::{AppState, GatewayError};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::handlers::public::{system_config_bool, system_config_string};
|
use crate::handlers::shared::{system_config_bool, system_config_string};
|
||||||
use crate::{AppState, GatewayError};
|
use crate::{AppState, GatewayError};
|
||||||
use axum::body::Bytes;
|
use axum::body::Bytes;
|
||||||
use axum::http;
|
use axum::http;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -14,14 +14,9 @@ pub(crate) use self::catalog_helpers::{
|
|||||||
ApiFormatHealthMonitorOptions,
|
ApiFormatHealthMonitorOptions,
|
||||||
};
|
};
|
||||||
pub(crate) use self::system_modules_helpers::{
|
pub(crate) use self::system_modules_helpers::{
|
||||||
apply_admin_email_template_update, build_admin_email_template_payload,
|
build_admin_keys_grouped_by_format_payload, build_public_auth_modules_status_payload,
|
||||||
build_admin_email_templates_payload, build_admin_keys_grouped_by_format_payload,
|
capability_detail_by_name, enabled_key_capability_short_names, ldap_module_config_is_valid,
|
||||||
build_public_auth_modules_status_payload, capability_detail_by_name,
|
serialize_public_capability, supported_capability_names, PUBLIC_CAPABILITY_DEFINITIONS,
|
||||||
enabled_key_capability_short_names, escape_admin_email_template_html,
|
|
||||||
ldap_module_config_is_valid, module_available_from_env, preview_admin_email_template,
|
|
||||||
read_admin_email_template_payload, render_admin_email_template_html,
|
|
||||||
reset_admin_email_template, serialize_public_capability, supported_capability_names,
|
|
||||||
system_config_bool, system_config_string, PUBLIC_CAPABILITY_DEFINITIONS,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(crate) use self::support::{
|
pub(crate) use self::support::{
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
use super::{
|
use super::{
|
||||||
build_api_format_health_monitor_payload, build_public_auth_modules_status_payload,
|
build_api_format_health_monitor_payload, build_public_auth_modules_status_payload,
|
||||||
build_public_catalog_models_payload, build_public_catalog_search_models_payload,
|
build_public_catalog_models_payload, build_public_catalog_search_models_payload,
|
||||||
build_public_providers_payload, capability_detail_by_name, escape_admin_email_template_html,
|
build_public_providers_payload, capability_detail_by_name, ldap_module_config_is_valid,
|
||||||
ldap_module_config_is_valid, module_available_from_env, read_admin_email_template_payload,
|
serialize_public_capability, supported_capability_names, ApiFormatHealthMonitorOptions,
|
||||||
render_admin_email_template_html, serialize_public_capability, supported_capability_names,
|
|
||||||
system_config_bool, system_config_string, ApiFormatHealthMonitorOptions,
|
|
||||||
PUBLIC_CAPABILITY_DEFINITIONS,
|
PUBLIC_CAPABILITY_DEFINITIONS,
|
||||||
};
|
};
|
||||||
use crate::control::GatewayPublicRequestContext;
|
use crate::control::GatewayPublicRequestContext;
|
||||||
use crate::handlers::shared::{
|
use crate::handlers::shared::{
|
||||||
decrypt_catalog_secret_with_fallbacks, encrypt_catalog_secret_with_fallbacks, query_param_bool,
|
decrypt_catalog_secret_with_fallbacks, encrypt_catalog_secret_with_fallbacks,
|
||||||
query_param_optional_bool, query_param_value, unix_secs_to_rfc3339,
|
escape_admin_email_template_html, module_available_from_env, query_param_bool,
|
||||||
|
query_param_optional_bool, query_param_value, read_admin_email_template_payload,
|
||||||
|
render_admin_email_template_html, system_config_bool, system_config_string,
|
||||||
|
unix_secs_to_rfc3339,
|
||||||
};
|
};
|
||||||
use crate::{AppState, GatewayError};
|
use crate::{AppState, GatewayError};
|
||||||
use aether_data_contracts::repository::global_models::PublicGlobalModelQuery;
|
use aether_data_contracts::repository::global_models::PublicGlobalModelQuery;
|
||||||
|
|||||||
@@ -15,10 +15,3 @@ pub(crate) use self::system_modules_keys_grouped::build_admin_keys_grouped_by_fo
|
|||||||
pub(crate) use self::system_modules_modules::{
|
pub(crate) use self::system_modules_modules::{
|
||||||
build_public_auth_modules_status_payload, ldap_module_config_is_valid,
|
build_public_auth_modules_status_payload, ldap_module_config_is_valid,
|
||||||
};
|
};
|
||||||
pub(crate) use self::system_modules_system::{
|
|
||||||
apply_admin_email_template_update, build_admin_email_template_payload,
|
|
||||||
build_admin_email_templates_payload, escape_admin_email_template_html,
|
|
||||||
module_available_from_env, preview_admin_email_template, read_admin_email_template_payload,
|
|
||||||
render_admin_email_template_html, reset_admin_email_template, system_config_bool,
|
|
||||||
system_config_string,
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use super::{module_available_from_env, system_config_bool};
|
use crate::handlers::shared::{module_available_from_env, system_config_bool};
|
||||||
use crate::{AppState, GatewayError};
|
use crate::{AppState, GatewayError};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
|
|||||||
@@ -1,256 +1,4 @@
|
|||||||
use crate::{AppState, GatewayError};
|
pub(crate) use crate::handlers::shared::{
|
||||||
use serde_json::json;
|
escape_admin_email_template_html, module_available_from_env, read_admin_email_template_payload,
|
||||||
|
render_admin_email_template_html, system_config_bool, system_config_string,
|
||||||
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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct AdminEmailTemplateDefinition {
|
|
||||||
template_type: &'static str,
|
|
||||||
name: &'static str,
|
|
||||||
variables: &'static [&'static str],
|
|
||||||
default_subject: &'static str,
|
|
||||||
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>"#,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn admin_email_template_subject_key(template_type: &str) -> String {
|
|
||||||
format!("email_template_{template_type}_subject")
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|||||||
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 admin_proxy;
|
||||||
mod catalog;
|
mod catalog;
|
||||||
|
mod email_templates;
|
||||||
mod external_models;
|
mod external_models;
|
||||||
mod normalize;
|
mod normalize;
|
||||||
mod payloads;
|
mod payloads;
|
||||||
mod request_utils;
|
mod request_utils;
|
||||||
|
mod system_config_values;
|
||||||
mod usage_stats;
|
mod usage_stats;
|
||||||
|
|
||||||
pub(crate) use self::admin_proxy::{
|
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_catalog_key_supports_format, provider_key_health_summary,
|
||||||
provider_key_status_snapshot_payload,
|
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::external_models::OFFICIAL_EXTERNAL_MODEL_PROVIDERS;
|
||||||
pub(crate) use self::normalize::{
|
pub(crate) use self::normalize::{
|
||||||
normalize_json_array, normalize_json_object, normalize_string_list,
|
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,
|
sanitize_upstream_path_and_query, should_strip_forwarded_provider_credential_header,
|
||||||
should_strip_forwarded_trusted_admin_header, strip_query_param, unix_secs_to_rfc3339,
|
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::{
|
pub(crate) use self::usage_stats::{
|
||||||
admin_stats_bad_request_response, list_usage_for_optional_range, parse_bounded_u32, round_to,
|
admin_stats_bad_request_response, list_usage_for_optional_range, parse_bounded_u32, round_to,
|
||||||
AdminStatsTimeRange, AdminStatsUsageFilter,
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1095,10 +1095,29 @@ fn admin_shared_does_not_own_system_core_routes_or_payloads() {
|
|||||||
"auth/oauth_config.rs should own {pattern}"
|
"auth/oauth_config.rs should own {pattern}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
assert!(
|
||||||
|
!auth_oauth_config.contains("pub(crate) fn build_proxy_error_response"),
|
||||||
|
"auth/oauth_config.rs should not own build_proxy_error_response"
|
||||||
|
);
|
||||||
assert!(
|
assert!(
|
||||||
!workspace_file_exists("apps/aether-gateway/src/handlers/admin/system/shared/payloads.rs"),
|
!workspace_file_exists("apps/aether-gateway/src/handlers/admin/system/shared/payloads.rs"),
|
||||||
"system/shared/payloads.rs should be removed after oauth payload ownership moves to auth"
|
"system/shared/payloads.rs should be removed after oauth payload ownership moves to auth"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let admin_shared_mod =
|
||||||
|
read_workspace_file("apps/aether-gateway/src/handlers/admin/shared/mod.rs");
|
||||||
|
assert!(
|
||||||
|
admin_shared_mod.contains("mod proxy_errors;")
|
||||||
|
&& admin_shared_mod
|
||||||
|
.contains("pub(crate) use self::proxy_errors::build_proxy_error_response;"),
|
||||||
|
"handlers/admin/shared/mod.rs should expose shared admin proxy error builder"
|
||||||
|
);
|
||||||
|
let admin_shared_proxy_errors =
|
||||||
|
read_workspace_file("apps/aether-gateway/src/handlers/admin/shared/proxy_errors.rs");
|
||||||
|
assert!(
|
||||||
|
admin_shared_proxy_errors.contains("pub(crate) fn build_proxy_error_response"),
|
||||||
|
"handlers/admin/shared/proxy_errors.rs should own build_proxy_error_response"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1279,6 +1298,29 @@ fn admin_system_and_endpoint_roots_stay_thin() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let system_routes =
|
||||||
|
read_workspace_file("apps/aether-gateway/src/handlers/admin/system/core/system_routes.rs");
|
||||||
|
assert!(
|
||||||
|
!system_routes.contains("use crate::handlers::public::{"),
|
||||||
|
"handlers/admin/system/core/system_routes.rs should not borrow system-owned route helpers from handlers/public"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!system_routes.contains("crate::handlers::admin::auth::build_proxy_error_response")
|
||||||
|
&& !system_routes.contains("use crate::handlers::admin::auth::build_proxy_error_response;"),
|
||||||
|
"handlers/admin/system/core/system_routes.rs should not borrow proxy error builder from auth"
|
||||||
|
);
|
||||||
|
for pattern in [
|
||||||
|
"build_admin_email_template_payload",
|
||||||
|
"build_admin_email_templates_payload",
|
||||||
|
"preview_admin_email_template",
|
||||||
|
"reset_admin_email_template",
|
||||||
|
] {
|
||||||
|
assert!(
|
||||||
|
system_routes.contains(pattern),
|
||||||
|
"handlers/admin/system/core/system_routes.rs should keep delegating through admin system shared helper {pattern}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let endpoint_keys =
|
let endpoint_keys =
|
||||||
read_workspace_file("apps/aether-gateway/src/handlers/admin/endpoint/keys.rs");
|
read_workspace_file("apps/aether-gateway/src/handlers/admin/endpoint/keys.rs");
|
||||||
assert!(
|
assert!(
|
||||||
@@ -1349,6 +1391,7 @@ fn admin_provider_root_stays_thin() {
|
|||||||
"pub(crate) use self::endpoints_admin::{",
|
"pub(crate) use self::endpoints_admin::{",
|
||||||
"pub(crate) use self::pool_admin::{",
|
"pub(crate) use self::pool_admin::{",
|
||||||
"pub(crate) use self::write::{",
|
"pub(crate) use self::write::{",
|
||||||
|
"build_proxy_error_response",
|
||||||
"build_internal_control_error_response",
|
"build_internal_control_error_response",
|
||||||
"admin_provider_ops_local_action_response",
|
"admin_provider_ops_local_action_response",
|
||||||
"admin_provider_pool_config",
|
"admin_provider_pool_config",
|
||||||
@@ -1469,6 +1512,206 @@ fn admin_system_owns_admin_module_helpers() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn admin_system_owns_system_route_helpers() {
|
||||||
|
let public_system_helpers =
|
||||||
|
read_workspace_file("apps/aether-gateway/src/handlers/public/system_modules_helpers.rs");
|
||||||
|
for pattern in [
|
||||||
|
"current_aether_version",
|
||||||
|
"build_admin_system_check_update_payload",
|
||||||
|
"build_admin_system_stats_payload",
|
||||||
|
"build_admin_system_settings_payload",
|
||||||
|
"apply_admin_system_settings_update",
|
||||||
|
"build_admin_api_formats_payload",
|
||||||
|
"build_admin_system_config_export_payload",
|
||||||
|
"build_admin_system_users_export_payload",
|
||||||
|
"build_admin_system_configs_payload",
|
||||||
|
"build_admin_system_config_detail_payload",
|
||||||
|
"apply_admin_system_config_update",
|
||||||
|
"delete_admin_system_config",
|
||||||
|
"serialize_admin_system_users_export_wallet",
|
||||||
|
"module_available_from_env",
|
||||||
|
"system_config_bool",
|
||||||
|
"system_config_string",
|
||||||
|
"read_admin_email_template_payload",
|
||||||
|
"escape_admin_email_template_html",
|
||||||
|
"render_admin_email_template_html",
|
||||||
|
] {
|
||||||
|
assert!(
|
||||||
|
!public_system_helpers.contains(pattern),
|
||||||
|
"handlers/public/system_modules_helpers.rs should not re-export admin system helper {pattern}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let public_mod = read_workspace_file("apps/aether-gateway/src/handlers/public/mod.rs");
|
||||||
|
for pattern in [
|
||||||
|
"current_aether_version",
|
||||||
|
"build_admin_system_check_update_payload",
|
||||||
|
"build_admin_system_stats_payload",
|
||||||
|
"build_admin_system_settings_payload",
|
||||||
|
"apply_admin_system_settings_update",
|
||||||
|
"build_admin_api_formats_payload",
|
||||||
|
"build_admin_system_config_export_payload",
|
||||||
|
"build_admin_system_users_export_payload",
|
||||||
|
"build_admin_system_configs_payload",
|
||||||
|
"build_admin_system_config_detail_payload",
|
||||||
|
"apply_admin_system_config_update",
|
||||||
|
"delete_admin_system_config",
|
||||||
|
"serialize_admin_system_users_export_wallet",
|
||||||
|
"module_available_from_env",
|
||||||
|
"system_config_bool",
|
||||||
|
"system_config_string",
|
||||||
|
"read_admin_email_template_payload",
|
||||||
|
"escape_admin_email_template_html",
|
||||||
|
"render_admin_email_template_html",
|
||||||
|
] {
|
||||||
|
assert!(
|
||||||
|
!public_mod.contains(pattern),
|
||||||
|
"handlers/public/mod.rs should not re-export admin system helper {pattern}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let public_system_file = read_workspace_file(
|
||||||
|
"apps/aether-gateway/src/handlers/public/system_modules_helpers/system.rs",
|
||||||
|
);
|
||||||
|
for pattern in [
|
||||||
|
"pub(crate) fn current_aether_version",
|
||||||
|
"pub(crate) fn build_admin_system_check_update_payload",
|
||||||
|
"pub(crate) async fn build_admin_system_stats_payload",
|
||||||
|
"pub(crate) async fn build_admin_system_settings_payload",
|
||||||
|
"pub(crate) async fn build_admin_system_config_export_payload",
|
||||||
|
"pub(crate) async fn build_admin_system_users_export_payload",
|
||||||
|
"pub(crate) fn build_admin_system_configs_payload",
|
||||||
|
"pub(crate) async fn build_admin_system_config_detail_payload",
|
||||||
|
"pub(crate) async fn apply_admin_system_config_update",
|
||||||
|
"pub(crate) async fn delete_admin_system_config",
|
||||||
|
"pub(crate) fn serialize_admin_system_users_export_wallet",
|
||||||
|
"pub(crate) fn module_available_from_env",
|
||||||
|
"pub(crate) fn system_config_bool",
|
||||||
|
"pub(crate) fn system_config_string",
|
||||||
|
"pub(crate) async fn read_admin_email_template_payload",
|
||||||
|
"pub(crate) fn escape_admin_email_template_html",
|
||||||
|
"pub(crate) fn render_admin_email_template_html",
|
||||||
|
] {
|
||||||
|
assert!(
|
||||||
|
!public_system_file.contains(pattern),
|
||||||
|
"handlers/public/system_modules_helpers/system.rs should not own shared/admin system helper {pattern}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let shared_mod = read_workspace_file("apps/aether-gateway/src/handlers/shared/mod.rs");
|
||||||
|
for pattern in [
|
||||||
|
"mod email_templates;",
|
||||||
|
"mod system_config_values;",
|
||||||
|
"pub(crate) use self::email_templates::{",
|
||||||
|
"pub(crate) use self::system_config_values::{",
|
||||||
|
] {
|
||||||
|
assert!(
|
||||||
|
shared_mod.contains(pattern),
|
||||||
|
"handlers/shared/mod.rs should wire shared system helper owner {pattern}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let shared_system_config_values =
|
||||||
|
read_workspace_file("apps/aether-gateway/src/handlers/shared/system_config_values.rs");
|
||||||
|
for pattern in [
|
||||||
|
"pub(crate) fn module_available_from_env",
|
||||||
|
"pub(crate) fn system_config_bool",
|
||||||
|
"pub(crate) fn system_config_string",
|
||||||
|
] {
|
||||||
|
assert!(
|
||||||
|
shared_system_config_values.contains(pattern),
|
||||||
|
"handlers/shared/system_config_values.rs should own {pattern}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let shared_email_templates =
|
||||||
|
read_workspace_file("apps/aether-gateway/src/handlers/shared/email_templates.rs");
|
||||||
|
for pattern in [
|
||||||
|
"pub(crate) fn admin_email_template_definition",
|
||||||
|
"pub(crate) fn admin_email_template_subject_key",
|
||||||
|
"pub(crate) fn admin_email_template_html_key",
|
||||||
|
"pub(crate) async fn read_admin_email_template_payload",
|
||||||
|
"pub(crate) fn escape_admin_email_template_html",
|
||||||
|
"pub(crate) fn render_admin_email_template_html",
|
||||||
|
] {
|
||||||
|
assert!(
|
||||||
|
shared_email_templates.contains(pattern),
|
||||||
|
"handlers/shared/email_templates.rs should own {pattern}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let system_shared_mod =
|
||||||
|
read_workspace_file("apps/aether-gateway/src/handlers/admin/system/shared/mod.rs");
|
||||||
|
for pattern in [
|
||||||
|
"mod configs;",
|
||||||
|
"mod email_templates;",
|
||||||
|
"mod settings;",
|
||||||
|
"pub(crate) use self::email_templates::{",
|
||||||
|
"pub(crate) use self::configs::*;",
|
||||||
|
"pub(crate) use self::settings::*;",
|
||||||
|
] {
|
||||||
|
assert!(
|
||||||
|
system_shared_mod.contains(pattern),
|
||||||
|
"handlers/admin/system/shared/mod.rs should wire system helper owner {pattern}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
!workspace_file_exists("apps/aether-gateway/src/handlers/admin/system/shared/system.rs"),
|
||||||
|
"handlers/admin/system/shared/system.rs should be replaced by email_templates.rs"
|
||||||
|
);
|
||||||
|
|
||||||
|
let system_shared_settings =
|
||||||
|
read_workspace_file("apps/aether-gateway/src/handlers/admin/system/shared/settings.rs");
|
||||||
|
for pattern in [
|
||||||
|
"pub(crate) fn current_aether_version",
|
||||||
|
"pub(crate) fn build_admin_system_check_update_payload",
|
||||||
|
"pub(crate) async fn build_admin_system_stats_payload",
|
||||||
|
"pub(crate) async fn build_admin_system_settings_payload",
|
||||||
|
"pub(crate) async fn apply_admin_system_settings_update",
|
||||||
|
"pub(crate) fn build_admin_api_formats_payload",
|
||||||
|
] {
|
||||||
|
assert!(
|
||||||
|
system_shared_settings.contains(pattern),
|
||||||
|
"handlers/admin/system/shared/settings.rs should own {pattern}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let system_shared_configs =
|
||||||
|
read_workspace_file("apps/aether-gateway/src/handlers/admin/system/shared/configs.rs");
|
||||||
|
for pattern in [
|
||||||
|
"pub(crate) async fn build_admin_system_config_export_payload",
|
||||||
|
"pub(crate) fn serialize_admin_system_users_export_wallet",
|
||||||
|
"pub(crate) async fn build_admin_system_users_export_payload",
|
||||||
|
"pub(crate) fn build_admin_system_configs_payload",
|
||||||
|
"pub(crate) async fn build_admin_system_config_detail_payload",
|
||||||
|
"pub(crate) async fn apply_admin_system_config_update",
|
||||||
|
"pub(crate) async fn delete_admin_system_config",
|
||||||
|
] {
|
||||||
|
assert!(
|
||||||
|
system_shared_configs.contains(pattern),
|
||||||
|
"handlers/admin/system/shared/configs.rs should own {pattern}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let system_shared_email_templates = read_workspace_file(
|
||||||
|
"apps/aether-gateway/src/handlers/admin/system/shared/email_templates.rs",
|
||||||
|
);
|
||||||
|
for pattern in [
|
||||||
|
"pub(crate) async fn build_admin_email_templates_payload",
|
||||||
|
"pub(crate) async fn build_admin_email_template_payload",
|
||||||
|
"pub(crate) async fn apply_admin_email_template_update",
|
||||||
|
"pub(crate) async fn preview_admin_email_template",
|
||||||
|
"pub(crate) async fn reset_admin_email_template",
|
||||||
|
] {
|
||||||
|
assert!(
|
||||||
|
system_shared_email_templates.contains(pattern),
|
||||||
|
"handlers/admin/system/shared/email_templates.rs should own {pattern}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn admin_monitoring_snapshots_stay_app_local() {
|
fn admin_monitoring_snapshots_stay_app_local() {
|
||||||
let monitoring_cache_types = read_workspace_file(
|
let monitoring_cache_types = read_workspace_file(
|
||||||
|
|||||||
@@ -3871,7 +3871,13 @@ async fn gateway_handles_wallet_balance_locally_without_proxying_upstream() {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn gateway_handles_wallet_today_cost_locally_without_proxying_upstream() {
|
async fn gateway_handles_wallet_today_cost_locally_without_proxying_upstream() {
|
||||||
let now = Utc::now();
|
let now = chrono::DateTime::<chrono::Utc>::from_naive_utc_and_offset(
|
||||||
|
Utc::now()
|
||||||
|
.date_naive()
|
||||||
|
.and_hms_opt(12, 0, 0)
|
||||||
|
.expect("midday should be valid"),
|
||||||
|
chrono::Utc,
|
||||||
|
);
|
||||||
let user = sample_auth_user(now);
|
let user = sample_auth_user(now);
|
||||||
let access_token = build_test_auth_token(
|
let access_token = build_test_auth_token(
|
||||||
"access",
|
"access",
|
||||||
|
|||||||
@@ -590,6 +590,9 @@
|
|||||||
- [x] 2026-04-07:M3 第十九轮已将 `handlers/admin/system/core/model_routes.rs` 的 model-owned catalog/external-cache 路由整体迁入 `handlers/admin/model/catalog_routes.rs`;旧 `system/core/model_routes.rs` 现退化为 thin wrapper,`system/core` 不再保留 model catalog data-unavailable helper。架构护栏同步要求该 route seam 归 `model` 子域拥有
|
- [x] 2026-04-07:M3 第十九轮已将 `handlers/admin/system/core/model_routes.rs` 的 model-owned catalog/external-cache 路由整体迁入 `handlers/admin/model/catalog_routes.rs`;旧 `system/core/model_routes.rs` 现退化为 thin wrapper,`system/core` 不再保留 model catalog data-unavailable helper。架构护栏同步要求该 route seam 归 `model` 子域拥有
|
||||||
- [x] 2026-04-07:M3 第二十轮已将 `handlers/admin/system/core/{management_tokens_routes,modules_routes}.rs` 的 system-owned route owner 迁入 `handlers/admin/system/{management_tokens,modules}.rs`;旧 `system/core/*` 入口现退化为 thin wrapper,`handlers/admin/system/mod.rs` 直接暴露 `management_tokens/modules` seam,不再把这两条 route owner 藏在 `core` 内部
|
- [x] 2026-04-07:M3 第二十轮已将 `handlers/admin/system/core/{management_tokens_routes,modules_routes}.rs` 的 system-owned route owner 迁入 `handlers/admin/system/{management_tokens,modules}.rs`;旧 `system/core/*` 入口现退化为 thin wrapper,`handlers/admin/system/mod.rs` 直接暴露 `management_tokens/modules` seam,不再把这两条 route owner 藏在 `core` 内部
|
||||||
- [x] 2026-04-07:M3 第二十一轮已将 admin module DTO / path parser / runtime / validation / status payload owner 从 `handlers/public/system_modules_helpers/modules.rs` 下沉到 `handlers/admin/system/shared/modules.rs`;`handlers/public/system_modules_helpers.rs` 与 `handlers/public/mod.rs` 不再 re-export 这批 admin helper,public 侧仅保留 public auth module 状态能力。架构护栏同步要求 admin module helper 不得回流到 `handlers/public/system_modules_helpers*`
|
- [x] 2026-04-07:M3 第二十一轮已将 admin module DTO / path parser / runtime / validation / status payload owner 从 `handlers/public/system_modules_helpers/modules.rs` 下沉到 `handlers/admin/system/shared/modules.rs`;`handlers/public/system_modules_helpers.rs` 与 `handlers/public/mod.rs` 不再 re-export 这批 admin helper,public 侧仅保留 public auth module 状态能力。架构护栏同步要求 admin module helper 不得回流到 `handlers/public/system_modules_helpers*`
|
||||||
|
- [x] 2026-04-07:M3 第二十二轮已将 `handlers/admin/system/core/system_routes.rs` 对 system-owned settings / configs / export helper 的依赖切到 `handlers/admin/system/shared/{settings,configs}.rs`;`handlers/public/system_modules_helpers.rs` 与 `handlers/public/mod.rs` 不再对外暴露 `current_aether_version`、system settings/config CRUD、config export/users export 或 `serialize_admin_system_users_export_wallet`
|
||||||
|
- [x] 2026-04-07:M3 第二十三轮已将 `system_config_*`、`module_available_from_env` 与 admin email template 底层读取/渲染原语上提到 `handlers/shared/{system_config_values,email_templates}.rs`,并将 admin 邮件模板 route helper 明确下沉到 `handlers/admin/system/shared/email_templates.rs`;`handlers/public/system_modules_helpers/system.rs` 现退化为 shared helper re-export,不再持有 ownership。架构护栏同步要求 shared owner 固定在 `handlers/shared/*`,且 `handlers/admin/system/shared/system.rs` 被 `email_templates.rs` 取代
|
||||||
|
- [x] 2026-04-07:M3 第二十四轮已将 `build_proxy_error_response` 从 `handlers/admin/auth/oauth_config.rs` 上提到 `handlers/admin/shared/proxy_errors.rs`;`system/core/system_routes.rs`、`system/adaptive/routes.rs`、`features/video_tasks/routes.rs`、`provider/strategy/shared.rs` 与 `auth/oauth_routes.rs` 均改为直接依赖 `handlers/admin/shared`。架构护栏同步禁止该错误 builder 回流到 `auth` 或被 `provider/system` 根入口继续借用
|
||||||
- [x] 2026-04-06:M4 第一轮已完成 `ai_pipeline/planner/gateway_facade.rs` 落地,统一承接 auth snapshot、provider transport、scheduler candidate 选择、request-candidate 持久化与 unused-candidate 标记
|
- [x] 2026-04-06:M4 第一轮已完成 `ai_pipeline/planner/gateway_facade.rs` 落地,统一承接 auth snapshot、provider transport、scheduler candidate 选择、request-candidate 持久化与 unused-candidate 标记
|
||||||
- [x] 2026-04-06:M4 第一轮通过并行子代理完成 `planner/{candidate_affinity,standard/family/**,passthrough/provider/family/**,specialized/{files,video},standard/openai/{chat,cli}/**}` 对 `gateway_facade` 的首批切换
|
- [x] 2026-04-06:M4 第一轮通过并行子代理完成 `planner/{candidate_affinity,standard/family/**,passthrough/provider/family/**,specialized/{files,video},standard/openai/{chat,cli}/**}` 对 `gateway_facade` 的首批切换
|
||||||
- [x] 2026-04-06:M4 第二轮已补齐 `planner/{decision/control_plan,standard/{normalize,matrix},passthrough/provider/request}` 的 facade 类型边界;最终验证通过 `cargo check -p aether-gateway` 与 `cargo test -p aether-gateway --lib`,结果 `1228 passed; 0 failed`
|
- [x] 2026-04-06:M4 第二轮已补齐 `planner/{decision/control_plan,standard/{normalize,matrix},passthrough/provider/request}` 的 facade 类型边界;最终验证通过 `cargo check -p aether-gateway` 与 `cargo test -p aether-gateway --lib`,结果 `1228 passed; 0 failed`
|
||||||
|
|||||||
Reference in New Issue
Block a user