mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-08 02:17:46 +08:00
refactor: 大规模模块拆分与重组,新增 aether-admin crate
- 新建独立 aether-admin crate 承载 admin 相关共享契约与纯辅助函数 - 拆分 ai_pipeline 下 kiro/private_envelope/conversion/planner 等大文件为子模块目录 - 重组 admin handlers 各业务域(billing/oauth/provider/system/users 等)为目录结构,移除 shared.rs/builders.rs 等反模式 - 移除 ai_pipeline runtime adapters 旧实现(claude/openai/gemini/kiro/vertex/antigravity 等),改由 provider transport 统一承载 - 移除 control_facade/execution_facade/auth_snapshot_facade 等冗余 facade 层 - 拆分 query/billing 与 query/monitoring 模块、state/runtime/payments 与 security 模块 - 扩展架构测试覆盖 admin_billing/admin_model/admin_users 等新模块 - 删除 docs/architecture/refactor-execution-plan.md 已完成的执行计划文档
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
use crate::handlers::admin::request::AdminAppState;
|
||||
use crate::handlers::admin::shared::build_proxy_error_response;
|
||||
use crate::GatewayError;
|
||||
use aether_admin::system::{
|
||||
admin_adaptive_dispatcher_not_found_response, admin_adaptive_key_not_found_response,
|
||||
admin_adaptive_key_payload, build_admin_adaptive_reset_learning_payload,
|
||||
build_admin_adaptive_set_limit_payload, build_admin_adaptive_stats_payload,
|
||||
build_admin_adaptive_summary_payload, build_admin_adaptive_toggle_mode_payload,
|
||||
};
|
||||
use axum::{
|
||||
body::{Body, Bytes},
|
||||
http,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
|
||||
impl<'a> AdminAppState<'a> {
|
||||
pub(crate) async fn build_admin_adaptive_keys_response(
|
||||
&self,
|
||||
provider_id: Option<&str>,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
let payload = self
|
||||
.load_admin_adaptive_candidate_keys(provider_id)
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|key| key.rpm_limit.is_none())
|
||||
.map(|key| admin_adaptive_key_payload(&key))
|
||||
.collect::<Vec<_>>();
|
||||
Ok(Json(payload).into_response())
|
||||
}
|
||||
|
||||
pub(crate) async fn build_admin_adaptive_summary_response(
|
||||
&self,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
let keys = self.load_admin_adaptive_candidate_keys(None).await?;
|
||||
Ok(Json(build_admin_adaptive_summary_payload(&keys)).into_response())
|
||||
}
|
||||
|
||||
pub(crate) async fn build_admin_adaptive_stats_response(
|
||||
&self,
|
||||
key_id: &str,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
let Some(key) = self.find_admin_adaptive_key(key_id).await? else {
|
||||
return Ok(admin_adaptive_key_not_found_response(key_id));
|
||||
};
|
||||
Ok(Json(build_admin_adaptive_stats_payload(&key)).into_response())
|
||||
}
|
||||
|
||||
pub(crate) async fn toggle_admin_adaptive_mode_response(
|
||||
&self,
|
||||
key_id: &str,
|
||||
request_body: &Bytes,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct AdminAdaptiveToggleModeRequest {
|
||||
enabled: bool,
|
||||
#[serde(default)]
|
||||
fixed_limit: Option<u32>,
|
||||
}
|
||||
|
||||
let body = match serde_json::from_slice::<AdminAdaptiveToggleModeRequest>(request_body) {
|
||||
Ok(payload) => payload,
|
||||
Err(_) => {
|
||||
return Ok(build_proxy_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"invalid_request",
|
||||
"请求数据验证失败",
|
||||
None,
|
||||
));
|
||||
}
|
||||
};
|
||||
let Some(mut key) = self.find_admin_adaptive_key(key_id).await? else {
|
||||
return Ok(admin_adaptive_key_not_found_response(key_id));
|
||||
};
|
||||
let message = if body.enabled {
|
||||
key.rpm_limit = None;
|
||||
"已切换为自适应模式,系统将自动学习并调整 RPM 限制".to_string()
|
||||
} else {
|
||||
let Some(fixed_limit) = body.fixed_limit else {
|
||||
return Ok((
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
Json(json!({
|
||||
"detail": "禁用自适应模式时必须提供 fixed_limit 参数",
|
||||
})),
|
||||
)
|
||||
.into_response());
|
||||
};
|
||||
if !(1..=100).contains(&fixed_limit) {
|
||||
return Ok((
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
Json(json!({
|
||||
"detail": "fixed_limit 超出范围(1-100)",
|
||||
})),
|
||||
)
|
||||
.into_response());
|
||||
}
|
||||
key.rpm_limit = Some(fixed_limit);
|
||||
format!("已切换为固定限制模式,RPM 限制设为 {fixed_limit}")
|
||||
};
|
||||
let Some(updated) = self.update_provider_catalog_key(&key).await? else {
|
||||
return Ok(admin_adaptive_key_not_found_response(key_id));
|
||||
};
|
||||
Ok(Json(build_admin_adaptive_toggle_mode_payload(&updated, message)).into_response())
|
||||
}
|
||||
|
||||
pub(crate) async fn set_admin_adaptive_limit_response(
|
||||
&self,
|
||||
key_id: &str,
|
||||
limit: u32,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
let Some(mut key) = self.find_admin_adaptive_key(key_id).await? else {
|
||||
return Ok(admin_adaptive_key_not_found_response(key_id));
|
||||
};
|
||||
let was_adaptive = key.rpm_limit.is_none();
|
||||
key.rpm_limit = Some(limit);
|
||||
let Some(updated) = self.update_provider_catalog_key(&key).await? else {
|
||||
return Ok(admin_adaptive_key_not_found_response(key_id));
|
||||
};
|
||||
Ok(Json(build_admin_adaptive_set_limit_payload(
|
||||
&updated,
|
||||
was_adaptive,
|
||||
limit,
|
||||
))
|
||||
.into_response())
|
||||
}
|
||||
|
||||
pub(crate) async fn reset_admin_adaptive_learning_response(
|
||||
&self,
|
||||
key_id: &str,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
let Some(mut key) = self.find_admin_adaptive_key(key_id).await? else {
|
||||
return Ok(admin_adaptive_key_not_found_response(key_id));
|
||||
};
|
||||
key.learned_rpm_limit = None;
|
||||
key.concurrent_429_count = None;
|
||||
key.rpm_429_count = None;
|
||||
key.last_429_at_unix_secs = None;
|
||||
key.last_429_type = None;
|
||||
key.adjustment_history = None;
|
||||
let Some(updated) = self.update_provider_catalog_key(&key).await? else {
|
||||
return Ok(admin_adaptive_key_not_found_response(key_id));
|
||||
};
|
||||
Ok(Json(build_admin_adaptive_reset_learning_payload(&updated.id)).into_response())
|
||||
}
|
||||
|
||||
pub(crate) fn admin_adaptive_dispatcher_not_found_response(&self) -> Response<Body> {
|
||||
admin_adaptive_dispatcher_not_found_response()
|
||||
}
|
||||
|
||||
async fn find_admin_adaptive_key(
|
||||
&self,
|
||||
key_id: &str,
|
||||
) -> Result<
|
||||
Option<aether_data_contracts::repository::provider_catalog::StoredProviderCatalogKey>,
|
||||
GatewayError,
|
||||
> {
|
||||
Ok(self
|
||||
.read_provider_catalog_keys_by_ids(std::slice::from_ref(&key_id.to_string()))
|
||||
.await?
|
||||
.into_iter()
|
||||
.next())
|
||||
}
|
||||
|
||||
async fn load_admin_adaptive_candidate_keys(
|
||||
&self,
|
||||
provider_id: Option<&str>,
|
||||
) -> Result<
|
||||
Vec<aether_data_contracts::repository::provider_catalog::StoredProviderCatalogKey>,
|
||||
GatewayError,
|
||||
> {
|
||||
if let Some(provider_id) = provider_id.filter(|value| !value.trim().is_empty()) {
|
||||
return self
|
||||
.list_provider_catalog_keys_by_provider_ids(std::slice::from_ref(
|
||||
&provider_id.to_string(),
|
||||
))
|
||||
.await;
|
||||
}
|
||||
|
||||
let provider_ids = self
|
||||
.list_provider_catalog_providers(false)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|provider| provider.id)
|
||||
.collect::<Vec<_>>();
|
||||
if provider_ids.is_empty() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
self.list_provider_catalog_keys_by_provider_ids(&provider_ids)
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
use crate::handlers::admin::request::AdminAppState;
|
||||
use crate::handlers::admin::system::shared::configs::is_sensitive_admin_system_config_key;
|
||||
use crate::handlers::admin::system::shared::export::{
|
||||
build_admin_system_export_providers_payload, decrypt_admin_system_export_secret,
|
||||
ADMIN_SYSTEM_CONFIG_EXPORT_VERSION, ADMIN_SYSTEM_EXPORT_PAGE_LIMIT,
|
||||
};
|
||||
use crate::handlers::shared::{system_config_string, unix_secs_to_rfc3339};
|
||||
use crate::GatewayError;
|
||||
use aether_admin::system::serialize_admin_system_users_export_wallet;
|
||||
use aether_data_contracts::repository::global_models::AdminGlobalModelListQuery;
|
||||
use chrono::Utc;
|
||||
use serde_json::json;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
impl<'a> AdminAppState<'a> {
|
||||
pub(crate) async fn build_admin_system_config_export_payload(
|
||||
&self,
|
||||
) -> Result<serde_json::Value, GatewayError> {
|
||||
let global_models = self
|
||||
.list_admin_global_models(&AdminGlobalModelListQuery {
|
||||
offset: 0,
|
||||
limit: ADMIN_SYSTEM_EXPORT_PAGE_LIMIT,
|
||||
is_active: None,
|
||||
search: None,
|
||||
})
|
||||
.await?
|
||||
.items;
|
||||
let global_model_name_by_id = global_models
|
||||
.iter()
|
||||
.map(|model| (model.id.clone(), model.name.clone()))
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
let global_models_data = global_models
|
||||
.iter()
|
||||
.map(|model| {
|
||||
json!({
|
||||
"name": model.name,
|
||||
"display_name": model.display_name,
|
||||
"default_price_per_request": model.default_price_per_request,
|
||||
"default_tiered_pricing": model.default_tiered_pricing,
|
||||
"supported_capabilities": model.supported_capabilities,
|
||||
"config": model.config,
|
||||
"is_active": model.is_active,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let providers_data =
|
||||
build_admin_system_export_providers_payload(self, &global_model_name_by_id).await?;
|
||||
|
||||
let ldap_data = self.get_ldap_module_config().await?.map(|config| {
|
||||
let bind_password = config
|
||||
.bind_password_encrypted
|
||||
.as_deref()
|
||||
.and_then(|ciphertext| decrypt_admin_system_export_secret(self, ciphertext))
|
||||
.unwrap_or_default();
|
||||
json!({
|
||||
"server_url": config.server_url,
|
||||
"bind_dn": config.bind_dn,
|
||||
"bind_password": bind_password,
|
||||
"base_dn": config.base_dn,
|
||||
"user_search_filter": config.user_search_filter,
|
||||
"username_attr": config.username_attr,
|
||||
"email_attr": config.email_attr,
|
||||
"display_name_attr": config.display_name_attr,
|
||||
"is_enabled": config.is_enabled,
|
||||
"is_exclusive": config.is_exclusive,
|
||||
"use_starttls": config.use_starttls,
|
||||
"connect_timeout": config.connect_timeout,
|
||||
})
|
||||
});
|
||||
|
||||
let system_configs = self.list_system_config_entries().await?;
|
||||
let system_configs_data = system_configs
|
||||
.iter()
|
||||
.map(|entry| {
|
||||
let value = if is_sensitive_admin_system_config_key(&entry.key) {
|
||||
entry
|
||||
.value
|
||||
.as_str()
|
||||
.and_then(|ciphertext| decrypt_admin_system_export_secret(self, ciphertext))
|
||||
.map(serde_json::Value::String)
|
||||
.unwrap_or_else(|| entry.value.clone())
|
||||
} else {
|
||||
entry.value.clone()
|
||||
};
|
||||
json!({
|
||||
"key": entry.key,
|
||||
"value": value,
|
||||
"description": entry.description,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let oauth_providers = self.list_oauth_provider_configs().await?;
|
||||
let oauth_data = oauth_providers
|
||||
.iter()
|
||||
.map(|provider| {
|
||||
let client_secret = provider
|
||||
.client_secret_encrypted
|
||||
.as_deref()
|
||||
.and_then(|ciphertext| decrypt_admin_system_export_secret(self, ciphertext))
|
||||
.unwrap_or_default();
|
||||
json!({
|
||||
"provider_type": provider.provider_type,
|
||||
"display_name": provider.display_name,
|
||||
"client_id": provider.client_id,
|
||||
"client_secret": client_secret,
|
||||
"authorization_url_override": provider.authorization_url_override,
|
||||
"token_url_override": provider.token_url_override,
|
||||
"userinfo_url_override": provider.userinfo_url_override,
|
||||
"scopes": provider.scopes,
|
||||
"redirect_uri": provider.redirect_uri,
|
||||
"frontend_callback_url": provider.frontend_callback_url,
|
||||
"attribute_mapping": provider.attribute_mapping,
|
||||
"extra_config": provider.extra_config,
|
||||
"is_enabled": provider.is_enabled,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let proxy_nodes = self.list_proxy_nodes().await?;
|
||||
let proxy_nodes_data = proxy_nodes
|
||||
.iter()
|
||||
.map(|node| {
|
||||
json!({
|
||||
"id": node.id,
|
||||
"name": node.name,
|
||||
"ip": node.ip,
|
||||
"port": node.port,
|
||||
"region": node.region,
|
||||
"is_manual": node.is_manual,
|
||||
"proxy_url": node.proxy_url,
|
||||
"proxy_username": node.proxy_username,
|
||||
"proxy_password": node.proxy_password,
|
||||
"tunnel_mode": node.tunnel_mode,
|
||||
"heartbeat_interval": node.heartbeat_interval,
|
||||
"remote_config": node.remote_config,
|
||||
"config_version": node.config_version,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(json!({
|
||||
"version": ADMIN_SYSTEM_CONFIG_EXPORT_VERSION,
|
||||
"exported_at": Utc::now().to_rfc3339(),
|
||||
"global_models": global_models_data,
|
||||
"providers": providers_data,
|
||||
"proxy_nodes": proxy_nodes_data,
|
||||
"ldap_config": ldap_data,
|
||||
"oauth_providers": oauth_data,
|
||||
"system_configs": system_configs_data,
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) async fn build_admin_system_users_export_payload(
|
||||
&self,
|
||||
) -> Result<serde_json::Value, GatewayError> {
|
||||
const ADMIN_SYSTEM_USERS_EXPORT_VERSION: &str = "1.3";
|
||||
|
||||
let users = self.list_non_admin_export_users().await?;
|
||||
let user_ids = users.iter().map(|user| user.id.clone()).collect::<Vec<_>>();
|
||||
let user_wallets = self.list_wallet_snapshots_by_user_ids(&user_ids).await?;
|
||||
let user_api_keys = self
|
||||
.list_auth_api_key_export_records_by_user_ids(&user_ids)
|
||||
.await?;
|
||||
let standalone_api_keys = self.list_auth_api_key_export_standalone_records().await?;
|
||||
let standalone_api_key_ids = standalone_api_keys
|
||||
.iter()
|
||||
.map(|key| key.api_key_id.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let standalone_wallets = self
|
||||
.list_wallet_snapshots_by_api_key_ids(&standalone_api_key_ids)
|
||||
.await?;
|
||||
|
||||
let wallets_by_user_id = user_wallets
|
||||
.into_iter()
|
||||
.filter_map(|wallet| wallet.user_id.clone().map(|user_id| (user_id, wallet)))
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
let wallets_by_api_key_id = standalone_wallets
|
||||
.into_iter()
|
||||
.filter_map(|wallet| {
|
||||
wallet
|
||||
.api_key_id
|
||||
.clone()
|
||||
.map(|api_key_id| (api_key_id, wallet))
|
||||
})
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
|
||||
let mut api_keys_by_user_id = BTreeMap::<
|
||||
String,
|
||||
Vec<aether_data::repository::auth::StoredAuthApiKeyExportRecord>,
|
||||
>::new();
|
||||
for key in user_api_keys.into_iter().filter(|key| !key.is_standalone) {
|
||||
api_keys_by_user_id
|
||||
.entry(key.user_id.clone())
|
||||
.or_default()
|
||||
.push(key);
|
||||
}
|
||||
|
||||
let users_data = users
|
||||
.iter()
|
||||
.map(|user| {
|
||||
let wallet = wallets_by_user_id.get(&user.id);
|
||||
let wallet_payload = serialize_admin_system_users_export_wallet(wallet);
|
||||
let api_keys = api_keys_by_user_id.remove(&user.id).unwrap_or_default();
|
||||
let api_keys_payload = api_keys
|
||||
.iter()
|
||||
.map(|key| {
|
||||
self.build_admin_system_users_export_api_key_payload(key, None, true)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
json!({
|
||||
"email": user.email.clone(),
|
||||
"email_verified": user.email_verified,
|
||||
"username": user.username.clone(),
|
||||
"password_hash": user.password_hash.clone(),
|
||||
"role": user.role.clone(),
|
||||
"allowed_providers": user.allowed_providers.clone(),
|
||||
"allowed_api_formats": user.allowed_api_formats.clone(),
|
||||
"allowed_models": user.allowed_models.clone(),
|
||||
"rate_limit": user.rate_limit,
|
||||
"model_capability_settings": user.model_capability_settings.clone(),
|
||||
"unlimited": wallet
|
||||
.map(|entry| entry.limit_mode.eq_ignore_ascii_case("unlimited"))
|
||||
.unwrap_or(false),
|
||||
"wallet": wallet_payload,
|
||||
"is_active": user.is_active,
|
||||
"api_keys": api_keys_payload,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let standalone_keys_data = standalone_api_keys
|
||||
.iter()
|
||||
.map(|key| {
|
||||
self.build_admin_system_users_export_api_key_payload(
|
||||
key,
|
||||
wallets_by_api_key_id.get(&key.api_key_id),
|
||||
false,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(json!({
|
||||
"version": ADMIN_SYSTEM_USERS_EXPORT_VERSION,
|
||||
"exported_at": Utc::now().to_rfc3339(),
|
||||
"users": users_data,
|
||||
"standalone_keys": standalone_keys_data,
|
||||
}))
|
||||
}
|
||||
|
||||
fn build_admin_system_users_export_api_key_payload(
|
||||
&self,
|
||||
key: &aether_data::repository::auth::StoredAuthApiKeyExportRecord,
|
||||
wallet: Option<&aether_data::repository::wallet::StoredWalletSnapshot>,
|
||||
include_is_standalone: bool,
|
||||
) -> serde_json::Value {
|
||||
let mut payload = serde_json::Map::from_iter([
|
||||
("key_hash".to_string(), json!(key.key_hash.clone())),
|
||||
("name".to_string(), json!(key.name.clone())),
|
||||
(
|
||||
"allowed_providers".to_string(),
|
||||
json!(key.allowed_providers.clone()),
|
||||
),
|
||||
(
|
||||
"allowed_api_formats".to_string(),
|
||||
json!(key.allowed_api_formats.clone()),
|
||||
),
|
||||
(
|
||||
"allowed_models".to_string(),
|
||||
json!(key.allowed_models.clone()),
|
||||
),
|
||||
("rate_limit".to_string(), json!(key.rate_limit)),
|
||||
("concurrent_limit".to_string(), json!(key.concurrent_limit)),
|
||||
(
|
||||
"force_capabilities".to_string(),
|
||||
json!(key.force_capabilities.clone()),
|
||||
),
|
||||
("is_active".to_string(), json!(key.is_active)),
|
||||
(
|
||||
"expires_at".to_string(),
|
||||
json!(key.expires_at_unix_secs.and_then(unix_secs_to_rfc3339)),
|
||||
),
|
||||
(
|
||||
"auto_delete_on_expiry".to_string(),
|
||||
json!(key.auto_delete_on_expiry),
|
||||
),
|
||||
("total_requests".to_string(), json!(key.total_requests)),
|
||||
("total_cost_usd".to_string(), json!(key.total_cost_usd)),
|
||||
(
|
||||
"wallet".to_string(),
|
||||
serialize_admin_system_users_export_wallet(wallet)
|
||||
.unwrap_or(serde_json::Value::Null),
|
||||
),
|
||||
]);
|
||||
|
||||
if let Some(ciphertext) = key.key_encrypted.as_deref() {
|
||||
if let Some(plaintext) = decrypt_admin_system_export_secret(self, ciphertext) {
|
||||
payload.insert("key".to_string(), serde_json::Value::String(plaintext));
|
||||
} else {
|
||||
payload.insert(
|
||||
"key_encrypted".to_string(),
|
||||
serde_json::Value::String(ciphertext.to_string()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if include_is_standalone {
|
||||
payload.insert("is_standalone".to_string(), json!(key.is_standalone));
|
||||
}
|
||||
|
||||
serde_json::Value::Object(payload)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
use super::AdminAppState;
|
||||
use crate::GatewayError;
|
||||
|
||||
mod adaptive;
|
||||
mod export;
|
||||
mod modules;
|
||||
mod proxy_nodes;
|
||||
mod templates;
|
||||
|
||||
impl<'a> AdminAppState<'a> {
|
||||
pub(crate) async fn upsert_system_config_json_value(
|
||||
&self,
|
||||
key: &str,
|
||||
value: &serde_json::Value,
|
||||
description: Option<&str>,
|
||||
) -> Result<serde_json::Value, GatewayError> {
|
||||
self.app
|
||||
.upsert_system_config_json_value(key, value, description)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn read_system_config_json_value(
|
||||
&self,
|
||||
key: &str,
|
||||
) -> Result<Option<serde_json::Value>, GatewayError> {
|
||||
self.app.read_system_config_json_value(key).await
|
||||
}
|
||||
|
||||
pub(crate) async fn list_system_config_entries(
|
||||
&self,
|
||||
) -> Result<Vec<crate::data::state::StoredSystemConfigEntry>, GatewayError> {
|
||||
self.app.list_system_config_entries().await
|
||||
}
|
||||
|
||||
pub(crate) async fn upsert_system_config_entry(
|
||||
&self,
|
||||
key: &str,
|
||||
value: &serde_json::Value,
|
||||
description: Option<&str>,
|
||||
) -> Result<crate::data::state::StoredSystemConfigEntry, GatewayError> {
|
||||
self.app
|
||||
.upsert_system_config_entry(key, value, description)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_system_config_value(&self, key: &str) -> Result<bool, GatewayError> {
|
||||
self.app.delete_system_config_value(key).await
|
||||
}
|
||||
|
||||
pub(crate) async fn list_proxy_nodes(
|
||||
&self,
|
||||
) -> Result<Vec<aether_data::repository::proxy_nodes::StoredProxyNode>, GatewayError> {
|
||||
self.app.list_proxy_nodes().await
|
||||
}
|
||||
|
||||
pub(crate) async fn read_admin_system_stats(
|
||||
&self,
|
||||
) -> Result<aether_data::repository::system::AdminSystemStats, GatewayError> {
|
||||
self.app.read_admin_system_stats().await
|
||||
}
|
||||
|
||||
pub(crate) async fn find_proxy_node(
|
||||
&self,
|
||||
node_id: &str,
|
||||
) -> Result<Option<aether_data::repository::proxy_nodes::StoredProxyNode>, GatewayError> {
|
||||
self.app.find_proxy_node(node_id).await
|
||||
}
|
||||
|
||||
pub(crate) async fn list_proxy_node_events(
|
||||
&self,
|
||||
node_id: &str,
|
||||
limit: usize,
|
||||
) -> Result<Vec<aether_data::repository::proxy_nodes::StoredProxyNodeEvent>, GatewayError> {
|
||||
self.app.list_proxy_node_events(node_id, limit).await
|
||||
}
|
||||
|
||||
pub(crate) async fn read_admin_email_template_payload(
|
||||
&self,
|
||||
template_type: &str,
|
||||
) -> Result<Option<serde_json::Value>, GatewayError> {
|
||||
crate::handlers::shared::read_admin_email_template_payload(self.app, template_type).await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
use crate::handlers::admin::request::AdminAppState;
|
||||
use crate::handlers::admin::system::shared::modules as admin_system_modules;
|
||||
use crate::handlers::shared::module_available_from_env;
|
||||
use crate::GatewayError;
|
||||
use axum::{body::Bytes, http};
|
||||
use serde_json::json;
|
||||
|
||||
impl<'a> AdminAppState<'a> {
|
||||
pub(crate) async fn build_admin_modules_status_payload(
|
||||
&self,
|
||||
) -> Result<serde_json::Value, GatewayError> {
|
||||
admin_system_modules::build_admin_modules_status_payload(self).await
|
||||
}
|
||||
|
||||
pub(crate) async fn build_admin_module_status_payload(
|
||||
&self,
|
||||
module_name: &str,
|
||||
) -> Result<Result<serde_json::Value, (http::StatusCode, serde_json::Value)>, GatewayError>
|
||||
{
|
||||
let Some(module) = admin_system_modules::admin_module_by_name(module_name) else {
|
||||
return Ok(Err((
|
||||
http::StatusCode::NOT_FOUND,
|
||||
json!({ "detail": format!("模块 '{module_name}' 不存在") }),
|
||||
)));
|
||||
};
|
||||
let runtime = admin_system_modules::build_admin_module_runtime_state(self).await?;
|
||||
Ok(Ok(admin_system_modules::build_admin_module_status_payload(
|
||||
self, module, &runtime,
|
||||
)
|
||||
.await?))
|
||||
}
|
||||
|
||||
pub(crate) async fn set_admin_module_enabled_payload(
|
||||
&self,
|
||||
module_name: &str,
|
||||
request_body: &Bytes,
|
||||
) -> Result<Result<serde_json::Value, (http::StatusCode, serde_json::Value)>, GatewayError>
|
||||
{
|
||||
let Some(module) = admin_system_modules::admin_module_by_name(module_name) else {
|
||||
return Ok(Err((
|
||||
http::StatusCode::NOT_FOUND,
|
||||
json!({ "detail": format!("模块 '{module_name}' 不存在") }),
|
||||
)));
|
||||
};
|
||||
let available = module_available_from_env(module.env_key, module.default_available);
|
||||
if !available {
|
||||
return Ok(Err((
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
json!({
|
||||
"detail": format!(
|
||||
"模块 '{}' 不可用,无法启用。请检查环境变量 {} 和依赖库。",
|
||||
module.name, module.env_key
|
||||
)
|
||||
}),
|
||||
)));
|
||||
}
|
||||
let payload = match serde_json::from_slice::<
|
||||
admin_system_modules::AdminSetModuleEnabledRequest,
|
||||
>(request_body)
|
||||
{
|
||||
Ok(payload) => payload,
|
||||
Err(_) => {
|
||||
return Ok(Err((
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
json!({ "detail": "请求体格式错误,需要 enabled 字段" }),
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
let runtime = admin_system_modules::build_admin_module_runtime_state(self).await?;
|
||||
if payload.enabled {
|
||||
let (config_validated, config_error) =
|
||||
admin_system_modules::build_admin_module_validation_result(module, &runtime);
|
||||
if !config_validated {
|
||||
return Ok(Err((
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
json!({
|
||||
"detail": format!(
|
||||
"模块配置未验证通过: {}",
|
||||
config_error.unwrap_or_else(|| "未知错误".to_string())
|
||||
)
|
||||
}),
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
let _ = self
|
||||
.upsert_system_config_json_value(
|
||||
&format!("module.{}.enabled", module.name),
|
||||
&json!(payload.enabled),
|
||||
Some(&format!("模块 [{}] 启用状态", module.display_name)),
|
||||
)
|
||||
.await?;
|
||||
let updated_runtime = admin_system_modules::build_admin_module_runtime_state(self).await?;
|
||||
Ok(Ok(admin_system_modules::build_admin_module_status_payload(
|
||||
self,
|
||||
module,
|
||||
&updated_runtime,
|
||||
)
|
||||
.await?))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
use crate::handlers::admin::request::AdminAppState;
|
||||
use crate::GatewayError;
|
||||
use aether_admin::system::{
|
||||
build_admin_proxy_node_event_payload, build_admin_proxy_node_events_payload_response,
|
||||
build_admin_proxy_node_payload, build_admin_proxy_nodes_data_unavailable_response,
|
||||
build_admin_proxy_nodes_invalid_status_response, build_admin_proxy_nodes_list_payload_response,
|
||||
build_admin_proxy_nodes_not_found_response,
|
||||
};
|
||||
use axum::{body::Body, response::Response};
|
||||
|
||||
impl<'a> AdminAppState<'a> {
|
||||
pub(crate) async fn build_admin_proxy_nodes_list_response(
|
||||
&self,
|
||||
skip: usize,
|
||||
limit: usize,
|
||||
status: Option<String>,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
if !self.has_proxy_node_reader() {
|
||||
return Ok(build_admin_proxy_nodes_data_unavailable_response());
|
||||
}
|
||||
if let Some(status) = status.as_deref() {
|
||||
if !matches!(status, "offline" | "online") {
|
||||
return Ok(build_admin_proxy_nodes_invalid_status_response());
|
||||
}
|
||||
}
|
||||
|
||||
let mut nodes = self.list_proxy_nodes().await?;
|
||||
nodes.sort_by(|left, right| left.name.cmp(&right.name));
|
||||
let filtered = nodes
|
||||
.into_iter()
|
||||
.filter(|node| {
|
||||
status
|
||||
.as_deref()
|
||||
.map(|value| node.status.eq_ignore_ascii_case(value))
|
||||
.unwrap_or(true)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let total = filtered.len();
|
||||
let items = filtered
|
||||
.into_iter()
|
||||
.skip(skip)
|
||||
.take(limit)
|
||||
.map(|node| build_admin_proxy_node_payload(&node))
|
||||
.collect::<Vec<_>>();
|
||||
Ok(build_admin_proxy_nodes_list_payload_response(
|
||||
items, total, skip, limit,
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) async fn build_admin_proxy_node_events_response(
|
||||
&self,
|
||||
node_id: &str,
|
||||
limit: usize,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
if !self.has_proxy_node_reader() {
|
||||
return Ok(build_admin_proxy_nodes_data_unavailable_response());
|
||||
}
|
||||
if self.find_proxy_node(node_id).await?.is_none() {
|
||||
return Ok(build_admin_proxy_nodes_not_found_response());
|
||||
}
|
||||
let items = self
|
||||
.list_proxy_node_events(node_id, limit)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|event| build_admin_proxy_node_event_payload(&event))
|
||||
.collect::<Vec<_>>();
|
||||
Ok(build_admin_proxy_node_events_payload_response(items))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
use crate::handlers::admin::request::AdminAppState;
|
||||
use crate::handlers::shared::{
|
||||
admin_email_template_definition, admin_email_template_html_key,
|
||||
admin_email_template_subject_key, render_admin_email_template_html, system_config_string,
|
||||
};
|
||||
use crate::GatewayError;
|
||||
use aether_admin::system::{
|
||||
admin_email_template_not_found_error, build_admin_email_template_preview_payload,
|
||||
build_admin_email_template_reset_payload, build_admin_email_template_saved_payload,
|
||||
build_admin_email_templates_payload as build_admin_email_templates_payload_value,
|
||||
parse_admin_email_template_preview_payload, parse_admin_email_template_update,
|
||||
};
|
||||
use axum::{body::Bytes, http};
|
||||
use serde_json::json;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
impl<'a> AdminAppState<'a> {
|
||||
pub(crate) async fn build_admin_email_templates_payload(
|
||||
&self,
|
||||
) -> Result<serde_json::Value, GatewayError> {
|
||||
let mut templates = Vec::new();
|
||||
for template_type in ["verification", "password_reset"] {
|
||||
if let Some(payload) = self
|
||||
.read_admin_email_template_payload(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(build_admin_email_templates_payload_value(templates))
|
||||
}
|
||||
|
||||
pub(crate) async fn build_admin_email_template_payload(
|
||||
&self,
|
||||
template_type: &str,
|
||||
) -> Result<Result<serde_json::Value, (http::StatusCode, serde_json::Value)>, GatewayError>
|
||||
{
|
||||
let Some(payload) = self
|
||||
.read_admin_email_template_payload(template_type)
|
||||
.await?
|
||||
else {
|
||||
return Ok(Err(admin_email_template_not_found_error(template_type)));
|
||||
};
|
||||
Ok(Ok(payload))
|
||||
}
|
||||
|
||||
pub(crate) async fn apply_admin_email_template_update(
|
||||
&self,
|
||||
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(admin_email_template_not_found_error(template_type)));
|
||||
};
|
||||
let update = match parse_admin_email_template_update(request_body) {
|
||||
Ok(update) => update,
|
||||
Err(err) => return Ok(Err(err)),
|
||||
};
|
||||
|
||||
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) = update.subject {
|
||||
if subject.is_empty() {
|
||||
let _ = self.delete_system_config_value(&subject_key).await?;
|
||||
} else {
|
||||
let _ = self
|
||||
.upsert_system_config_json_value(
|
||||
&subject_key,
|
||||
&serde_json::json!(subject),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(html) = update.html {
|
||||
if html.is_empty() {
|
||||
let _ = self.delete_system_config_value(&html_key).await?;
|
||||
} else {
|
||||
let _ = self
|
||||
.upsert_system_config_json_value(&html_key, &serde_json::json!(html), None)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Ok(build_admin_email_template_saved_payload()))
|
||||
}
|
||||
|
||||
pub(crate) async fn preview_admin_email_template(
|
||||
&self,
|
||||
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(admin_email_template_not_found_error(template_type)));
|
||||
};
|
||||
|
||||
let payload = match parse_admin_email_template_preview_payload(
|
||||
request_body.map(|bytes| bytes.as_ref()),
|
||||
) {
|
||||
Ok(payload) => payload,
|
||||
Err(err) => return Ok(Err(err)),
|
||||
};
|
||||
|
||||
let resolved = self
|
||||
.read_admin_email_template_payload(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 = self.read_system_config_json_value("email_app_name").await?;
|
||||
let smtp_from_name = self.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 = 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(), "[email protected]".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::<BTreeMap<_, _>>();
|
||||
|
||||
let rendered_html = render_admin_email_template_html(html, &preview_variables)?;
|
||||
|
||||
Ok(Ok(build_admin_email_template_preview_payload(
|
||||
rendered_html,
|
||||
preview_variables,
|
||||
)))
|
||||
}
|
||||
|
||||
pub(crate) async fn reset_admin_email_template(
|
||||
&self,
|
||||
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(admin_email_template_not_found_error(template_type)));
|
||||
};
|
||||
|
||||
let _ = self
|
||||
.delete_system_config_value(&admin_email_template_subject_key(definition.template_type))
|
||||
.await?;
|
||||
let _ = self
|
||||
.delete_system_config_value(&admin_email_template_html_key(definition.template_type))
|
||||
.await?;
|
||||
|
||||
Ok(Ok(build_admin_email_template_reset_payload(
|
||||
definition.template_type,
|
||||
definition.name,
|
||||
definition.default_subject,
|
||||
definition.default_html,
|
||||
)))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user