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:
@@ -1,5 +1,5 @@
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::{AppState, GatewayError};
|
||||
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
|
||||
use crate::GatewayError;
|
||||
use axum::body::{Body, Bytes};
|
||||
use axum::http::Response;
|
||||
|
||||
@@ -7,8 +7,8 @@ mod routes;
|
||||
mod shared;
|
||||
|
||||
pub(crate) async fn maybe_build_local_admin_adaptive_response(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
state: &AdminAppState<'_>,
|
||||
request_context: &AdminRequestContext<'_>,
|
||||
request_body: Option<&Bytes>,
|
||||
) -> Result<Option<Response<Body>>, GatewayError> {
|
||||
routes::maybe_build_local_admin_adaptive_response(state, request_context, request_body).await
|
||||
|
||||
@@ -1,38 +1,23 @@
|
||||
use super::shared::{
|
||||
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_key_not_found_response, admin_adaptive_key_payload,
|
||||
admin_adaptive_load_candidate_keys,
|
||||
};
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::handlers::admin::shared::{
|
||||
build_proxy_error_response, query_param_value, unix_secs_to_rfc3339,
|
||||
};
|
||||
use crate::{AppState, GatewayError};
|
||||
use super::shared::admin_adaptive_key_id_from_path;
|
||||
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
|
||||
use crate::handlers::admin::shared::{build_proxy_error_response, query_param_value};
|
||||
use crate::GatewayError;
|
||||
use axum::{
|
||||
body::{Body, Bytes},
|
||||
http,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
|
||||
const ADMIN_ADAPTIVE_DATA_UNAVAILABLE_DETAIL: &str = "Admin adaptive data unavailable";
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct AdminAdaptiveToggleModeRequest {
|
||||
enabled: bool,
|
||||
#[serde(default)]
|
||||
fixed_limit: Option<u32>,
|
||||
}
|
||||
|
||||
pub(super) async fn maybe_build_local_admin_adaptive_response(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
state: &AdminAppState<'_>,
|
||||
request_context: &AdminRequestContext<'_>,
|
||||
request_body: Option<&Bytes>,
|
||||
) -> Result<Option<Response<Body>>, GatewayError> {
|
||||
let Some(decision) = request_context.control_decision.as_ref() else {
|
||||
let Some(decision) = request_context.decision() else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
@@ -52,136 +37,42 @@ pub(super) async fn maybe_build_local_admin_adaptive_response(
|
||||
}
|
||||
|
||||
if decision.route_kind.as_deref() == Some("list_keys")
|
||||
&& request_context.request_method == http::Method::GET
|
||||
&& request_context.method() == http::Method::GET
|
||||
&& matches!(
|
||||
request_context.request_path.as_str(),
|
||||
request_context.path(),
|
||||
"/api/admin/adaptive/keys" | "/api/admin/adaptive/keys/"
|
||||
)
|
||||
{
|
||||
let provider_id = query_param_value(
|
||||
request_context.request_query_string.as_deref(),
|
||||
"provider_id",
|
||||
);
|
||||
let payload = admin_adaptive_load_candidate_keys(state, provider_id.as_deref())
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|key| key.rpm_limit.is_none())
|
||||
.map(|key| admin_adaptive_key_payload(&key))
|
||||
.collect::<Vec<_>>();
|
||||
return Ok(Some(Json(payload).into_response()));
|
||||
}
|
||||
|
||||
if decision.route_kind.as_deref() == Some("summary")
|
||||
&& request_context.request_method == http::Method::GET
|
||||
&& matches!(
|
||||
request_context.request_path.as_str(),
|
||||
"/api/admin/adaptive/summary" | "/api/admin/adaptive/summary/"
|
||||
)
|
||||
{
|
||||
let keys = admin_adaptive_load_candidate_keys(state, None).await?;
|
||||
let adaptive_keys = keys
|
||||
.into_iter()
|
||||
.filter(|key| key.rpm_limit.is_none())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let total_keys = adaptive_keys.len() as u64;
|
||||
let total_concurrent_429 = adaptive_keys
|
||||
.iter()
|
||||
.map(|key| u64::from(key.concurrent_429_count.unwrap_or(0)))
|
||||
.sum::<u64>();
|
||||
let total_rpm_429 = adaptive_keys
|
||||
.iter()
|
||||
.map(|key| u64::from(key.rpm_429_count.unwrap_or(0)))
|
||||
.sum::<u64>();
|
||||
let mut recent_adjustments = vec![];
|
||||
let mut total_adjustments = 0usize;
|
||||
for key in adaptive_keys {
|
||||
let history = admin_adaptive_adjustment_items(key.adjustment_history.as_ref());
|
||||
total_adjustments += history.len();
|
||||
for adjustment in history.into_iter().rev().take(3) {
|
||||
let mut payload = adjustment;
|
||||
payload.insert("key_id".to_string(), json!(key.id));
|
||||
payload.insert("key_name".to_string(), json!(key.name));
|
||||
recent_adjustments.push(serde_json::Value::Object(payload));
|
||||
}
|
||||
}
|
||||
recent_adjustments.sort_by(|left, right| {
|
||||
let lhs = left
|
||||
.get("timestamp")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or_default();
|
||||
let rhs = right
|
||||
.get("timestamp")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or_default();
|
||||
rhs.cmp(lhs)
|
||||
});
|
||||
|
||||
let provider_id = query_param_value(request_context.query_string(), "provider_id");
|
||||
return Ok(Some(
|
||||
Json(json!({
|
||||
"total_adaptive_keys": total_keys,
|
||||
"total_concurrent_429_errors": total_concurrent_429,
|
||||
"total_rpm_429_errors": total_rpm_429,
|
||||
"total_adjustments": total_adjustments,
|
||||
"recent_adjustments": recent_adjustments.into_iter().take(10).collect::<Vec<_>>(),
|
||||
}))
|
||||
.into_response(),
|
||||
state
|
||||
.build_admin_adaptive_keys_response(provider_id.as_deref())
|
||||
.await?,
|
||||
));
|
||||
}
|
||||
|
||||
let key_id = admin_adaptive_key_id_from_path(&request_context.request_path);
|
||||
if decision.route_kind.as_deref() == Some("summary")
|
||||
&& request_context.method() == http::Method::GET
|
||||
&& matches!(
|
||||
request_context.path(),
|
||||
"/api/admin/adaptive/summary" | "/api/admin/adaptive/summary/"
|
||||
)
|
||||
{
|
||||
return Ok(Some(state.build_admin_adaptive_summary_response().await?));
|
||||
}
|
||||
|
||||
let key_id = admin_adaptive_key_id_from_path(request_context.path());
|
||||
if key_id.is_none() {
|
||||
return Ok(Some(admin_adaptive_dispatcher_not_found_response()));
|
||||
return Ok(Some(state.admin_adaptive_dispatcher_not_found_response()));
|
||||
}
|
||||
let key_id = key_id.expect("checked is_some above");
|
||||
|
||||
if decision.route_kind.as_deref() == Some("get_stats")
|
||||
&& request_context.request_method == http::Method::GET
|
||||
&& request_context.request_path.ends_with("/stats")
|
||||
&& request_context.method() == http::Method::GET
|
||||
&& request_context.path().ends_with("/stats")
|
||||
{
|
||||
let Some(key) = admin_adaptive_find_key(state, &key_id).await? else {
|
||||
return Ok(Some(admin_adaptive_key_not_found_response(&key_id)));
|
||||
};
|
||||
let status_snapshot = key
|
||||
.status_snapshot
|
||||
.as_ref()
|
||||
.and_then(serde_json::Value::as_object);
|
||||
let adjustments = admin_adaptive_adjustment_items(key.adjustment_history.as_ref());
|
||||
let adjustment_count = adjustments.len();
|
||||
let recent_adjustments = adjustments
|
||||
.into_iter()
|
||||
.rev()
|
||||
.take(10)
|
||||
.map(serde_json::Value::Object)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
return Ok(Some(
|
||||
Json(json!({
|
||||
"adaptive_mode": key.rpm_limit.is_none(),
|
||||
"rpm_limit": key.rpm_limit,
|
||||
"effective_limit": admin_adaptive_effective_limit(&key),
|
||||
"learned_limit": key.learned_rpm_limit,
|
||||
"concurrent_429_count": key.concurrent_429_count.unwrap_or(0),
|
||||
"rpm_429_count": key.rpm_429_count.unwrap_or(0),
|
||||
"last_429_at": key.last_429_at_unix_secs.and_then(unix_secs_to_rfc3339),
|
||||
"last_429_type": key.last_429_type,
|
||||
"adjustment_count": adjustment_count,
|
||||
"recent_adjustments": recent_adjustments,
|
||||
"learning_confidence": status_snapshot.and_then(|value| value.get("learning_confidence")).cloned(),
|
||||
"enforcement_active": status_snapshot.and_then(|value| value.get("enforcement_active")).cloned(),
|
||||
"observation_count": status_snapshot
|
||||
.and_then(|value| value.get("observation_count"))
|
||||
.and_then(serde_json::Value::as_u64)
|
||||
.unwrap_or(0),
|
||||
"header_observation_count": status_snapshot
|
||||
.and_then(|value| value.get("header_observation_count"))
|
||||
.and_then(serde_json::Value::as_u64)
|
||||
.unwrap_or(0),
|
||||
"latest_upstream_limit": status_snapshot
|
||||
.and_then(|value| value.get("latest_upstream_limit"))
|
||||
.and_then(serde_json::Value::as_u64),
|
||||
}))
|
||||
.into_response(),
|
||||
state.build_admin_adaptive_stats_response(&key_id).await?,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -197,8 +88,8 @@ pub(super) async fn maybe_build_local_admin_adaptive_response(
|
||||
}
|
||||
|
||||
if decision.route_kind.as_deref() == Some("toggle_mode")
|
||||
&& request_context.request_method == http::Method::PATCH
|
||||
&& request_context.request_path.ends_with("/mode")
|
||||
&& request_context.method() == http::Method::PATCH
|
||||
&& request_context.path().ends_with("/mode")
|
||||
{
|
||||
let Some(request_body) = request_body else {
|
||||
return Ok(Some(build_proxy_error_response(
|
||||
@@ -208,71 +99,18 @@ pub(super) async fn maybe_build_local_admin_adaptive_response(
|
||||
None,
|
||||
)));
|
||||
};
|
||||
let body = match serde_json::from_slice::<AdminAdaptiveToggleModeRequest>(request_body) {
|
||||
Ok(payload) => payload,
|
||||
Err(_) => {
|
||||
return Ok(Some(build_proxy_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"invalid_request",
|
||||
"请求数据验证失败",
|
||||
None,
|
||||
)));
|
||||
}
|
||||
};
|
||||
let Some(mut key) = admin_adaptive_find_key(state, &key_id).await? else {
|
||||
return Ok(Some(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(Some(
|
||||
(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
Json(json!({
|
||||
"detail": "禁用自适应模式时必须提供 fixed_limit 参数",
|
||||
})),
|
||||
)
|
||||
.into_response(),
|
||||
));
|
||||
};
|
||||
if !(1..=100).contains(&fixed_limit) {
|
||||
return Ok(Some(
|
||||
(
|
||||
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) = state.update_provider_catalog_key(&key).await? else {
|
||||
return Ok(Some(admin_adaptive_key_not_found_response(&key_id)));
|
||||
};
|
||||
return Ok(Some(
|
||||
Json(json!({
|
||||
"message": message,
|
||||
"key_id": updated.id,
|
||||
"is_adaptive": updated.rpm_limit.is_none(),
|
||||
"rpm_limit": updated.rpm_limit,
|
||||
"effective_limit": admin_adaptive_effective_limit(&updated),
|
||||
}))
|
||||
.into_response(),
|
||||
state
|
||||
.toggle_admin_adaptive_mode_response(&key_id, request_body)
|
||||
.await?,
|
||||
));
|
||||
}
|
||||
|
||||
if decision.route_kind.as_deref() == Some("set_limit")
|
||||
&& request_context.request_method == http::Method::PATCH
|
||||
&& request_context.request_path.ends_with("/limit")
|
||||
&& request_context.method() == http::Method::PATCH
|
||||
&& request_context.path().ends_with("/limit")
|
||||
{
|
||||
let Some(limit_value) =
|
||||
query_param_value(request_context.request_query_string.as_deref(), "limit")
|
||||
else {
|
||||
let Some(limit_value) = query_param_value(request_context.query_string(), "limit") else {
|
||||
return Ok(Some(
|
||||
(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
@@ -300,50 +138,23 @@ pub(super) async fn maybe_build_local_admin_adaptive_response(
|
||||
));
|
||||
}
|
||||
|
||||
let Some(mut key) = admin_adaptive_find_key(state, &key_id).await? else {
|
||||
return Ok(Some(admin_adaptive_key_not_found_response(&key_id)));
|
||||
};
|
||||
let was_adaptive = key.rpm_limit.is_none();
|
||||
key.rpm_limit = Some(limit);
|
||||
let Some(updated) = state.update_provider_catalog_key(&key).await? else {
|
||||
return Ok(Some(admin_adaptive_key_not_found_response(&key_id)));
|
||||
};
|
||||
return Ok(Some(
|
||||
Json(json!({
|
||||
"message": format!("已设置为固定限制模式,RPM 限制为 {limit}"),
|
||||
"key_id": updated.id,
|
||||
"is_adaptive": false,
|
||||
"rpm_limit": updated.rpm_limit,
|
||||
"previous_mode": if was_adaptive { "adaptive" } else { "fixed" },
|
||||
}))
|
||||
.into_response(),
|
||||
state
|
||||
.set_admin_adaptive_limit_response(&key_id, limit)
|
||||
.await?,
|
||||
));
|
||||
}
|
||||
|
||||
if decision.route_kind.as_deref() == Some("reset_learning")
|
||||
&& request_context.request_method == http::Method::DELETE
|
||||
&& request_context.request_path.ends_with("/learning")
|
||||
&& request_context.method() == http::Method::DELETE
|
||||
&& request_context.path().ends_with("/learning")
|
||||
{
|
||||
let Some(mut key) = admin_adaptive_find_key(state, &key_id).await? else {
|
||||
return Ok(Some(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) = state.update_provider_catalog_key(&key).await? else {
|
||||
return Ok(Some(admin_adaptive_key_not_found_response(&key_id)));
|
||||
};
|
||||
return Ok(Some(
|
||||
Json(json!({
|
||||
"message": "学习状态已重置",
|
||||
"key_id": updated.id,
|
||||
}))
|
||||
.into_response(),
|
||||
state
|
||||
.reset_admin_adaptive_learning_response(&key_id)
|
||||
.await?,
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Some(admin_adaptive_dispatcher_not_found_response()))
|
||||
Ok(Some(state.admin_adaptive_dispatcher_not_found_response()))
|
||||
}
|
||||
|
||||
@@ -1,86 +1,36 @@
|
||||
use crate::handlers::admin::shared::json_string_list;
|
||||
use crate::{AppState, GatewayError};
|
||||
use crate::handlers::admin::request::AdminAppState;
|
||||
use crate::GatewayError;
|
||||
use aether_data_contracts::repository::provider_catalog::StoredProviderCatalogKey;
|
||||
use axum::{
|
||||
body::Body,
|
||||
http,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use serde_json::json;
|
||||
use axum::{body::Body, response::Response};
|
||||
|
||||
pub(super) fn admin_adaptive_effective_limit(key: &StoredProviderCatalogKey) -> Option<u32> {
|
||||
if key.rpm_limit.is_none() {
|
||||
key.learned_rpm_limit
|
||||
} else {
|
||||
key.rpm_limit
|
||||
}
|
||||
aether_admin::system::admin_adaptive_effective_limit(key)
|
||||
}
|
||||
|
||||
pub(super) fn admin_adaptive_adjustment_items(
|
||||
value: Option<&serde_json::Value>,
|
||||
) -> Vec<serde_json::Map<String, serde_json::Value>> {
|
||||
value
|
||||
.and_then(serde_json::Value::as_array)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter_map(serde_json::Value::as_object)
|
||||
.cloned()
|
||||
.collect()
|
||||
aether_admin::system::admin_adaptive_adjustment_items(value)
|
||||
}
|
||||
|
||||
pub(super) fn admin_adaptive_key_payload(key: &StoredProviderCatalogKey) -> serde_json::Value {
|
||||
json!({
|
||||
"id": key.id,
|
||||
"name": key.name,
|
||||
"provider_id": key.provider_id,
|
||||
"api_formats": json_string_list(key.api_formats.as_ref()),
|
||||
"is_adaptive": key.rpm_limit.is_none(),
|
||||
"rpm_limit": key.rpm_limit,
|
||||
"effective_limit": admin_adaptive_effective_limit(key),
|
||||
"learned_rpm_limit": key.learned_rpm_limit,
|
||||
"concurrent_429_count": key.concurrent_429_count.unwrap_or(0),
|
||||
"rpm_429_count": key.rpm_429_count.unwrap_or(0),
|
||||
})
|
||||
aether_admin::system::admin_adaptive_key_payload(key)
|
||||
}
|
||||
|
||||
pub(super) fn admin_adaptive_key_not_found_response(key_id: &str) -> Response<Body> {
|
||||
(
|
||||
http::StatusCode::NOT_FOUND,
|
||||
Json(json!({ "detail": format!("Key {key_id} 不存在") })),
|
||||
)
|
||||
.into_response()
|
||||
aether_admin::system::admin_adaptive_key_not_found_response(key_id)
|
||||
}
|
||||
|
||||
pub(super) fn admin_adaptive_dispatcher_not_found_response() -> Response<Body> {
|
||||
(
|
||||
http::StatusCode::NOT_FOUND,
|
||||
Json(json!({ "detail": "Adaptive route not found" })),
|
||||
)
|
||||
.into_response()
|
||||
aether_admin::system::admin_adaptive_dispatcher_not_found_response()
|
||||
}
|
||||
|
||||
pub(super) fn admin_adaptive_key_id_from_path(path: &str) -> Option<String> {
|
||||
let normalized = path.trim_end_matches('/');
|
||||
let mut segments = normalized.split('/').filter(|segment| !segment.is_empty());
|
||||
match (
|
||||
segments.next(),
|
||||
segments.next(),
|
||||
segments.next(),
|
||||
segments.next(),
|
||||
segments.next(),
|
||||
) {
|
||||
(Some("api"), Some("admin"), Some("adaptive"), Some("keys"), Some(key_id))
|
||||
if !key_id.is_empty() =>
|
||||
{
|
||||
Some(key_id.to_string())
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
aether_admin::system::admin_adaptive_key_id_from_path(path)
|
||||
}
|
||||
|
||||
pub(super) async fn admin_adaptive_find_key(
|
||||
state: &AppState,
|
||||
state: &AdminAppState<'_>,
|
||||
key_id: &str,
|
||||
) -> Result<Option<StoredProviderCatalogKey>, GatewayError> {
|
||||
Ok(state
|
||||
@@ -91,7 +41,7 @@ pub(super) async fn admin_adaptive_find_key(
|
||||
}
|
||||
|
||||
pub(super) async fn admin_adaptive_load_candidate_keys(
|
||||
state: &AppState,
|
||||
state: &AdminAppState<'_>,
|
||||
provider_id: Option<&str>,
|
||||
) -> Result<Vec<StoredProviderCatalogKey>, GatewayError> {
|
||||
if let Some(provider_id) = provider_id.filter(|value| !value.trim().is_empty()) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::{AppState, GatewayError};
|
||||
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
|
||||
use crate::GatewayError;
|
||||
use axum::{
|
||||
body::{Body, Bytes},
|
||||
http,
|
||||
@@ -39,12 +39,12 @@ const ADMIN_AWS_REGIONS: &[&str] = &[
|
||||
mod system_routes;
|
||||
|
||||
pub(crate) async fn maybe_build_local_admin_core_response(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
state: &AdminAppState<'_>,
|
||||
request_context: &AdminRequestContext<'_>,
|
||||
request_body: Option<&Bytes>,
|
||||
) -> Result<Option<Response<Body>>, GatewayError> {
|
||||
if let Some(response) =
|
||||
crate::handlers::admin::system::maybe_build_local_admin_management_tokens_response(
|
||||
super::management_tokens::maybe_build_local_admin_management_tokens_response(
|
||||
state,
|
||||
request_context,
|
||||
)
|
||||
@@ -61,13 +61,12 @@ pub(crate) async fn maybe_build_local_admin_core_response(
|
||||
{
|
||||
return Ok(Some(response));
|
||||
}
|
||||
if let Some(response) =
|
||||
crate::handlers::admin::system::maybe_build_local_admin_modules_response(
|
||||
state,
|
||||
request_context,
|
||||
request_body,
|
||||
)
|
||||
.await?
|
||||
if let Some(response) = super::modules::maybe_build_local_admin_modules_response(
|
||||
state,
|
||||
request_context,
|
||||
request_body,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
return Ok(Some(response));
|
||||
}
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
use super::ADMIN_AWS_REGIONS;
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
|
||||
use crate::handlers::admin::shared::attach_admin_audit_response;
|
||||
use crate::handlers::admin::shared::build_proxy_error_response;
|
||||
use crate::handlers::admin::system::shared::configs::{
|
||||
apply_admin_system_config_update, build_admin_system_config_detail_payload,
|
||||
build_admin_system_config_export_payload, build_admin_system_configs_payload,
|
||||
build_admin_system_users_export_payload, delete_admin_system_config,
|
||||
};
|
||||
use crate::handlers::admin::system::shared::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,
|
||||
build_admin_system_configs_payload, delete_admin_system_config,
|
||||
};
|
||||
use crate::handlers::admin::system::shared::paths::{
|
||||
admin_system_config_key_from_path, admin_system_email_template_preview_type_from_path,
|
||||
@@ -21,7 +16,7 @@ use crate::handlers::admin::system::shared::settings::{
|
||||
build_admin_system_check_update_payload, build_admin_system_settings_payload,
|
||||
build_admin_system_stats_payload, current_aether_version,
|
||||
};
|
||||
use crate::{AppState, GatewayError};
|
||||
use crate::GatewayError;
|
||||
use axum::{
|
||||
body::{Body, Bytes},
|
||||
http,
|
||||
@@ -31,20 +26,22 @@ use axum::{
|
||||
use serde_json::json;
|
||||
|
||||
pub(super) async fn maybe_build_local_admin_core_system_response(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
state: &AdminAppState<'_>,
|
||||
request_context: &AdminRequestContext<'_>,
|
||||
request_body: Option<&Bytes>,
|
||||
) -> Result<Option<Response<Body>>, GatewayError> {
|
||||
let Some(decision) = request_context.control_decision.as_ref() else {
|
||||
let Some(decision) = request_context.decision() else {
|
||||
return Ok(None);
|
||||
};
|
||||
let request_method = request_context.method();
|
||||
let request_path = request_context.path();
|
||||
if decision.route_family.as_deref() != Some("system_manage") {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
if decision.route_kind.as_deref() == Some("version")
|
||||
&& request_context.request_method == http::Method::GET
|
||||
&& request_context.request_path == "/api/admin/system/version"
|
||||
&& request_method == http::Method::GET
|
||||
&& request_path == "/api/admin/system/version"
|
||||
{
|
||||
return Ok(Some(
|
||||
Json(json!({ "version": current_aether_version() })).into_response(),
|
||||
@@ -52,8 +49,8 @@ pub(super) async fn maybe_build_local_admin_core_system_response(
|
||||
}
|
||||
|
||||
if decision.route_kind.as_deref() == Some("check_update")
|
||||
&& request_context.request_method == http::Method::GET
|
||||
&& request_context.request_path == "/api/admin/system/check-update"
|
||||
&& request_method == http::Method::GET
|
||||
&& request_path == "/api/admin/system/check-update"
|
||||
{
|
||||
return Ok(Some(
|
||||
Json(build_admin_system_check_update_payload()).into_response(),
|
||||
@@ -61,8 +58,8 @@ pub(super) async fn maybe_build_local_admin_core_system_response(
|
||||
}
|
||||
|
||||
if decision.route_kind.as_deref() == Some("aws_regions")
|
||||
&& request_context.request_method == http::Method::GET
|
||||
&& request_context.request_path == "/api/admin/system/aws-regions"
|
||||
&& request_method == http::Method::GET
|
||||
&& request_path == "/api/admin/system/aws-regions"
|
||||
{
|
||||
return Ok(Some(
|
||||
Json(json!({ "regions": ADMIN_AWS_REGIONS })).into_response(),
|
||||
@@ -70,8 +67,8 @@ pub(super) async fn maybe_build_local_admin_core_system_response(
|
||||
}
|
||||
|
||||
if decision.route_kind.as_deref() == Some("stats")
|
||||
&& request_context.request_method == http::Method::GET
|
||||
&& request_context.request_path == "/api/admin/system/stats"
|
||||
&& request_method == http::Method::GET
|
||||
&& request_path == "/api/admin/system/stats"
|
||||
{
|
||||
return Ok(Some(
|
||||
Json(build_admin_system_stats_payload(state).await?).into_response(),
|
||||
@@ -79,8 +76,8 @@ pub(super) async fn maybe_build_local_admin_core_system_response(
|
||||
}
|
||||
|
||||
if decision.route_kind.as_deref() == Some("settings_get")
|
||||
&& request_context.request_method == http::Method::GET
|
||||
&& request_context.request_path == "/api/admin/system/settings"
|
||||
&& request_method == http::Method::GET
|
||||
&& request_path == "/api/admin/system/settings"
|
||||
{
|
||||
return Ok(Some(
|
||||
Json(build_admin_system_settings_payload(state).await?).into_response(),
|
||||
@@ -88,11 +85,11 @@ pub(super) async fn maybe_build_local_admin_core_system_response(
|
||||
}
|
||||
|
||||
if decision.route_kind.as_deref() == Some("config_export")
|
||||
&& request_context.request_method == http::Method::GET
|
||||
&& request_context.request_path == "/api/admin/system/config/export"
|
||||
&& request_method == http::Method::GET
|
||||
&& request_path == "/api/admin/system/config/export"
|
||||
{
|
||||
return Ok(Some(attach_admin_audit_response(
|
||||
Json(build_admin_system_config_export_payload(state).await?).into_response(),
|
||||
Json(state.build_admin_system_config_export_payload().await?).into_response(),
|
||||
"admin_system_config_exported",
|
||||
"export_system_config",
|
||||
"system_config_export",
|
||||
@@ -101,11 +98,11 @@ pub(super) async fn maybe_build_local_admin_core_system_response(
|
||||
}
|
||||
|
||||
if decision.route_kind.as_deref() == Some("users_export")
|
||||
&& request_context.request_method == http::Method::GET
|
||||
&& request_context.request_path == "/api/admin/system/users/export"
|
||||
&& request_method == http::Method::GET
|
||||
&& request_path == "/api/admin/system/users/export"
|
||||
{
|
||||
return Ok(Some(attach_admin_audit_response(
|
||||
Json(build_admin_system_users_export_payload(state).await?).into_response(),
|
||||
Json(state.build_admin_system_users_export_payload().await?).into_response(),
|
||||
"admin_system_users_exported",
|
||||
"export_system_users",
|
||||
"user_export",
|
||||
@@ -127,7 +124,7 @@ pub(super) async fn maybe_build_local_admin_core_system_response(
|
||||
| "purge_request_bodies"
|
||||
| "purge_stats"
|
||||
)
|
||||
) && request_context.request_method == http::Method::POST
|
||||
) && request_method == http::Method::POST
|
||||
{
|
||||
return Ok(Some(
|
||||
(
|
||||
@@ -139,8 +136,8 @@ pub(super) async fn maybe_build_local_admin_core_system_response(
|
||||
}
|
||||
|
||||
if decision.route_kind.as_deref() == Some("settings_set")
|
||||
&& request_context.request_method == http::Method::PUT
|
||||
&& request_context.request_path == "/api/admin/system/settings"
|
||||
&& request_method == http::Method::PUT
|
||||
&& request_path == "/api/admin/system/settings"
|
||||
{
|
||||
let Some(request_body) = request_body else {
|
||||
return Ok(Some(
|
||||
@@ -166,8 +163,8 @@ pub(super) async fn maybe_build_local_admin_core_system_response(
|
||||
}
|
||||
|
||||
if decision.route_kind.as_deref() == Some("configs_list")
|
||||
&& request_context.request_method == http::Method::GET
|
||||
&& is_admin_system_configs_root(&request_context.request_path)
|
||||
&& request_method == http::Method::GET
|
||||
&& is_admin_system_configs_root(request_path)
|
||||
{
|
||||
let entries = state.list_system_config_entries().await?;
|
||||
return Ok(Some(
|
||||
@@ -175,11 +172,8 @@ pub(super) async fn maybe_build_local_admin_core_system_response(
|
||||
));
|
||||
}
|
||||
|
||||
if decision.route_kind.as_deref() == Some("config_get")
|
||||
&& request_context.request_method == http::Method::GET
|
||||
{
|
||||
let Some(config_key) = admin_system_config_key_from_path(&request_context.request_path)
|
||||
else {
|
||||
if decision.route_kind.as_deref() == Some("config_get") && request_method == http::Method::GET {
|
||||
let Some(config_key) = admin_system_config_key_from_path(request_path) else {
|
||||
return Ok(Some(build_proxy_error_response(
|
||||
http::StatusCode::NOT_FOUND,
|
||||
"not_found",
|
||||
@@ -195,11 +189,8 @@ pub(super) async fn maybe_build_local_admin_core_system_response(
|
||||
));
|
||||
}
|
||||
|
||||
if decision.route_kind.as_deref() == Some("config_set")
|
||||
&& request_context.request_method == http::Method::PUT
|
||||
{
|
||||
let Some(config_key) = admin_system_config_key_from_path(&request_context.request_path)
|
||||
else {
|
||||
if decision.route_kind.as_deref() == Some("config_set") && request_method == http::Method::PUT {
|
||||
let Some(config_key) = admin_system_config_key_from_path(request_path) else {
|
||||
return Ok(Some(build_proxy_error_response(
|
||||
http::StatusCode::NOT_FOUND,
|
||||
"not_found",
|
||||
@@ -230,10 +221,9 @@ pub(super) async fn maybe_build_local_admin_core_system_response(
|
||||
}
|
||||
|
||||
if decision.route_kind.as_deref() == Some("config_delete")
|
||||
&& request_context.request_method == http::Method::DELETE
|
||||
&& request_method == http::Method::DELETE
|
||||
{
|
||||
let Some(config_key) = admin_system_config_key_from_path(&request_context.request_path)
|
||||
else {
|
||||
let Some(config_key) = admin_system_config_key_from_path(request_path) else {
|
||||
return Ok(Some(build_proxy_error_response(
|
||||
http::StatusCode::NOT_FOUND,
|
||||
"not_found",
|
||||
@@ -256,8 +246,8 @@ pub(super) async fn maybe_build_local_admin_core_system_response(
|
||||
}
|
||||
|
||||
if decision.route_kind.as_deref() == Some("api_formats")
|
||||
&& request_context.request_method == http::Method::GET
|
||||
&& request_context.request_path == "/api/admin/system/api-formats"
|
||||
&& request_method == http::Method::GET
|
||||
&& request_path == "/api/admin/system/api-formats"
|
||||
{
|
||||
return Ok(Some(
|
||||
Json(build_admin_api_formats_payload()).into_response(),
|
||||
@@ -265,20 +255,18 @@ pub(super) async fn maybe_build_local_admin_core_system_response(
|
||||
}
|
||||
|
||||
if decision.route_kind.as_deref() == Some("email_templates_list")
|
||||
&& request_context.request_method == http::Method::GET
|
||||
&& is_admin_system_email_templates_root(&request_context.request_path)
|
||||
&& request_method == http::Method::GET
|
||||
&& is_admin_system_email_templates_root(request_path)
|
||||
{
|
||||
return Ok(Some(
|
||||
Json(build_admin_email_templates_payload(state).await?).into_response(),
|
||||
Json(state.build_admin_email_templates_payload().await?).into_response(),
|
||||
));
|
||||
}
|
||||
|
||||
if decision.route_kind.as_deref() == Some("email_template_get")
|
||||
&& request_context.request_method == http::Method::GET
|
||||
&& request_method == http::Method::GET
|
||||
{
|
||||
let Some(template_type) =
|
||||
admin_system_email_template_type_from_path(&request_context.request_path)
|
||||
else {
|
||||
let Some(template_type) = admin_system_email_template_type_from_path(request_path) else {
|
||||
return Ok(Some(build_proxy_error_response(
|
||||
http::StatusCode::NOT_FOUND,
|
||||
"not_found",
|
||||
@@ -287,7 +275,10 @@ pub(super) async fn maybe_build_local_admin_core_system_response(
|
||||
)));
|
||||
};
|
||||
return Ok(Some(
|
||||
match build_admin_email_template_payload(state, &template_type).await? {
|
||||
match state
|
||||
.build_admin_email_template_payload(&template_type)
|
||||
.await?
|
||||
{
|
||||
Ok(payload) => Json(payload).into_response(),
|
||||
Err((status, payload)) => (status, Json(payload)).into_response(),
|
||||
},
|
||||
@@ -295,11 +286,9 @@ pub(super) async fn maybe_build_local_admin_core_system_response(
|
||||
}
|
||||
|
||||
if decision.route_kind.as_deref() == Some("email_template_set")
|
||||
&& request_context.request_method == http::Method::PUT
|
||||
&& request_method == http::Method::PUT
|
||||
{
|
||||
let Some(template_type) =
|
||||
admin_system_email_template_type_from_path(&request_context.request_path)
|
||||
else {
|
||||
let Some(template_type) = admin_system_email_template_type_from_path(request_path) else {
|
||||
return Ok(Some(build_proxy_error_response(
|
||||
http::StatusCode::NOT_FOUND,
|
||||
"not_found",
|
||||
@@ -316,7 +305,10 @@ pub(super) async fn maybe_build_local_admin_core_system_response(
|
||||
)));
|
||||
};
|
||||
return Ok(Some(
|
||||
match apply_admin_email_template_update(state, &template_type, request_body).await? {
|
||||
match state
|
||||
.apply_admin_email_template_update(&template_type, request_body)
|
||||
.await?
|
||||
{
|
||||
Ok(payload) => Json(payload).into_response(),
|
||||
Err((status, payload)) => (status, Json(payload)).into_response(),
|
||||
},
|
||||
@@ -324,10 +316,9 @@ pub(super) async fn maybe_build_local_admin_core_system_response(
|
||||
}
|
||||
|
||||
if decision.route_kind.as_deref() == Some("email_template_preview")
|
||||
&& request_context.request_method == http::Method::POST
|
||||
&& request_method == http::Method::POST
|
||||
{
|
||||
let Some(template_type) =
|
||||
admin_system_email_template_preview_type_from_path(&request_context.request_path)
|
||||
let Some(template_type) = admin_system_email_template_preview_type_from_path(request_path)
|
||||
else {
|
||||
return Ok(Some(build_proxy_error_response(
|
||||
http::StatusCode::NOT_FOUND,
|
||||
@@ -337,7 +328,10 @@ pub(super) async fn maybe_build_local_admin_core_system_response(
|
||||
)));
|
||||
};
|
||||
return Ok(Some(
|
||||
match preview_admin_email_template(state, &template_type, request_body).await? {
|
||||
match state
|
||||
.preview_admin_email_template(&template_type, request_body)
|
||||
.await?
|
||||
{
|
||||
Ok(payload) => Json(payload).into_response(),
|
||||
Err((status, payload)) => (status, Json(payload)).into_response(),
|
||||
},
|
||||
@@ -345,10 +339,9 @@ pub(super) async fn maybe_build_local_admin_core_system_response(
|
||||
}
|
||||
|
||||
if decision.route_kind.as_deref() == Some("email_template_reset")
|
||||
&& request_context.request_method == http::Method::POST
|
||||
&& request_method == http::Method::POST
|
||||
{
|
||||
let Some(template_type) =
|
||||
admin_system_email_template_reset_type_from_path(&request_context.request_path)
|
||||
let Some(template_type) = admin_system_email_template_reset_type_from_path(request_path)
|
||||
else {
|
||||
return Ok(Some(build_proxy_error_response(
|
||||
http::StatusCode::NOT_FOUND,
|
||||
@@ -358,7 +351,7 @@ pub(super) async fn maybe_build_local_admin_core_system_response(
|
||||
)));
|
||||
};
|
||||
return Ok(Some(
|
||||
match reset_admin_email_template(state, &template_type).await? {
|
||||
match state.reset_admin_email_template(&template_type).await? {
|
||||
Ok(payload) => Json(payload).into_response(),
|
||||
Err((status, payload)) => (status, Json(payload)).into_response(),
|
||||
},
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
|
||||
use crate::handlers::admin::shared::{query_param_optional_bool, query_param_value};
|
||||
use crate::handlers::admin::system::shared::paths::{
|
||||
admin_management_token_id_from_path, admin_management_token_status_id_from_path,
|
||||
is_admin_management_tokens_root,
|
||||
};
|
||||
use crate::handlers::internal::build_management_token_payload;
|
||||
use crate::{AppState, GatewayError};
|
||||
use crate::GatewayError;
|
||||
use aether_data::repository::management_tokens::ManagementTokenListQuery;
|
||||
use axum::{
|
||||
body::Body,
|
||||
@@ -16,10 +16,10 @@ use axum::{
|
||||
use serde_json::json;
|
||||
|
||||
pub(crate) async fn maybe_build_local_admin_management_tokens_response(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
state: &AdminAppState<'_>,
|
||||
request_context: &AdminRequestContext<'_>,
|
||||
) -> Result<Option<Response<Body>>, GatewayError> {
|
||||
let Some(decision) = request_context.control_decision.as_ref() else {
|
||||
let Some(decision) = request_context.decision() else {
|
||||
return Ok(None);
|
||||
};
|
||||
if decision.route_family.as_deref() != Some("management_tokens_manage") {
|
||||
@@ -44,19 +44,18 @@ pub(crate) async fn maybe_build_local_admin_management_tokens_response(
|
||||
}
|
||||
|
||||
if decision.route_kind.as_deref() == Some("list_tokens")
|
||||
&& request_context.request_method == http::Method::GET
|
||||
&& is_admin_management_tokens_root(&request_context.request_path)
|
||||
&& request_context.method() == http::Method::GET
|
||||
&& is_admin_management_tokens_root(request_context.path())
|
||||
{
|
||||
if !state.has_management_token_reader() {
|
||||
return Ok(None);
|
||||
}
|
||||
let user_id = query_param_value(request_context.request_query_string.as_deref(), "user_id");
|
||||
let is_active =
|
||||
query_param_optional_bool(request_context.request_query_string.as_deref(), "is_active");
|
||||
let skip = query_param_value(request_context.request_query_string.as_deref(), "skip")
|
||||
let user_id = query_param_value(request_context.query_string(), "user_id");
|
||||
let is_active = query_param_optional_bool(request_context.query_string(), "is_active");
|
||||
let skip = query_param_value(request_context.query_string(), "skip")
|
||||
.and_then(|value| value.parse::<usize>().ok())
|
||||
.unwrap_or(0);
|
||||
let limit = query_param_value(request_context.request_query_string.as_deref(), "limit")
|
||||
let limit = query_param_value(request_context.query_string(), "limit")
|
||||
.and_then(|value| value.parse::<usize>().ok())
|
||||
.filter(|value| *value > 0 && *value <= 100)
|
||||
.unwrap_or(50);
|
||||
@@ -85,13 +84,12 @@ pub(crate) async fn maybe_build_local_admin_management_tokens_response(
|
||||
}
|
||||
|
||||
if decision.route_kind.as_deref() == Some("get_token")
|
||||
&& request_context.request_method == http::Method::GET
|
||||
&& request_context.method() == http::Method::GET
|
||||
{
|
||||
if !state.has_management_token_reader() {
|
||||
return Ok(None);
|
||||
}
|
||||
let Some(token_id) = admin_management_token_id_from_path(&request_context.request_path)
|
||||
else {
|
||||
let Some(token_id) = admin_management_token_id_from_path(request_context.path()) else {
|
||||
return Ok(Some(
|
||||
(
|
||||
http::StatusCode::NOT_FOUND,
|
||||
@@ -117,13 +115,12 @@ pub(crate) async fn maybe_build_local_admin_management_tokens_response(
|
||||
}
|
||||
|
||||
if decision.route_kind.as_deref() == Some("delete_token")
|
||||
&& request_context.request_method == http::Method::DELETE
|
||||
&& request_context.method() == http::Method::DELETE
|
||||
{
|
||||
if !state.has_management_token_writer() {
|
||||
return Ok(None);
|
||||
}
|
||||
let Some(token_id) = admin_management_token_id_from_path(&request_context.request_path)
|
||||
else {
|
||||
let Some(token_id) = admin_management_token_id_from_path(request_context.path()) else {
|
||||
return Ok(Some(
|
||||
(
|
||||
http::StatusCode::NOT_FOUND,
|
||||
@@ -157,13 +154,12 @@ pub(crate) async fn maybe_build_local_admin_management_tokens_response(
|
||||
}
|
||||
|
||||
if decision.route_kind.as_deref() == Some("toggle_status")
|
||||
&& request_context.request_method == http::Method::PATCH
|
||||
&& request_context.method() == http::Method::PATCH
|
||||
{
|
||||
if !state.has_management_token_writer() {
|
||||
return Ok(None);
|
||||
}
|
||||
let Some(token_id) =
|
||||
admin_management_token_status_id_from_path(&request_context.request_path)
|
||||
let Some(token_id) = admin_management_token_status_id_from_path(request_context.path())
|
||||
else {
|
||||
return Ok(Some(
|
||||
(
|
||||
|
||||
@@ -3,11 +3,7 @@ mod core;
|
||||
mod management_tokens;
|
||||
mod modules;
|
||||
mod proxy_nodes;
|
||||
pub(crate) mod shared;
|
||||
mod routes;
|
||||
pub(super) mod shared;
|
||||
|
||||
pub(crate) use self::adaptive::maybe_build_local_admin_adaptive_response;
|
||||
pub(crate) use self::core::maybe_build_local_admin_core_response;
|
||||
pub(crate) use self::management_tokens::maybe_build_local_admin_management_tokens_response;
|
||||
pub(crate) use self::modules::maybe_build_local_admin_modules_response;
|
||||
pub(crate) use self::proxy_nodes::maybe_build_local_admin_proxy_nodes_response;
|
||||
pub(crate) use crate::handlers::admin::provider::pool_admin::maybe_build_local_admin_pool_response;
|
||||
pub(super) use self::routes::maybe_build_local_admin_system_response;
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
|
||||
use crate::handlers::admin::system::shared::modules::{
|
||||
admin_module_by_name, admin_module_name_from_enabled_path, admin_module_name_from_status_path,
|
||||
build_admin_module_runtime_state, build_admin_module_status_payload,
|
||||
build_admin_module_validation_result, build_admin_modules_status_payload,
|
||||
AdminSetModuleEnabledRequest,
|
||||
admin_module_name_from_enabled_path, admin_module_name_from_status_path,
|
||||
};
|
||||
use crate::handlers::admin::system::shared::paths::{
|
||||
is_admin_system_configs_root, is_admin_system_email_templates_root,
|
||||
};
|
||||
use crate::handlers::shared::module_available_from_env;
|
||||
use crate::{AppState, GatewayError};
|
||||
use crate::GatewayError;
|
||||
use axum::{
|
||||
body::{Body, Bytes},
|
||||
http,
|
||||
@@ -19,11 +12,11 @@ use axum::{
|
||||
use serde_json::json;
|
||||
|
||||
pub(crate) async fn maybe_build_local_admin_modules_response(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
state: &AdminAppState<'_>,
|
||||
request_context: &AdminRequestContext<'_>,
|
||||
request_body: Option<&Bytes>,
|
||||
) -> Result<Option<Response<Body>>, GatewayError> {
|
||||
let Some(decision) = request_context.control_decision.as_ref() else {
|
||||
let Some(decision) = request_context.decision() else {
|
||||
return Ok(None);
|
||||
};
|
||||
if decision.route_family.as_deref() != Some("modules_manage") {
|
||||
@@ -31,21 +24,21 @@ pub(crate) async fn maybe_build_local_admin_modules_response(
|
||||
}
|
||||
|
||||
if decision.route_kind.as_deref() == Some("status_list")
|
||||
&& request_context.request_method == http::Method::GET
|
||||
&& request_context.request_path == "/api/admin/modules/status"
|
||||
&& request_context.method() == http::Method::GET
|
||||
&& request_context.path() == "/api/admin/modules/status"
|
||||
{
|
||||
let payload = build_admin_modules_status_payload(state).await?;
|
||||
return Ok(Some(Json(payload).into_response()));
|
||||
return Ok(Some(
|
||||
Json(state.build_admin_modules_status_payload().await?).into_response(),
|
||||
));
|
||||
}
|
||||
|
||||
if decision.route_kind.as_deref() == Some("status_detail")
|
||||
&& request_context.request_method == http::Method::GET
|
||||
&& request_context.method() == http::Method::GET
|
||||
&& request_context
|
||||
.request_path
|
||||
.path()
|
||||
.starts_with("/api/admin/modules/status/")
|
||||
{
|
||||
let Some(module_name) = admin_module_name_from_status_path(&request_context.request_path)
|
||||
else {
|
||||
let Some(module_name) = admin_module_name_from_status_path(request_context.path()) else {
|
||||
return Ok(Some(
|
||||
(
|
||||
http::StatusCode::NOT_FOUND,
|
||||
@@ -54,29 +47,25 @@ pub(crate) async fn maybe_build_local_admin_modules_response(
|
||||
.into_response(),
|
||||
));
|
||||
};
|
||||
let Some(module) = admin_module_by_name(&module_name) else {
|
||||
return Ok(Some(
|
||||
(
|
||||
http::StatusCode::NOT_FOUND,
|
||||
Json(json!({ "detail": format!("模块 '{module_name}' 不存在") })),
|
||||
)
|
||||
.into_response(),
|
||||
));
|
||||
};
|
||||
let runtime = build_admin_module_runtime_state(state).await?;
|
||||
let payload = build_admin_module_status_payload(state, module, &runtime).await?;
|
||||
return Ok(Some(Json(payload).into_response()));
|
||||
return Ok(Some(
|
||||
match state
|
||||
.build_admin_module_status_payload(&module_name)
|
||||
.await?
|
||||
{
|
||||
Ok(payload) => Json(payload).into_response(),
|
||||
Err((status, payload)) => (status, Json(payload)).into_response(),
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
if decision.route_kind.as_deref() == Some("set_enabled")
|
||||
&& request_context.request_method == http::Method::PUT
|
||||
&& request_context.method() == http::Method::PUT
|
||||
&& request_context
|
||||
.request_path
|
||||
.path()
|
||||
.starts_with("/api/admin/modules/status/")
|
||||
&& request_context.request_path.ends_with("/enabled")
|
||||
&& request_context.path().ends_with("/enabled")
|
||||
{
|
||||
let Some(module_name) = admin_module_name_from_enabled_path(&request_context.request_path)
|
||||
else {
|
||||
let Some(module_name) = admin_module_name_from_enabled_path(request_context.path()) else {
|
||||
return Ok(Some(
|
||||
(
|
||||
http::StatusCode::NOT_FOUND,
|
||||
@@ -85,30 +74,6 @@ pub(crate) async fn maybe_build_local_admin_modules_response(
|
||||
.into_response(),
|
||||
));
|
||||
};
|
||||
let Some(module) = admin_module_by_name(&module_name) else {
|
||||
return Ok(Some(
|
||||
(
|
||||
http::StatusCode::NOT_FOUND,
|
||||
Json(json!({ "detail": format!("模块 '{module_name}' 不存在") })),
|
||||
)
|
||||
.into_response(),
|
||||
));
|
||||
};
|
||||
let available = module_available_from_env(module.env_key, module.default_available);
|
||||
if !available {
|
||||
return Ok(Some(
|
||||
(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
Json(json!({
|
||||
"detail": format!(
|
||||
"模块 '{}' 不可用,无法启用。请检查环境变量 {} 和依赖库。",
|
||||
module.name, module.env_key
|
||||
)
|
||||
})),
|
||||
)
|
||||
.into_response(),
|
||||
));
|
||||
}
|
||||
let Some(request_body) = request_body else {
|
||||
return Ok(Some(
|
||||
(
|
||||
@@ -118,47 +83,15 @@ pub(crate) async fn maybe_build_local_admin_modules_response(
|
||||
.into_response(),
|
||||
));
|
||||
};
|
||||
let payload = match serde_json::from_slice::<AdminSetModuleEnabledRequest>(request_body) {
|
||||
Ok(payload) => payload,
|
||||
Err(_) => {
|
||||
return Ok(Some(
|
||||
(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
Json(json!({ "detail": "请求体格式错误,需要 enabled 字段" })),
|
||||
)
|
||||
.into_response(),
|
||||
));
|
||||
}
|
||||
};
|
||||
let runtime = build_admin_module_runtime_state(state).await?;
|
||||
if payload.enabled {
|
||||
let (config_validated, config_error) =
|
||||
build_admin_module_validation_result(module, &runtime);
|
||||
if !config_validated {
|
||||
return Ok(Some(
|
||||
(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
Json(json!({
|
||||
"detail": format!(
|
||||
"模块配置未验证通过: {}",
|
||||
config_error.unwrap_or_else(|| "未知错误".to_string())
|
||||
)
|
||||
})),
|
||||
)
|
||||
.into_response(),
|
||||
));
|
||||
}
|
||||
}
|
||||
let _ = state
|
||||
.upsert_system_config_json_value(
|
||||
&format!("module.{}.enabled", module.name),
|
||||
&json!(payload.enabled),
|
||||
Some(&format!("模块 [{}] 启用状态", module.display_name)),
|
||||
)
|
||||
.await?;
|
||||
let updated_runtime = build_admin_module_runtime_state(state).await?;
|
||||
let payload = build_admin_module_status_payload(state, module, &updated_runtime).await?;
|
||||
return Ok(Some(Json(payload).into_response()));
|
||||
return Ok(Some(
|
||||
match state
|
||||
.set_admin_module_enabled_payload(&module_name, request_body)
|
||||
.await?
|
||||
{
|
||||
Ok(payload) => Json(payload).into_response(),
|
||||
Err((status, payload)) => (status, Json(payload)).into_response(),
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
|
||||
@@ -1,132 +1,17 @@
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::handlers::admin::shared::{query_param_value, unix_secs_to_rfc3339};
|
||||
use crate::{AppState, GatewayError};
|
||||
use aether_data::repository::proxy_nodes::{StoredProxyNode, StoredProxyNodeEvent};
|
||||
use axum::{
|
||||
body::Body,
|
||||
http,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
|
||||
use crate::handlers::admin::shared::query_param_value;
|
||||
use crate::GatewayError;
|
||||
use aether_admin::system::{
|
||||
admin_proxy_node_event_node_id_from_path, build_admin_proxy_nodes_data_unavailable_response,
|
||||
build_admin_proxy_nodes_not_found_response,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
const ADMIN_PROXY_NODES_DATA_UNAVAILABLE_DETAIL: &str = "Admin proxy nodes data unavailable";
|
||||
|
||||
fn build_admin_proxy_nodes_data_unavailable_response() -> Response<Body> {
|
||||
(
|
||||
http::StatusCode::SERVICE_UNAVAILABLE,
|
||||
Json(json!({ "detail": ADMIN_PROXY_NODES_DATA_UNAVAILABLE_DETAIL })),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
|
||||
fn mask_admin_proxy_node_password(password: Option<&str>) -> Option<String> {
|
||||
let password = password?;
|
||||
if password.is_empty() {
|
||||
return None;
|
||||
}
|
||||
if password.len() < 8 {
|
||||
return Some("****".to_string());
|
||||
}
|
||||
Some(format!(
|
||||
"{}****{}",
|
||||
&password[..2],
|
||||
&password[password.len() - 2..]
|
||||
))
|
||||
}
|
||||
|
||||
fn build_admin_proxy_node_payload(node: &StoredProxyNode) -> serde_json::Value {
|
||||
let mut payload = serde_json::Map::from_iter([
|
||||
("id".to_string(), json!(node.id)),
|
||||
("name".to_string(), json!(node.name)),
|
||||
("ip".to_string(), json!(node.ip)),
|
||||
("port".to_string(), json!(node.port)),
|
||||
("region".to_string(), json!(node.region)),
|
||||
("status".to_string(), json!(node.status)),
|
||||
("is_manual".to_string(), json!(node.is_manual)),
|
||||
("tunnel_mode".to_string(), json!(node.tunnel_mode)),
|
||||
("tunnel_connected".to_string(), json!(node.tunnel_connected)),
|
||||
(
|
||||
"tunnel_connected_at".to_string(),
|
||||
json!(node
|
||||
.tunnel_connected_at_unix_secs
|
||||
.and_then(unix_secs_to_rfc3339)),
|
||||
),
|
||||
("registered_by".to_string(), json!(node.registered_by)),
|
||||
(
|
||||
"last_heartbeat_at".to_string(),
|
||||
json!(node
|
||||
.last_heartbeat_at_unix_secs
|
||||
.and_then(unix_secs_to_rfc3339)),
|
||||
),
|
||||
(
|
||||
"heartbeat_interval".to_string(),
|
||||
json!(node.heartbeat_interval),
|
||||
),
|
||||
(
|
||||
"active_connections".to_string(),
|
||||
json!(node.active_connections),
|
||||
),
|
||||
("total_requests".to_string(), json!(node.total_requests)),
|
||||
("avg_latency_ms".to_string(), json!(node.avg_latency_ms)),
|
||||
("failed_requests".to_string(), json!(node.failed_requests)),
|
||||
("dns_failures".to_string(), json!(node.dns_failures)),
|
||||
("stream_errors".to_string(), json!(node.stream_errors)),
|
||||
("proxy_metadata".to_string(), json!(node.proxy_metadata)),
|
||||
("hardware_info".to_string(), json!(node.hardware_info)),
|
||||
(
|
||||
"estimated_max_concurrency".to_string(),
|
||||
json!(node.estimated_max_concurrency),
|
||||
),
|
||||
("remote_config".to_string(), json!(node.remote_config)),
|
||||
("config_version".to_string(), json!(node.config_version)),
|
||||
(
|
||||
"created_at".to_string(),
|
||||
json!(node.created_at_unix_secs.and_then(unix_secs_to_rfc3339)),
|
||||
),
|
||||
(
|
||||
"updated_at".to_string(),
|
||||
json!(node.updated_at_unix_secs.and_then(unix_secs_to_rfc3339)),
|
||||
),
|
||||
]);
|
||||
|
||||
if node.is_manual {
|
||||
payload.insert("proxy_url".to_string(), json!(node.proxy_url));
|
||||
payload.insert("proxy_username".to_string(), json!(node.proxy_username));
|
||||
payload.insert(
|
||||
"proxy_password".to_string(),
|
||||
json!(mask_admin_proxy_node_password(
|
||||
node.proxy_password.as_deref()
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
serde_json::Value::Object(payload)
|
||||
}
|
||||
|
||||
fn build_admin_proxy_node_event_payload(event: &StoredProxyNodeEvent) -> serde_json::Value {
|
||||
json!({
|
||||
"id": event.id,
|
||||
"event_type": event.event_type,
|
||||
"detail": event.detail,
|
||||
"created_at": event.created_at_unix_secs.and_then(unix_secs_to_rfc3339),
|
||||
})
|
||||
}
|
||||
|
||||
fn admin_proxy_node_event_node_id_from_path(request_path: &str) -> Option<&str> {
|
||||
let node_id = request_path.strip_prefix("/api/admin/proxy-nodes/")?;
|
||||
let node_id = node_id.strip_suffix("/events")?;
|
||||
if node_id.is_empty() || node_id.contains('/') {
|
||||
return None;
|
||||
}
|
||||
Some(node_id)
|
||||
}
|
||||
use axum::{body::Body, http, response::Response};
|
||||
|
||||
pub(crate) async fn maybe_build_local_admin_proxy_nodes_response(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
state: &AdminAppState<'_>,
|
||||
request_context: &AdminRequestContext<'_>,
|
||||
) -> Result<Option<Response<Body>>, GatewayError> {
|
||||
let Some(decision) = request_context.control_decision.as_ref() else {
|
||||
let Some(decision) = request_context.decision() else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
@@ -135,112 +20,45 @@ pub(crate) async fn maybe_build_local_admin_proxy_nodes_response(
|
||||
}
|
||||
|
||||
if decision.route_kind.as_deref() == Some("list_nodes")
|
||||
&& request_context.request_method == http::Method::GET
|
||||
&& request_context.method() == http::Method::GET
|
||||
&& matches!(
|
||||
request_context.request_path.as_str(),
|
||||
request_context.path(),
|
||||
"/api/admin/proxy-nodes" | "/api/admin/proxy-nodes/"
|
||||
)
|
||||
{
|
||||
if !state.data.has_proxy_node_reader() {
|
||||
return Ok(Some(build_admin_proxy_nodes_data_unavailable_response()));
|
||||
}
|
||||
|
||||
let skip = query_param_value(request_context.request_query_string.as_deref(), "skip")
|
||||
let skip = query_param_value(request_context.query_string(), "skip")
|
||||
.and_then(|value| value.parse::<usize>().ok())
|
||||
.unwrap_or(0);
|
||||
let limit = query_param_value(request_context.request_query_string.as_deref(), "limit")
|
||||
let limit = query_param_value(request_context.query_string(), "limit")
|
||||
.and_then(|value| value.parse::<usize>().ok())
|
||||
.filter(|value| *value > 0 && *value <= 1000)
|
||||
.unwrap_or(100);
|
||||
let status = query_param_value(request_context.request_query_string.as_deref(), "status")
|
||||
let status = query_param_value(request_context.query_string(), "status")
|
||||
.map(|value| value.trim().to_ascii_lowercase())
|
||||
.filter(|value| !value.is_empty());
|
||||
|
||||
if let Some(status) = status.as_deref() {
|
||||
if !matches!(status, "offline" | "online") {
|
||||
return Ok(Some(
|
||||
(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
Json(json!({
|
||||
"detail": "status 必须是以下之一: ['offline', 'online']"
|
||||
})),
|
||||
)
|
||||
.into_response(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let mut nodes = state.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<_>>();
|
||||
|
||||
return Ok(Some(
|
||||
Json(json!({
|
||||
"items": items,
|
||||
"total": total,
|
||||
"skip": skip,
|
||||
"limit": limit,
|
||||
}))
|
||||
.into_response(),
|
||||
state
|
||||
.build_admin_proxy_nodes_list_response(skip, limit, status)
|
||||
.await?,
|
||||
));
|
||||
}
|
||||
|
||||
if decision.route_kind.as_deref() == Some("list_node_events")
|
||||
&& request_context.request_method == http::Method::GET
|
||||
&& request_context.method() == http::Method::GET
|
||||
{
|
||||
if !state.data.has_proxy_node_reader() {
|
||||
return Ok(Some(build_admin_proxy_nodes_data_unavailable_response()));
|
||||
}
|
||||
|
||||
let Some(node_id) = admin_proxy_node_event_node_id_from_path(&request_context.request_path)
|
||||
else {
|
||||
return Ok(Some(
|
||||
(
|
||||
http::StatusCode::NOT_FOUND,
|
||||
Json(json!({ "detail": "Proxy node 不存在" })),
|
||||
)
|
||||
.into_response(),
|
||||
));
|
||||
let Some(node_id) = admin_proxy_node_event_node_id_from_path(request_context.path()) else {
|
||||
return Ok(Some(build_admin_proxy_nodes_not_found_response()));
|
||||
};
|
||||
|
||||
if state.find_proxy_node(node_id).await?.is_none() {
|
||||
return Ok(Some(
|
||||
(
|
||||
http::StatusCode::NOT_FOUND,
|
||||
Json(json!({ "detail": "Proxy node 不存在" })),
|
||||
)
|
||||
.into_response(),
|
||||
));
|
||||
}
|
||||
|
||||
let limit = query_param_value(request_context.request_query_string.as_deref(), "limit")
|
||||
let limit = query_param_value(request_context.query_string(), "limit")
|
||||
.and_then(|value| value.parse::<usize>().ok())
|
||||
.filter(|value| *value > 0 && *value <= 200)
|
||||
.unwrap_or(50);
|
||||
let items = state
|
||||
.list_proxy_node_events(node_id, limit)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|event| build_admin_proxy_node_event_payload(&event))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
return Ok(Some(Json(json!({ "items": items })).into_response()));
|
||||
return Ok(Some(
|
||||
state
|
||||
.build_admin_proxy_node_events_response(node_id, limit)
|
||||
.await?,
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Some(build_admin_proxy_nodes_data_unavailable_response()))
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
use super::{adaptive, core, proxy_nodes};
|
||||
use crate::handlers::admin::provider::pool_admin;
|
||||
use crate::handlers::admin::request::{AdminRouteRequest, AdminRouteResult};
|
||||
|
||||
pub(crate) async fn maybe_build_local_admin_system_response(
|
||||
request: AdminRouteRequest<'_>,
|
||||
) -> AdminRouteResult {
|
||||
if let Some(response) = core::maybe_build_local_admin_core_response(
|
||||
&request.state(),
|
||||
&request.request_context(),
|
||||
request.request_body(),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
return Ok(Some(response));
|
||||
}
|
||||
|
||||
if let Some(response) = adaptive::maybe_build_local_admin_adaptive_response(
|
||||
&request.state(),
|
||||
&request.request_context(),
|
||||
request.request_body(),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
return Ok(Some(response));
|
||||
}
|
||||
|
||||
if let Some(response) = pool_admin::maybe_build_local_admin_pool_response(
|
||||
&request.state(),
|
||||
&request.request_context(),
|
||||
request.request_body(),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
return Ok(Some(response));
|
||||
}
|
||||
|
||||
if let Some(response) = proxy_nodes::maybe_build_local_admin_proxy_nodes_response(
|
||||
&request.state(),
|
||||
&request.request_context(),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
return Ok(Some(response));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
@@ -1,829 +1,73 @@
|
||||
use crate::api::ai::admin_endpoint_signature_parts;
|
||||
use crate::handlers::shared::{decrypt_catalog_secret_with_fallbacks, unix_secs_to_rfc3339};
|
||||
use crate::{AppState, GatewayError};
|
||||
use aether_crypto::encrypt_python_fernet_plaintext;
|
||||
use aether_data_contracts::repository::global_models::{
|
||||
AdminGlobalModelListQuery, AdminProviderModelListQuery,
|
||||
use crate::handlers::admin::request::AdminAppState;
|
||||
use crate::handlers::shared::unix_secs_to_rfc3339;
|
||||
use crate::GatewayError;
|
||||
use aether_admin::system::{
|
||||
admin_system_config_default_value as admin_system_config_default_value_pure,
|
||||
admin_system_config_delete_keys as admin_system_config_delete_keys_pure,
|
||||
build_admin_system_config_deleted_payload,
|
||||
build_admin_system_config_detail_payload as build_admin_system_config_detail_payload_pure,
|
||||
build_admin_system_config_updated_payload,
|
||||
build_admin_system_configs_payload as build_admin_system_configs_payload_pure,
|
||||
is_sensitive_admin_system_config_key as is_sensitive_admin_system_config_key_pure,
|
||||
normalize_admin_system_config_key as normalize_admin_system_config_key_pure,
|
||||
parse_admin_system_config_update,
|
||||
};
|
||||
use aether_data_contracts::repository::provider_catalog::StoredProviderCatalogEndpoint;
|
||||
use aether_crypto::encrypt_python_fernet_plaintext;
|
||||
use axum::body::Bytes;
|
||||
use axum::http;
|
||||
use chrono::Utc;
|
||||
use serde_json::json;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
const ADMIN_SYSTEM_CONFIG_EXPORT_VERSION: &str = "2.2";
|
||||
const ADMIN_SYSTEM_EXPORT_PAGE_LIMIT: usize = 10_000;
|
||||
const PROVIDER_OPS_SENSITIVE_CREDENTIAL_FIELDS: &[&str] = &[
|
||||
"api_key",
|
||||
"password",
|
||||
"refresh_token",
|
||||
"session_token",
|
||||
"session_cookie",
|
||||
"token_cookie",
|
||||
"auth_cookie",
|
||||
"cookie_string",
|
||||
"cookie",
|
||||
];
|
||||
const REQUEST_RECORD_LEVEL_KEY: &str = "request_record_level";
|
||||
const LEGACY_REQUEST_LOG_LEVEL_KEY: &str = "request_log_level";
|
||||
const SENSITIVE_SYSTEM_CONFIG_KEYS: &[&str] = &["smtp_password"];
|
||||
const ADMIN_SYSTEM_USERS_EXPORT_VERSION: &str = "1.3";
|
||||
|
||||
pub(crate) fn decrypt_admin_system_export_secret(
|
||||
state: &AppState,
|
||||
ciphertext: &str,
|
||||
) -> Option<String> {
|
||||
decrypt_catalog_secret_with_fallbacks(state.encryption_key(), ciphertext)
|
||||
}
|
||||
|
||||
pub(crate) fn normalize_admin_system_export_api_formats(
|
||||
raw_formats: Option<&serde_json::Value>,
|
||||
) -> Vec<String> {
|
||||
let Some(raw_formats) = raw_formats.and_then(serde_json::Value::as_array) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let mut normalized = Vec::new();
|
||||
let mut seen = BTreeSet::new();
|
||||
for raw in raw_formats {
|
||||
let Some(value) = raw
|
||||
.as_str()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let Some((signature, _, _)) = admin_endpoint_signature_parts(value) else {
|
||||
continue;
|
||||
};
|
||||
if seen.insert(signature) {
|
||||
normalized.push(signature.to_string());
|
||||
}
|
||||
}
|
||||
normalized
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_admin_system_export_key_api_formats(
|
||||
raw_formats: Option<&serde_json::Value>,
|
||||
provider_endpoint_formats: &[String],
|
||||
) -> Vec<String> {
|
||||
let normalized = normalize_admin_system_export_api_formats(raw_formats);
|
||||
if !normalized.is_empty() {
|
||||
return normalized;
|
||||
}
|
||||
if raw_formats.is_none() {
|
||||
return provider_endpoint_formats.to_vec();
|
||||
}
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
pub(crate) fn collect_admin_system_export_provider_endpoint_formats(
|
||||
endpoints: &[StoredProviderCatalogEndpoint],
|
||||
) -> Vec<String> {
|
||||
endpoints
|
||||
.iter()
|
||||
.filter_map(|endpoint| admin_endpoint_signature_parts(&endpoint.api_format))
|
||||
.map(|(signature, _, _)| signature.to_string())
|
||||
.collect::<BTreeSet<_>>()
|
||||
.into_iter()
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn decrypt_admin_system_export_provider_config(
|
||||
state: &AppState,
|
||||
config: Option<&serde_json::Value>,
|
||||
) -> Option<serde_json::Value> {
|
||||
let mut decrypted = config.cloned()?;
|
||||
let Some(credentials) = decrypted
|
||||
.get_mut("provider_ops")
|
||||
.and_then(serde_json::Value::as_object_mut)
|
||||
.and_then(|provider_ops| provider_ops.get_mut("connector"))
|
||||
.and_then(serde_json::Value::as_object_mut)
|
||||
.and_then(|connector| connector.get_mut("credentials"))
|
||||
.and_then(serde_json::Value::as_object_mut)
|
||||
else {
|
||||
return Some(decrypted);
|
||||
};
|
||||
|
||||
for field in PROVIDER_OPS_SENSITIVE_CREDENTIAL_FIELDS {
|
||||
let Some(serde_json::Value::String(ciphertext)) = credentials.get(*field).cloned() else {
|
||||
continue;
|
||||
};
|
||||
if let Some(plaintext) = decrypt_admin_system_export_secret(state, &ciphertext) {
|
||||
credentials.insert((*field).to_string(), serde_json::Value::String(plaintext));
|
||||
}
|
||||
}
|
||||
|
||||
Some(decrypted)
|
||||
}
|
||||
|
||||
pub(crate) async fn build_admin_system_config_export_payload(
|
||||
state: &AppState,
|
||||
) -> Result<serde_json::Value, GatewayError> {
|
||||
let global_models = state
|
||||
.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 = state.list_provider_catalog_providers(false).await?;
|
||||
let provider_ids = providers
|
||||
.iter()
|
||||
.map(|provider| provider.id.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let endpoints = state
|
||||
.list_provider_catalog_endpoints_by_provider_ids(&provider_ids)
|
||||
.await?;
|
||||
let keys = state
|
||||
.list_provider_catalog_keys_by_provider_ids(&provider_ids)
|
||||
.await?;
|
||||
|
||||
let mut endpoints_by_provider = BTreeMap::<String, Vec<_>>::new();
|
||||
for endpoint in endpoints {
|
||||
endpoints_by_provider
|
||||
.entry(endpoint.provider_id.clone())
|
||||
.or_default()
|
||||
.push(endpoint);
|
||||
}
|
||||
let mut keys_by_provider = BTreeMap::<String, Vec<_>>::new();
|
||||
for key in keys {
|
||||
keys_by_provider
|
||||
.entry(key.provider_id.clone())
|
||||
.or_default()
|
||||
.push(key);
|
||||
}
|
||||
|
||||
let mut provider_models_by_provider = BTreeMap::<String, Vec<_>>::new();
|
||||
for provider in &providers {
|
||||
let models = state
|
||||
.list_admin_provider_models(&AdminProviderModelListQuery {
|
||||
provider_id: provider.id.clone(),
|
||||
is_active: None,
|
||||
offset: 0,
|
||||
limit: ADMIN_SYSTEM_EXPORT_PAGE_LIMIT,
|
||||
})
|
||||
.await?;
|
||||
provider_models_by_provider.insert(provider.id.clone(), models);
|
||||
}
|
||||
|
||||
let providers_data = providers
|
||||
.iter()
|
||||
.map(|provider| {
|
||||
let endpoints = endpoints_by_provider.remove(&provider.id).unwrap_or_default();
|
||||
let provider_endpoint_formats =
|
||||
collect_admin_system_export_provider_endpoint_formats(&endpoints);
|
||||
let endpoints_data = endpoints
|
||||
.iter()
|
||||
.map(|endpoint| {
|
||||
json!({
|
||||
"api_format": endpoint.api_format,
|
||||
"base_url": endpoint.base_url,
|
||||
"header_rules": endpoint.header_rules,
|
||||
"body_rules": endpoint.body_rules,
|
||||
"max_retries": endpoint.max_retries,
|
||||
"is_active": endpoint.is_active,
|
||||
"custom_path": endpoint.custom_path,
|
||||
"config": endpoint.config,
|
||||
"format_acceptance_config": endpoint.format_acceptance_config,
|
||||
"proxy": endpoint.proxy,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut keys = keys_by_provider.remove(&provider.id).unwrap_or_default();
|
||||
keys.sort_by(|left, right| {
|
||||
left.internal_priority
|
||||
.cmp(&right.internal_priority)
|
||||
.then(
|
||||
left.created_at_unix_secs
|
||||
.unwrap_or(0)
|
||||
.cmp(&right.created_at_unix_secs.unwrap_or(0)),
|
||||
)
|
||||
.then(left.id.cmp(&right.id))
|
||||
});
|
||||
let keys_data = keys
|
||||
.iter()
|
||||
.map(|key| {
|
||||
let api_formats = resolve_admin_system_export_key_api_formats(
|
||||
key.api_formats.as_ref(),
|
||||
&provider_endpoint_formats,
|
||||
);
|
||||
let mut payload = json!({
|
||||
"api_formats": api_formats,
|
||||
"supported_endpoints": api_formats,
|
||||
"auth_type": key.auth_type,
|
||||
"name": key.name,
|
||||
"note": key.note,
|
||||
"rate_multipliers": key.rate_multipliers,
|
||||
"internal_priority": key.internal_priority,
|
||||
"global_priority_by_format": key.global_priority_by_format,
|
||||
"rpm_limit": key.rpm_limit,
|
||||
"allowed_models": key.allowed_models,
|
||||
"capabilities": key.capabilities,
|
||||
"cache_ttl_minutes": key.cache_ttl_minutes,
|
||||
"max_probe_interval_minutes": key.max_probe_interval_minutes,
|
||||
"is_active": key.is_active,
|
||||
"proxy": key.proxy,
|
||||
"fingerprint": key.fingerprint,
|
||||
"auto_fetch_models": key.auto_fetch_models,
|
||||
"locked_models": key.locked_models,
|
||||
"model_include_patterns": key.model_include_patterns,
|
||||
"model_exclude_patterns": key.model_exclude_patterns,
|
||||
"api_key": decrypt_admin_system_export_secret(state, &key.encrypted_api_key)
|
||||
.unwrap_or_default(),
|
||||
});
|
||||
if let Some(ciphertext) = key.encrypted_auth_config.as_deref() {
|
||||
if let Some(plaintext) =
|
||||
decrypt_admin_system_export_secret(state, ciphertext)
|
||||
{
|
||||
payload["auth_config"] = json!(plaintext);
|
||||
}
|
||||
}
|
||||
payload
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let models_data = provider_models_by_provider
|
||||
.remove(&provider.id)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|model| {
|
||||
json!({
|
||||
"provider_model_name": model.provider_model_name,
|
||||
"provider_model_mappings": model.provider_model_mappings,
|
||||
"price_per_request": model.price_per_request,
|
||||
"tiered_pricing": model.tiered_pricing,
|
||||
"supports_vision": model.supports_vision,
|
||||
"supports_function_calling": model.supports_function_calling,
|
||||
"supports_streaming": model.supports_streaming,
|
||||
"supports_extended_thinking": model.supports_extended_thinking,
|
||||
"supports_image_generation": model.supports_image_generation,
|
||||
"is_active": model.is_active,
|
||||
"config": model.config,
|
||||
"global_model_name": global_model_name_by_id.get(&model.global_model_id),
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
json!({
|
||||
"name": provider.name,
|
||||
"description": provider.description,
|
||||
"website": provider.website,
|
||||
"provider_type": provider.provider_type,
|
||||
"billing_type": provider.billing_type,
|
||||
"monthly_quota_usd": provider.monthly_quota_usd,
|
||||
"quota_reset_day": provider.quota_reset_day,
|
||||
"provider_priority": provider.provider_priority,
|
||||
"keep_priority_on_conversion": provider.keep_priority_on_conversion,
|
||||
"enable_format_conversion": provider.enable_format_conversion,
|
||||
"is_active": provider.is_active,
|
||||
"concurrent_limit": provider.concurrent_limit,
|
||||
"max_retries": provider.max_retries,
|
||||
"proxy": provider.proxy,
|
||||
"request_timeout": provider.request_timeout_secs,
|
||||
"stream_first_byte_timeout": provider.stream_first_byte_timeout_secs,
|
||||
"config": decrypt_admin_system_export_provider_config(state, provider.config.as_ref()),
|
||||
"endpoints": endpoints_data,
|
||||
"api_keys": keys_data,
|
||||
"models": models_data,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let ldap_config = state.get_ldap_module_config().await?;
|
||||
let ldap_data = ldap_config.map(|config| {
|
||||
let bind_password = config
|
||||
.bind_password_encrypted
|
||||
.as_deref()
|
||||
.and_then(|ciphertext| decrypt_admin_system_export_secret(state, 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 = state.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(state, 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 = state.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(state, 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 = state.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) fn serialize_admin_system_users_export_wallet(
|
||||
wallet: Option<&aether_data::repository::wallet::StoredWalletSnapshot>,
|
||||
) -> Option<serde_json::Value> {
|
||||
let wallet = wallet?;
|
||||
let recharge_balance = wallet.balance;
|
||||
let gift_balance = wallet.gift_balance;
|
||||
let spendable_balance = recharge_balance + gift_balance;
|
||||
let unlimited = wallet.limit_mode.eq_ignore_ascii_case("unlimited");
|
||||
|
||||
Some(json!({
|
||||
"id": wallet.id.clone(),
|
||||
"balance": spendable_balance,
|
||||
"recharge_balance": recharge_balance,
|
||||
"gift_balance": gift_balance,
|
||||
"refundable_balance": recharge_balance,
|
||||
"currency": wallet.currency.clone(),
|
||||
"status": wallet.status.clone(),
|
||||
"limit_mode": wallet.limit_mode.clone(),
|
||||
"unlimited": unlimited,
|
||||
"total_recharged": wallet.total_recharged,
|
||||
"total_consumed": wallet.total_consumed,
|
||||
"total_refunded": wallet.total_refunded,
|
||||
"total_adjusted": wallet.total_adjusted,
|
||||
"updated_at": unix_secs_to_rfc3339(wallet.updated_at_unix_secs),
|
||||
}))
|
||||
}
|
||||
|
||||
fn build_admin_system_users_export_api_key_payload(
|
||||
state: &AppState,
|
||||
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(state, 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)
|
||||
}
|
||||
|
||||
pub(crate) async fn build_admin_system_users_export_payload(
|
||||
state: &AppState,
|
||||
) -> Result<serde_json::Value, GatewayError> {
|
||||
let users = state.list_non_admin_export_users().await?;
|
||||
let user_ids = users.iter().map(|user| user.id.clone()).collect::<Vec<_>>();
|
||||
let user_wallets = state.list_wallet_snapshots_by_user_ids(&user_ids).await?;
|
||||
let user_api_keys = state
|
||||
.list_auth_api_key_export_records_by_user_ids(&user_ids)
|
||||
.await?;
|
||||
let standalone_api_keys = state.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 = state
|
||||
.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| build_admin_system_users_export_api_key_payload(state, 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| {
|
||||
build_admin_system_users_export_api_key_payload(
|
||||
state,
|
||||
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 normalize_admin_system_config_key(requested_key: &str) -> String {
|
||||
let trimmed = requested_key.trim();
|
||||
if trimmed.eq_ignore_ascii_case(LEGACY_REQUEST_LOG_LEVEL_KEY) {
|
||||
REQUEST_RECORD_LEVEL_KEY.to_string()
|
||||
} else {
|
||||
trimmed.to_string()
|
||||
}
|
||||
normalize_admin_system_config_key_pure(requested_key)
|
||||
}
|
||||
|
||||
fn admin_system_config_delete_keys(requested_key: &str) -> Vec<String> {
|
||||
let normalized = normalize_admin_system_config_key(requested_key);
|
||||
if normalized == REQUEST_RECORD_LEVEL_KEY {
|
||||
vec![
|
||||
REQUEST_RECORD_LEVEL_KEY.to_string(),
|
||||
LEGACY_REQUEST_LOG_LEVEL_KEY.to_string(),
|
||||
]
|
||||
} else {
|
||||
vec![normalized]
|
||||
}
|
||||
admin_system_config_delete_keys_pure(requested_key)
|
||||
}
|
||||
|
||||
fn is_sensitive_admin_system_config_key(key: &str) -> bool {
|
||||
SENSITIVE_SYSTEM_CONFIG_KEYS
|
||||
.iter()
|
||||
.any(|candidate| candidate.eq_ignore_ascii_case(key))
|
||||
}
|
||||
|
||||
fn system_config_is_set(value: &serde_json::Value) -> bool {
|
||||
match value {
|
||||
serde_json::Value::Null => false,
|
||||
serde_json::Value::Bool(value) => *value,
|
||||
serde_json::Value::Number(value) => value
|
||||
.as_i64()
|
||||
.map(|value| value != 0)
|
||||
.or_else(|| value.as_u64().map(|value| value != 0))
|
||||
.or_else(|| value.as_f64().map(|value| value != 0.0))
|
||||
.unwrap_or(false),
|
||||
serde_json::Value::String(value) => !value.trim().is_empty(),
|
||||
serde_json::Value::Array(value) => !value.is_empty(),
|
||||
serde_json::Value::Object(value) => !value.is_empty(),
|
||||
}
|
||||
pub(crate) fn is_sensitive_admin_system_config_key(key: &str) -> bool {
|
||||
is_sensitive_admin_system_config_key_pure(key)
|
||||
}
|
||||
|
||||
fn admin_system_config_default_value(key: &str) -> Option<serde_json::Value> {
|
||||
match key {
|
||||
"site_name" => Some(json!("Aether")),
|
||||
"site_subtitle" => Some(json!("AI Gateway")),
|
||||
"default_user_initial_gift_usd" => Some(json!(10.0)),
|
||||
"password_policy_level" => Some(json!("weak")),
|
||||
REQUEST_RECORD_LEVEL_KEY => Some(json!("basic")),
|
||||
"max_request_body_size" => Some(json!(5_242_880)),
|
||||
"max_response_body_size" => Some(json!(5_242_880)),
|
||||
"sensitive_headers" => Some(json!([
|
||||
"authorization",
|
||||
"x-api-key",
|
||||
"api-key",
|
||||
"cookie",
|
||||
"set-cookie"
|
||||
])),
|
||||
"detail_log_retention_days" => Some(json!(7)),
|
||||
"compressed_log_retention_days" => Some(json!(30)),
|
||||
"header_retention_days" => Some(json!(90)),
|
||||
"log_retention_days" => Some(json!(365)),
|
||||
"enable_auto_cleanup" => Some(json!(true)),
|
||||
"cleanup_batch_size" => Some(json!(1000)),
|
||||
"request_candidates_retention_days" => Some(json!(30)),
|
||||
"request_candidates_cleanup_batch_size" => Some(json!(5000)),
|
||||
"enable_provider_checkin" => Some(json!(true)),
|
||||
"provider_checkin_time" => Some(json!("01:05")),
|
||||
"provider_priority_mode" => Some(json!("provider")),
|
||||
"scheduling_mode" => Some(json!("cache_affinity")),
|
||||
"auto_delete_expired_keys" => Some(json!(false)),
|
||||
"email_suffix_mode" => Some(json!("none")),
|
||||
"email_suffix_list" => Some(json!([])),
|
||||
"enable_format_conversion" => Some(json!(true)),
|
||||
"keep_priority_on_conversion" => Some(json!(false)),
|
||||
"audit_log_retention_days" => Some(json!(30)),
|
||||
"enable_db_maintenance" => Some(json!(true)),
|
||||
"system_proxy_node_id" => Some(serde_json::Value::Null),
|
||||
"smtp_host" => Some(serde_json::Value::Null),
|
||||
"smtp_port" => Some(json!(587)),
|
||||
"smtp_user" => Some(serde_json::Value::Null),
|
||||
"smtp_password" => Some(serde_json::Value::Null),
|
||||
"smtp_use_tls" => Some(json!(true)),
|
||||
"smtp_use_ssl" => Some(json!(false)),
|
||||
"smtp_from_email" => Some(serde_json::Value::Null),
|
||||
"smtp_from_name" => Some(json!("Aether")),
|
||||
"enable_oauth_token_refresh" => Some(json!(true)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn build_admin_system_config_list_item(
|
||||
key: &str,
|
||||
value: &serde_json::Value,
|
||||
description: Option<&str>,
|
||||
updated_at_unix_secs: Option<u64>,
|
||||
) -> serde_json::Value {
|
||||
let masked_value = if is_sensitive_admin_system_config_key(key) {
|
||||
serde_json::Value::Null
|
||||
} else {
|
||||
value.clone()
|
||||
};
|
||||
let is_set = is_sensitive_admin_system_config_key(key).then(|| system_config_is_set(value));
|
||||
let mut payload = json!({
|
||||
"key": key,
|
||||
"description": description,
|
||||
"updated_at": updated_at_unix_secs.and_then(unix_secs_to_rfc3339),
|
||||
"value": masked_value,
|
||||
});
|
||||
if let Some(is_set) = is_set {
|
||||
payload["is_set"] = json!(is_set);
|
||||
}
|
||||
payload
|
||||
admin_system_config_default_value_pure(key)
|
||||
}
|
||||
|
||||
pub(crate) fn build_admin_system_configs_payload(
|
||||
entries: &[aether_data::repository::system::StoredSystemConfigEntry],
|
||||
) -> serde_json::Value {
|
||||
let has_request_record_level = entries
|
||||
.iter()
|
||||
.any(|entry| entry.key == REQUEST_RECORD_LEVEL_KEY);
|
||||
json!(entries
|
||||
.iter()
|
||||
.filter_map(|entry| {
|
||||
if entry.key == LEGACY_REQUEST_LOG_LEVEL_KEY && has_request_record_level {
|
||||
return None;
|
||||
}
|
||||
let key = if entry.key == LEGACY_REQUEST_LOG_LEVEL_KEY {
|
||||
REQUEST_RECORD_LEVEL_KEY
|
||||
} else {
|
||||
entry.key.as_str()
|
||||
};
|
||||
Some(build_admin_system_config_list_item(
|
||||
key,
|
||||
&entry.value,
|
||||
entry.description.as_deref(),
|
||||
entry.updated_at_unix_secs,
|
||||
))
|
||||
})
|
||||
.collect::<Vec<_>>())
|
||||
build_admin_system_configs_payload_pure(entries)
|
||||
}
|
||||
|
||||
pub(crate) async fn build_admin_system_config_detail_payload(
|
||||
state: &AppState,
|
||||
state: &AdminAppState<'_>,
|
||||
requested_key: &str,
|
||||
) -> Result<Result<serde_json::Value, (http::StatusCode, serde_json::Value)>, GatewayError> {
|
||||
let requested_key = requested_key.trim();
|
||||
let normalized_key = normalize_admin_system_config_key(requested_key);
|
||||
let value = state
|
||||
.read_system_config_json_value(&normalized_key)
|
||||
.read_system_config_json_value(&normalize_admin_system_config_key(requested_key))
|
||||
.await?
|
||||
.or_else(|| admin_system_config_default_value(&normalized_key));
|
||||
let Some(value) = value else {
|
||||
return Ok(Err((
|
||||
http::StatusCode::NOT_FOUND,
|
||||
json!({ "detail": format!("配置项 '{requested_key}' 不存在") }),
|
||||
)));
|
||||
};
|
||||
if is_sensitive_admin_system_config_key(&normalized_key) {
|
||||
return Ok(Ok(json!({
|
||||
"key": requested_key,
|
||||
"value": serde_json::Value::Null,
|
||||
"is_set": system_config_is_set(&value),
|
||||
})));
|
||||
}
|
||||
Ok(Ok(json!({
|
||||
"key": requested_key,
|
||||
"value": value,
|
||||
})))
|
||||
.or_else(|| {
|
||||
admin_system_config_default_value(&normalize_admin_system_config_key(requested_key))
|
||||
});
|
||||
Ok(build_admin_system_config_detail_payload_pure(
|
||||
requested_key,
|
||||
value,
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) async fn apply_admin_system_config_update(
|
||||
state: &AppState,
|
||||
state: &AdminAppState<'_>,
|
||||
requested_key: &str,
|
||||
request_body: &Bytes,
|
||||
) -> Result<Result<serde_json::Value, (http::StatusCode, serde_json::Value)>, GatewayError> {
|
||||
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 update = match parse_admin_system_config_update(requested_key, request_body) {
|
||||
Ok(update) => update,
|
||||
Err(err) => return Ok(Err(err)),
|
||||
};
|
||||
let normalized_key = normalize_admin_system_config_key(requested_key);
|
||||
let mut value = payload
|
||||
.get("value")
|
||||
.cloned()
|
||||
.unwrap_or(serde_json::Value::Null);
|
||||
let description = match payload.get("description") {
|
||||
Some(serde_json::Value::String(value)) => Some(value.trim().to_string()),
|
||||
Some(serde_json::Value::Null) | None => None,
|
||||
Some(_) => {
|
||||
return Ok(Err((
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
json!({ "detail": "请求数据验证失败" }),
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
if normalized_key == "password_policy_level" {
|
||||
match value.as_str().map(str::trim) {
|
||||
Some("weak" | "medium" | "strong") => {
|
||||
value = json!(value.as_str().unwrap().trim());
|
||||
}
|
||||
Some(_) => {
|
||||
return Ok(Err((
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
json!({ "detail": "请求数据验证失败" }),
|
||||
)));
|
||||
}
|
||||
None if value.is_null() => {
|
||||
value = json!("weak");
|
||||
}
|
||||
None => {
|
||||
return Ok(Err((
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
json!({ "detail": "请求数据验证失败" }),
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut value = update.value;
|
||||
let normalized_key = update.normalized_key;
|
||||
let description = update.description;
|
||||
|
||||
if is_sensitive_admin_system_config_key(&normalized_key)
|
||||
&& value.as_str().is_some_and(|raw| !raw.is_empty())
|
||||
@@ -850,16 +94,16 @@ pub(crate) async fn apply_admin_system_config_update(
|
||||
} else {
|
||||
updated.value.clone()
|
||||
};
|
||||
Ok(Ok(json!({
|
||||
"key": updated.key,
|
||||
"value": display_value,
|
||||
"description": updated.description,
|
||||
"updated_at": updated.updated_at_unix_secs.and_then(unix_secs_to_rfc3339),
|
||||
})))
|
||||
Ok(Ok(build_admin_system_config_updated_payload(
|
||||
updated.key,
|
||||
display_value,
|
||||
updated.description,
|
||||
updated.updated_at_unix_secs,
|
||||
)))
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_admin_system_config(
|
||||
state: &AppState,
|
||||
state: &AdminAppState<'_>,
|
||||
requested_key: &str,
|
||||
) -> Result<Result<serde_json::Value, (http::StatusCode, serde_json::Value)>, GatewayError> {
|
||||
let delete_keys = admin_system_config_delete_keys(requested_key);
|
||||
@@ -873,7 +117,5 @@ pub(crate) async fn delete_admin_system_config(
|
||||
json!({ "detail": format!("配置项 '{requested_key}' 不存在") }),
|
||||
)));
|
||||
}
|
||||
Ok(Ok(json!({
|
||||
"message": format!("配置项 '{}' 已删除", requested_key.trim()),
|
||||
})))
|
||||
Ok(Ok(build_admin_system_config_deleted_payload(requested_key)))
|
||||
}
|
||||
|
||||
@@ -1,224 +0,0 @@
|
||||
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(), "[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::<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,
|
||||
}
|
||||
})))
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
mod providers;
|
||||
mod support;
|
||||
|
||||
pub(crate) use self::providers::build_admin_system_export_providers_payload;
|
||||
pub(crate) use self::support::{
|
||||
decrypt_admin_system_export_secret, ADMIN_SYSTEM_CONFIG_EXPORT_VERSION,
|
||||
ADMIN_SYSTEM_EXPORT_PAGE_LIMIT,
|
||||
};
|
||||
@@ -0,0 +1,179 @@
|
||||
use super::support::{
|
||||
collect_admin_system_export_provider_endpoint_formats,
|
||||
decrypt_admin_system_export_provider_config, decrypt_admin_system_export_secret,
|
||||
resolve_admin_system_export_key_api_formats, ADMIN_SYSTEM_EXPORT_PAGE_LIMIT,
|
||||
};
|
||||
use crate::handlers::admin::request::AdminAppState;
|
||||
use crate::GatewayError;
|
||||
use aether_data_contracts::repository::global_models::AdminProviderModelListQuery;
|
||||
use serde_json::json;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
pub(crate) async fn build_admin_system_export_providers_payload(
|
||||
state: &AdminAppState<'_>,
|
||||
global_model_name_by_id: &BTreeMap<String, String>,
|
||||
) -> Result<Vec<serde_json::Value>, GatewayError> {
|
||||
let providers = state.list_provider_catalog_providers(false).await?;
|
||||
let provider_ids = providers
|
||||
.iter()
|
||||
.map(|provider| provider.id.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let endpoints = state
|
||||
.list_provider_catalog_endpoints_by_provider_ids(&provider_ids)
|
||||
.await?;
|
||||
let keys = state
|
||||
.list_provider_catalog_keys_by_provider_ids(&provider_ids)
|
||||
.await?;
|
||||
|
||||
let mut endpoints_by_provider = BTreeMap::<String, Vec<_>>::new();
|
||||
for endpoint in endpoints {
|
||||
endpoints_by_provider
|
||||
.entry(endpoint.provider_id.clone())
|
||||
.or_default()
|
||||
.push(endpoint);
|
||||
}
|
||||
let mut keys_by_provider = BTreeMap::<String, Vec<_>>::new();
|
||||
for key in keys {
|
||||
keys_by_provider
|
||||
.entry(key.provider_id.clone())
|
||||
.or_default()
|
||||
.push(key);
|
||||
}
|
||||
|
||||
let mut provider_models_by_provider = BTreeMap::<String, Vec<_>>::new();
|
||||
for provider in &providers {
|
||||
let models = state
|
||||
.list_admin_provider_models(&AdminProviderModelListQuery {
|
||||
provider_id: provider.id.clone(),
|
||||
is_active: None,
|
||||
offset: 0,
|
||||
limit: ADMIN_SYSTEM_EXPORT_PAGE_LIMIT,
|
||||
})
|
||||
.await?;
|
||||
provider_models_by_provider.insert(provider.id.clone(), models);
|
||||
}
|
||||
|
||||
Ok(providers
|
||||
.iter()
|
||||
.map(|provider| {
|
||||
let endpoints = endpoints_by_provider.remove(&provider.id).unwrap_or_default();
|
||||
let provider_endpoint_formats =
|
||||
collect_admin_system_export_provider_endpoint_formats(&endpoints);
|
||||
let endpoints_data = endpoints
|
||||
.iter()
|
||||
.map(|endpoint| {
|
||||
json!({
|
||||
"api_format": endpoint.api_format,
|
||||
"base_url": endpoint.base_url,
|
||||
"header_rules": endpoint.header_rules,
|
||||
"body_rules": endpoint.body_rules,
|
||||
"max_retries": endpoint.max_retries,
|
||||
"is_active": endpoint.is_active,
|
||||
"custom_path": endpoint.custom_path,
|
||||
"config": endpoint.config,
|
||||
"format_acceptance_config": endpoint.format_acceptance_config,
|
||||
"proxy": endpoint.proxy,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut keys = keys_by_provider.remove(&provider.id).unwrap_or_default();
|
||||
keys.sort_by(|left, right| {
|
||||
left.internal_priority
|
||||
.cmp(&right.internal_priority)
|
||||
.then(
|
||||
left.created_at_unix_secs
|
||||
.unwrap_or(0)
|
||||
.cmp(&right.created_at_unix_secs.unwrap_or(0)),
|
||||
)
|
||||
.then(left.id.cmp(&right.id))
|
||||
});
|
||||
let keys_data = keys
|
||||
.iter()
|
||||
.map(|key| {
|
||||
let api_formats = resolve_admin_system_export_key_api_formats(
|
||||
key.api_formats.as_ref(),
|
||||
&provider_endpoint_formats,
|
||||
);
|
||||
let mut payload = json!({
|
||||
"api_formats": api_formats,
|
||||
"supported_endpoints": api_formats,
|
||||
"auth_type": key.auth_type,
|
||||
"name": key.name,
|
||||
"note": key.note,
|
||||
"rate_multipliers": key.rate_multipliers,
|
||||
"internal_priority": key.internal_priority,
|
||||
"global_priority_by_format": key.global_priority_by_format,
|
||||
"rpm_limit": key.rpm_limit,
|
||||
"allowed_models": key.allowed_models,
|
||||
"capabilities": key.capabilities,
|
||||
"cache_ttl_minutes": key.cache_ttl_minutes,
|
||||
"max_probe_interval_minutes": key.max_probe_interval_minutes,
|
||||
"is_active": key.is_active,
|
||||
"proxy": key.proxy,
|
||||
"fingerprint": key.fingerprint,
|
||||
"auto_fetch_models": key.auto_fetch_models,
|
||||
"locked_models": key.locked_models,
|
||||
"model_include_patterns": key.model_include_patterns,
|
||||
"model_exclude_patterns": key.model_exclude_patterns,
|
||||
"api_key": decrypt_admin_system_export_secret(state, &key.encrypted_api_key)
|
||||
.unwrap_or_default(),
|
||||
});
|
||||
if let Some(ciphertext) = key.encrypted_auth_config.as_deref() {
|
||||
if let Some(plaintext) =
|
||||
decrypt_admin_system_export_secret(state, ciphertext)
|
||||
{
|
||||
payload["auth_config"] = json!(plaintext);
|
||||
}
|
||||
}
|
||||
payload
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let models_data = provider_models_by_provider
|
||||
.remove(&provider.id)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|model| {
|
||||
json!({
|
||||
"provider_model_name": model.provider_model_name,
|
||||
"provider_model_mappings": model.provider_model_mappings,
|
||||
"price_per_request": model.price_per_request,
|
||||
"tiered_pricing": model.tiered_pricing,
|
||||
"supports_vision": model.supports_vision,
|
||||
"supports_function_calling": model.supports_function_calling,
|
||||
"supports_streaming": model.supports_streaming,
|
||||
"supports_extended_thinking": model.supports_extended_thinking,
|
||||
"supports_image_generation": model.supports_image_generation,
|
||||
"is_active": model.is_active,
|
||||
"config": model.config,
|
||||
"global_model_name": global_model_name_by_id.get(&model.global_model_id),
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
json!({
|
||||
"name": provider.name,
|
||||
"description": provider.description,
|
||||
"website": provider.website,
|
||||
"provider_type": provider.provider_type,
|
||||
"billing_type": provider.billing_type,
|
||||
"monthly_quota_usd": provider.monthly_quota_usd,
|
||||
"quota_reset_day": provider.quota_reset_day,
|
||||
"provider_priority": provider.provider_priority,
|
||||
"keep_priority_on_conversion": provider.keep_priority_on_conversion,
|
||||
"enable_format_conversion": provider.enable_format_conversion,
|
||||
"is_active": provider.is_active,
|
||||
"concurrent_limit": provider.concurrent_limit,
|
||||
"max_retries": provider.max_retries,
|
||||
"proxy": provider.proxy,
|
||||
"request_timeout": provider.request_timeout_secs,
|
||||
"stream_first_byte_timeout": provider.stream_first_byte_timeout_secs,
|
||||
"config": decrypt_admin_system_export_provider_config(state, provider.config.as_ref()),
|
||||
"endpoints": endpoints_data,
|
||||
"api_keys": keys_data,
|
||||
"models": models_data,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>())
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
use super::super::configs::is_sensitive_admin_system_config_key;
|
||||
use crate::api::ai::admin_endpoint_signature_parts;
|
||||
use crate::handlers::admin::request::AdminAppState;
|
||||
use crate::handlers::shared::decrypt_catalog_secret_with_fallbacks;
|
||||
use aether_data_contracts::repository::provider_catalog::StoredProviderCatalogEndpoint;
|
||||
|
||||
pub(crate) const ADMIN_SYSTEM_CONFIG_EXPORT_VERSION: &str = "2.2";
|
||||
pub(crate) const ADMIN_SYSTEM_EXPORT_PAGE_LIMIT: usize = 10_000;
|
||||
|
||||
const PROVIDER_OPS_SENSITIVE_CREDENTIAL_FIELDS: &[&str] = &[
|
||||
"api_key",
|
||||
"password",
|
||||
"refresh_token",
|
||||
"session_token",
|
||||
"session_cookie",
|
||||
"token_cookie",
|
||||
"auth_cookie",
|
||||
"cookie_string",
|
||||
"cookie",
|
||||
];
|
||||
|
||||
pub(crate) fn decrypt_admin_system_export_secret(
|
||||
state: &AdminAppState<'_>,
|
||||
ciphertext: &str,
|
||||
) -> Option<String> {
|
||||
decrypt_catalog_secret_with_fallbacks(state.encryption_key(), ciphertext)
|
||||
}
|
||||
|
||||
pub(super) fn normalize_admin_system_export_api_formats(
|
||||
raw_formats: Option<&serde_json::Value>,
|
||||
) -> Vec<String> {
|
||||
aether_admin::system::normalize_admin_system_export_api_formats(raw_formats, |value| {
|
||||
admin_endpoint_signature_parts(value).map(|(signature, _, _)| signature.to_string())
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn resolve_admin_system_export_key_api_formats(
|
||||
raw_formats: Option<&serde_json::Value>,
|
||||
provider_endpoint_formats: &[String],
|
||||
) -> Vec<String> {
|
||||
aether_admin::system::resolve_admin_system_export_key_api_formats(
|
||||
raw_formats,
|
||||
provider_endpoint_formats,
|
||||
|value| {
|
||||
admin_endpoint_signature_parts(value).map(|(signature, _, _)| signature.to_string())
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn collect_admin_system_export_provider_endpoint_formats(
|
||||
endpoints: &[StoredProviderCatalogEndpoint],
|
||||
) -> Vec<String> {
|
||||
aether_admin::system::collect_admin_system_export_provider_endpoint_formats(
|
||||
endpoints,
|
||||
|value| {
|
||||
admin_endpoint_signature_parts(value).map(|(signature, _, _)| signature.to_string())
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn decrypt_admin_system_export_provider_config(
|
||||
state: &AdminAppState<'_>,
|
||||
config: Option<&serde_json::Value>,
|
||||
) -> Option<serde_json::Value> {
|
||||
let mut decrypted = config.cloned()?;
|
||||
let Some(credentials) = decrypted
|
||||
.get_mut("provider_ops")
|
||||
.and_then(serde_json::Value::as_object_mut)
|
||||
.and_then(|provider_ops| provider_ops.get_mut("connector"))
|
||||
.and_then(serde_json::Value::as_object_mut)
|
||||
.and_then(|connector| connector.get_mut("credentials"))
|
||||
.and_then(serde_json::Value::as_object_mut)
|
||||
else {
|
||||
return Some(decrypted);
|
||||
};
|
||||
|
||||
for field in PROVIDER_OPS_SENSITIVE_CREDENTIAL_FIELDS {
|
||||
let Some(serde_json::Value::String(ciphertext)) = credentials.get(*field).cloned() else {
|
||||
continue;
|
||||
};
|
||||
if let Some(plaintext) = decrypt_admin_system_export_secret(state, &ciphertext) {
|
||||
credentials.insert((*field).to_string(), serde_json::Value::String(plaintext));
|
||||
}
|
||||
}
|
||||
|
||||
Some(decrypted)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
pub(crate) mod configs;
|
||||
pub(crate) mod email_templates;
|
||||
pub(crate) mod export;
|
||||
pub(crate) mod modules;
|
||||
pub(crate) mod paths;
|
||||
pub(crate) mod settings;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use crate::handlers::admin::request::AdminAppState;
|
||||
use crate::handlers::shared::{module_available_from_env, system_config_bool};
|
||||
use crate::{AppState, GatewayError};
|
||||
use crate::GatewayError;
|
||||
use aether_admin::system as admin_system_kernel;
|
||||
use serde_json::json;
|
||||
|
||||
pub(crate) struct AdminModuleDefinition {
|
||||
@@ -109,57 +111,27 @@ pub(crate) fn admin_module_by_name(name: &str) -> Option<&'static AdminModuleDef
|
||||
}
|
||||
|
||||
pub(crate) fn admin_module_name_from_status_path(request_path: &str) -> Option<String> {
|
||||
request_path
|
||||
.strip_prefix("/api/admin/modules/status/")
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty() && !value.contains('/'))
|
||||
.map(ToOwned::to_owned)
|
||||
admin_system_kernel::admin_module_name_from_status_path(request_path)
|
||||
}
|
||||
|
||||
pub(crate) fn admin_module_name_from_enabled_path(request_path: &str) -> Option<String> {
|
||||
request_path
|
||||
.strip_prefix("/api/admin/modules/status/")
|
||||
.and_then(|value| value.strip_suffix("/enabled"))
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty() && !value.contains('/'))
|
||||
.map(ToOwned::to_owned)
|
||||
admin_system_kernel::admin_module_name_from_enabled_path(request_path)
|
||||
}
|
||||
|
||||
pub(crate) fn oauth_module_config_is_valid(
|
||||
providers: &[aether_data::repository::auth_modules::StoredOAuthProviderModuleConfig],
|
||||
) -> bool {
|
||||
!providers.is_empty()
|
||||
&& providers.iter().all(|provider| {
|
||||
!provider.client_id.trim().is_empty()
|
||||
&& provider
|
||||
.client_secret_encrypted
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.is_some()
|
||||
&& !provider.redirect_uri.trim().is_empty()
|
||||
})
|
||||
admin_system_kernel::oauth_module_config_is_valid(providers)
|
||||
}
|
||||
|
||||
pub(crate) fn ldap_module_config_is_valid(
|
||||
config: Option<&aether_data::repository::auth_modules::StoredLdapModuleConfig>,
|
||||
) -> bool {
|
||||
let Some(config) = config else {
|
||||
return false;
|
||||
};
|
||||
!config.server_url.trim().is_empty()
|
||||
&& !config.bind_dn.trim().is_empty()
|
||||
&& !config.base_dn.trim().is_empty()
|
||||
&& config
|
||||
.bind_password_encrypted
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.is_some()
|
||||
admin_system_kernel::ldap_module_config_is_valid(config)
|
||||
}
|
||||
|
||||
pub(crate) async fn build_admin_module_runtime_state(
|
||||
state: &AppState,
|
||||
state: &AdminAppState<'_>,
|
||||
) -> Result<AdminModuleRuntimeState, GatewayError> {
|
||||
let oauth_providers = state.list_enabled_oauth_module_providers().await?;
|
||||
let ldap_config = state.get_ldap_module_config().await?;
|
||||
@@ -221,116 +193,27 @@ pub(crate) fn build_admin_module_validation_result(
|
||||
module: &AdminModuleDefinition,
|
||||
runtime: &AdminModuleRuntimeState,
|
||||
) -> (bool, Option<String>) {
|
||||
match module.name {
|
||||
"oauth" => {
|
||||
if runtime.oauth_providers.is_empty() {
|
||||
return (
|
||||
false,
|
||||
Some("请先配置并启用至少一个 OAuth Provider".to_string()),
|
||||
);
|
||||
}
|
||||
for provider in &runtime.oauth_providers {
|
||||
if provider.client_id.trim().is_empty() {
|
||||
return (
|
||||
false,
|
||||
Some(format!(
|
||||
"Provider [{}] 未配置 Client ID",
|
||||
provider.display_name
|
||||
)),
|
||||
);
|
||||
}
|
||||
if provider
|
||||
.client_secret_encrypted
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.is_none()
|
||||
{
|
||||
return (
|
||||
false,
|
||||
Some(format!(
|
||||
"Provider [{}] 未配置 Client Secret",
|
||||
provider.display_name
|
||||
)),
|
||||
);
|
||||
}
|
||||
if provider.redirect_uri.trim().is_empty() {
|
||||
return (
|
||||
false,
|
||||
Some(format!(
|
||||
"Provider [{}] 未配置回调地址",
|
||||
provider.display_name
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
||||
(true, None)
|
||||
}
|
||||
"ldap" => {
|
||||
let Some(config) = runtime.ldap_config.as_ref() else {
|
||||
return (false, Some("请先配置 LDAP 连接信息".to_string()));
|
||||
};
|
||||
if config.server_url.trim().is_empty() {
|
||||
return (false, Some("请配置 LDAP 服务器地址".to_string()));
|
||||
}
|
||||
if config.bind_dn.trim().is_empty() {
|
||||
return (false, Some("请配置绑定 DN".to_string()));
|
||||
}
|
||||
if config.base_dn.trim().is_empty() {
|
||||
return (false, Some("请配置搜索基准 DN".to_string()));
|
||||
}
|
||||
if config
|
||||
.bind_password_encrypted
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.is_none()
|
||||
{
|
||||
return (false, Some("请配置绑定密码".to_string()));
|
||||
}
|
||||
(true, None)
|
||||
}
|
||||
"notification_email" => {
|
||||
if runtime.smtp_configured {
|
||||
(true, None)
|
||||
} else {
|
||||
(false, Some("请先完成邮件配置(SMTP)".to_string()))
|
||||
}
|
||||
}
|
||||
"gemini_files" => {
|
||||
if runtime.gemini_files_has_capable_key {
|
||||
(true, None)
|
||||
} else {
|
||||
(
|
||||
false,
|
||||
Some("至少启用一个具有「Gemini 文件 API」能力的 Key".to_string()),
|
||||
)
|
||||
}
|
||||
}
|
||||
"management_tokens" | "proxy_nodes" => (true, None),
|
||||
_ => (true, None),
|
||||
}
|
||||
admin_system_kernel::build_admin_module_validation_result(
|
||||
module.name,
|
||||
&runtime.oauth_providers,
|
||||
runtime.ldap_config.as_ref(),
|
||||
runtime.gemini_files_has_capable_key,
|
||||
runtime.smtp_configured,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn build_admin_module_health(
|
||||
module: &AdminModuleDefinition,
|
||||
runtime: &AdminModuleRuntimeState,
|
||||
) -> &'static str {
|
||||
match module.name {
|
||||
"management_tokens" | "proxy_nodes" => "healthy",
|
||||
"gemini_files" => {
|
||||
if runtime.gemini_files_has_capable_key {
|
||||
"healthy"
|
||||
} else {
|
||||
"degraded"
|
||||
}
|
||||
}
|
||||
_ => "unknown",
|
||||
}
|
||||
admin_system_kernel::build_admin_module_health(
|
||||
module.name,
|
||||
runtime.gemini_files_has_capable_key,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) async fn build_admin_module_status_payload(
|
||||
state: &AppState,
|
||||
state: &AdminAppState<'_>,
|
||||
module: &AdminModuleDefinition,
|
||||
runtime: &AdminModuleRuntimeState,
|
||||
) -> Result<serde_json::Value, GatewayError> {
|
||||
@@ -348,32 +231,30 @@ pub(crate) async fn build_admin_module_status_payload(
|
||||
} else {
|
||||
(false, None)
|
||||
};
|
||||
let active = available && enabled && config_validated;
|
||||
let health = if available {
|
||||
build_admin_module_health(module, runtime)
|
||||
} else {
|
||||
"unknown"
|
||||
};
|
||||
Ok(json!({
|
||||
"name": module.name,
|
||||
"available": available,
|
||||
"enabled": enabled,
|
||||
"active": active,
|
||||
"config_validated": config_validated,
|
||||
"config_error": if config_validated { serde_json::Value::Null } else { json!(config_error) },
|
||||
"display_name": module.display_name,
|
||||
"description": module.description,
|
||||
"category": module.category,
|
||||
"admin_route": if available { json!(module.admin_route) } else { serde_json::Value::Null },
|
||||
"admin_menu_icon": module.admin_menu_icon,
|
||||
"admin_menu_group": module.admin_menu_group,
|
||||
"admin_menu_order": module.admin_menu_order,
|
||||
"health": health,
|
||||
}))
|
||||
Ok(admin_system_kernel::build_admin_module_status_payload(
|
||||
module.name,
|
||||
module.display_name,
|
||||
module.description,
|
||||
module.category,
|
||||
module.admin_route,
|
||||
module.admin_menu_icon,
|
||||
module.admin_menu_group,
|
||||
module.admin_menu_order,
|
||||
available,
|
||||
enabled,
|
||||
config_validated,
|
||||
config_error,
|
||||
health,
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) async fn build_admin_modules_status_payload(
|
||||
state: &AppState,
|
||||
state: &AdminAppState<'_>,
|
||||
) -> Result<serde_json::Value, GatewayError> {
|
||||
let runtime = build_admin_module_runtime_state(state).await?;
|
||||
let mut payload = serde_json::Map::new();
|
||||
|
||||
@@ -1,84 +1,39 @@
|
||||
pub(crate) fn is_admin_management_tokens_root(request_path: &str) -> bool {
|
||||
matches!(
|
||||
request_path,
|
||||
"/api/admin/management-tokens" | "/api/admin/management-tokens/"
|
||||
)
|
||||
aether_admin::system::is_admin_management_tokens_root(request_path)
|
||||
}
|
||||
|
||||
pub(crate) fn is_admin_system_configs_root(request_path: &str) -> bool {
|
||||
matches!(
|
||||
request_path,
|
||||
"/api/admin/system/configs" | "/api/admin/system/configs/"
|
||||
)
|
||||
aether_admin::system::is_admin_system_configs_root(request_path)
|
||||
}
|
||||
|
||||
pub(crate) fn is_admin_system_email_templates_root(request_path: &str) -> bool {
|
||||
matches!(
|
||||
request_path,
|
||||
"/api/admin/system/email/templates" | "/api/admin/system/email/templates/"
|
||||
)
|
||||
aether_admin::system::is_admin_system_email_templates_root(request_path)
|
||||
}
|
||||
|
||||
pub(crate) fn admin_system_config_key_from_path(request_path: &str) -> Option<String> {
|
||||
let value = request_path
|
||||
.strip_prefix("/api/admin/system/configs/")?
|
||||
.trim()
|
||||
.trim_matches('/')
|
||||
.to_string();
|
||||
if value.is_empty() || value.contains('/') {
|
||||
None
|
||||
} else {
|
||||
Some(value)
|
||||
}
|
||||
aether_admin::system::admin_system_config_key_from_path(request_path)
|
||||
}
|
||||
|
||||
pub(crate) fn admin_system_email_template_type_from_path(request_path: &str) -> Option<String> {
|
||||
let value = request_path
|
||||
.strip_prefix("/api/admin/system/email/templates/")?
|
||||
.trim()
|
||||
.trim_matches('/')
|
||||
.to_string();
|
||||
if value.is_empty() || value.contains('/') {
|
||||
None
|
||||
} else {
|
||||
Some(value)
|
||||
}
|
||||
aether_admin::system::admin_system_email_template_type_from_path(request_path)
|
||||
}
|
||||
|
||||
pub(crate) fn admin_system_email_template_preview_type_from_path(
|
||||
request_path: &str,
|
||||
) -> Option<String> {
|
||||
request_path
|
||||
.strip_prefix("/api/admin/system/email/templates/")?
|
||||
.strip_suffix("/preview")
|
||||
.map(|value| value.trim().trim_matches('/').to_string())
|
||||
.filter(|value| !value.is_empty() && !value.contains('/'))
|
||||
aether_admin::system::admin_system_email_template_preview_type_from_path(request_path)
|
||||
}
|
||||
|
||||
pub(crate) fn admin_system_email_template_reset_type_from_path(
|
||||
request_path: &str,
|
||||
) -> Option<String> {
|
||||
request_path
|
||||
.strip_prefix("/api/admin/system/email/templates/")?
|
||||
.strip_suffix("/reset")
|
||||
.map(|value| value.trim().trim_matches('/').to_string())
|
||||
.filter(|value| !value.is_empty() && !value.contains('/'))
|
||||
aether_admin::system::admin_system_email_template_reset_type_from_path(request_path)
|
||||
}
|
||||
|
||||
pub(crate) fn admin_management_token_id_from_path(request_path: &str) -> Option<String> {
|
||||
let raw = request_path.strip_prefix("/api/admin/management-tokens/")?;
|
||||
let normalized = raw.trim().trim_matches('/');
|
||||
if normalized.is_empty() || normalized.contains('/') {
|
||||
None
|
||||
} else {
|
||||
Some(normalized.to_string())
|
||||
}
|
||||
aether_admin::system::admin_management_token_id_from_path(request_path)
|
||||
}
|
||||
|
||||
pub(crate) fn admin_management_token_status_id_from_path(request_path: &str) -> Option<String> {
|
||||
request_path
|
||||
.strip_prefix("/api/admin/management-tokens/")?
|
||||
.strip_suffix("/status")
|
||||
.map(|value| value.trim().trim_matches('/').to_string())
|
||||
.filter(|value| !value.is_empty() && !value.contains('/'))
|
||||
aether_admin::system::admin_management_token_status_id_from_path(request_path)
|
||||
}
|
||||
|
||||
@@ -1,85 +1,19 @@
|
||||
use crate::handlers::admin::request::AdminAppState;
|
||||
use crate::handlers::shared::{system_config_bool, system_config_string};
|
||||
use crate::{AppState, GatewayError};
|
||||
use crate::GatewayError;
|
||||
use aether_admin::system::{
|
||||
build_admin_api_formats_payload as build_admin_api_formats_payload_pure,
|
||||
build_admin_system_check_update_payload as build_admin_system_check_update_payload_pure,
|
||||
build_admin_system_settings_payload as build_admin_system_settings_payload_pure,
|
||||
build_admin_system_settings_updated_payload,
|
||||
build_admin_system_stats_payload as build_admin_system_stats_payload_pure,
|
||||
parse_admin_system_settings_update,
|
||||
};
|
||||
use axum::body::Bytes;
|
||||
use axum::http;
|
||||
use serde_json::json;
|
||||
use std::fs;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct AdminApiFormatDefinition {
|
||||
value: &'static str,
|
||||
label: &'static str,
|
||||
default_path: &'static str,
|
||||
aliases: &'static [&'static str],
|
||||
}
|
||||
|
||||
const ADMIN_API_FORMAT_DEFINITIONS: &[AdminApiFormatDefinition] = &[
|
||||
AdminApiFormatDefinition {
|
||||
value: "openai:chat",
|
||||
label: "OpenAI Chat",
|
||||
default_path: "/v1/chat/completions",
|
||||
aliases: &[
|
||||
"openai",
|
||||
"openai_compatible",
|
||||
"deepseek",
|
||||
"grok",
|
||||
"moonshot",
|
||||
"zhipu",
|
||||
"qwen",
|
||||
"baichuan",
|
||||
"minimax",
|
||||
],
|
||||
},
|
||||
AdminApiFormatDefinition {
|
||||
value: "openai:cli",
|
||||
label: "OpenAI CLI",
|
||||
default_path: "/v1/responses",
|
||||
aliases: &["openai_cli", "responses"],
|
||||
},
|
||||
AdminApiFormatDefinition {
|
||||
value: "openai:compact",
|
||||
label: "OpenAI Compact",
|
||||
default_path: "/v1/responses/compact",
|
||||
aliases: &["openai_compact", "responses_compact"],
|
||||
},
|
||||
AdminApiFormatDefinition {
|
||||
value: "openai:video",
|
||||
label: "OpenAI Video",
|
||||
default_path: "/v1/videos",
|
||||
aliases: &["openai_video", "sora"],
|
||||
},
|
||||
AdminApiFormatDefinition {
|
||||
value: "claude:chat",
|
||||
label: "Claude Chat",
|
||||
default_path: "/v1/messages",
|
||||
aliases: &["claude", "anthropic", "claude_compatible"],
|
||||
},
|
||||
AdminApiFormatDefinition {
|
||||
value: "claude:cli",
|
||||
label: "Claude CLI",
|
||||
default_path: "/v1/messages",
|
||||
aliases: &["claude_cli", "claude-cli"],
|
||||
},
|
||||
AdminApiFormatDefinition {
|
||||
value: "gemini:chat",
|
||||
label: "Gemini Chat",
|
||||
default_path: "/v1beta/models/{model}:{action}",
|
||||
aliases: &["gemini", "google", "vertex"],
|
||||
},
|
||||
AdminApiFormatDefinition {
|
||||
value: "gemini:cli",
|
||||
label: "Gemini CLI",
|
||||
default_path: "/v1beta/models/{model}:{action}",
|
||||
aliases: &["gemini_cli", "gemini-cli"],
|
||||
},
|
||||
AdminApiFormatDefinition {
|
||||
value: "gemini:video",
|
||||
label: "Gemini Video",
|
||||
default_path: "/v1beta/models/{model}:predictLongRunning",
|
||||
aliases: &["gemini_video", "veo"],
|
||||
},
|
||||
];
|
||||
|
||||
pub(crate) fn current_aether_version() -> String {
|
||||
let version_file =
|
||||
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../src/_version.py");
|
||||
@@ -101,19 +35,11 @@ pub(crate) fn current_aether_version() -> String {
|
||||
}
|
||||
|
||||
pub(crate) fn build_admin_system_check_update_payload() -> serde_json::Value {
|
||||
json!({
|
||||
"current_version": current_aether_version(),
|
||||
"latest_version": serde_json::Value::Null,
|
||||
"has_update": false,
|
||||
"release_url": serde_json::Value::Null,
|
||||
"release_notes": serde_json::Value::Null,
|
||||
"published_at": serde_json::Value::Null,
|
||||
"error": "检查更新需要 Rust 管理后端",
|
||||
})
|
||||
build_admin_system_check_update_payload_pure(current_aether_version())
|
||||
}
|
||||
|
||||
pub(crate) async fn build_admin_system_stats_payload(
|
||||
state: &AppState,
|
||||
state: &AdminAppState<'_>,
|
||||
) -> Result<serde_json::Value, GatewayError> {
|
||||
let providers = state
|
||||
.list_provider_catalog_providers(false)
|
||||
@@ -126,22 +52,18 @@ pub(crate) async fn build_admin_system_stats_payload(
|
||||
.count() as u64;
|
||||
let stats = state.read_admin_system_stats().await?;
|
||||
|
||||
Ok(json!({
|
||||
"users": {
|
||||
"total": stats.total_users,
|
||||
"active": stats.active_users,
|
||||
},
|
||||
"providers": {
|
||||
"total": total_providers,
|
||||
"active": active_providers,
|
||||
},
|
||||
"api_keys": stats.total_api_keys,
|
||||
"requests": stats.total_requests,
|
||||
}))
|
||||
Ok(build_admin_system_stats_payload_pure(
|
||||
stats.total_users,
|
||||
stats.active_users,
|
||||
total_providers,
|
||||
active_providers,
|
||||
stats.total_api_keys,
|
||||
stats.total_requests,
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) async fn build_admin_system_settings_payload(
|
||||
state: &AppState,
|
||||
state: &AdminAppState<'_>,
|
||||
) -> Result<serde_json::Value, GatewayError> {
|
||||
let default_provider_config = state
|
||||
.read_system_config_json_value("default_provider")
|
||||
@@ -172,141 +94,84 @@ pub(crate) async fn build_admin_system_settings_payload(
|
||||
_ => "weak".to_string(),
|
||||
};
|
||||
|
||||
Ok(json!({
|
||||
"default_provider": default_provider,
|
||||
"default_model": default_model,
|
||||
"enable_usage_tracking": enable_usage_tracking,
|
||||
"password_policy_level": password_policy_level,
|
||||
}))
|
||||
Ok(build_admin_system_settings_payload_pure(
|
||||
default_provider,
|
||||
default_model,
|
||||
enable_usage_tracking,
|
||||
password_policy_level,
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) async fn apply_admin_system_settings_update(
|
||||
state: &AppState,
|
||||
state: &AdminAppState<'_>,
|
||||
request_body: &Bytes,
|
||||
) -> Result<Result<serde_json::Value, (http::StatusCode, serde_json::Value)>, GatewayError> {
|
||||
let payload = match serde_json::from_slice::<serde_json::Value>(request_body) {
|
||||
Ok(serde_json::Value::Object(payload)) => payload,
|
||||
Ok(_) | Err(_) => {
|
||||
return Ok(Err((
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
json!({ "detail": "请求数据验证失败" }),
|
||||
)));
|
||||
}
|
||||
let update = match parse_admin_system_settings_update(request_body) {
|
||||
Ok(update) => update,
|
||||
Err(err) => return Ok(Err(err)),
|
||||
};
|
||||
|
||||
if let Some(default_provider) = payload.get("default_provider") {
|
||||
if let Some(default_provider) = default_provider.as_str() {
|
||||
let default_provider = default_provider.trim();
|
||||
if default_provider.is_empty() {
|
||||
let _ = state
|
||||
.upsert_system_config_json_value(
|
||||
"default_provider",
|
||||
&serde_json::Value::Null,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
let provider_exists = state
|
||||
.list_provider_catalog_providers(false)
|
||||
.await
|
||||
.ok()
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.any(|provider| provider.is_active && provider.name == default_provider);
|
||||
if !provider_exists {
|
||||
return Ok(Err((
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
json!({ "detail": format!("提供商 '{default_provider}' 不存在或未启用") }),
|
||||
)));
|
||||
}
|
||||
let _ = state
|
||||
.upsert_system_config_json_value(
|
||||
"default_provider",
|
||||
&json!(default_provider),
|
||||
Some("系统默认提供商,当用户未设置个人提供商时使用"),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
} else if !default_provider.is_null() {
|
||||
return Ok(Err((
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
json!({ "detail": "请求数据验证失败" }),
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(default_model) = payload.get("default_model") {
|
||||
if let Some(default_model) = default_model.as_str() {
|
||||
let value = default_model.trim();
|
||||
let config_value = if value.is_empty() {
|
||||
serde_json::Value::Null
|
||||
} else {
|
||||
json!(value)
|
||||
};
|
||||
let _ = state
|
||||
.upsert_system_config_json_value("default_model", &config_value, None)
|
||||
.await?;
|
||||
} else if !default_model.is_null() {
|
||||
return Ok(Err((
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
json!({ "detail": "请求数据验证失败" }),
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(enable_usage_tracking) = payload.get("enable_usage_tracking") {
|
||||
if let Some(enable_usage_tracking) = enable_usage_tracking.as_bool() {
|
||||
let _ = state
|
||||
.upsert_system_config_json_value(
|
||||
"enable_usage_tracking",
|
||||
&json!(enable_usage_tracking),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
} else if !enable_usage_tracking.is_null() {
|
||||
return Ok(Err((
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
json!({ "detail": "请求数据验证失败" }),
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(password_policy_level) = payload.get("password_policy_level") {
|
||||
if let Some(password_policy_level) = password_policy_level.as_str() {
|
||||
if !matches!(password_policy_level.trim(), "weak" | "medium" | "strong") {
|
||||
if let Some(default_provider) = update.default_provider {
|
||||
if let Some(default_provider) = default_provider {
|
||||
let provider_exists = state
|
||||
.list_provider_catalog_providers(false)
|
||||
.await
|
||||
.ok()
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.any(|provider| provider.is_active && provider.name == default_provider);
|
||||
if !provider_exists {
|
||||
return Ok(Err((
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
json!({ "detail": "请求数据验证失败" }),
|
||||
json!({ "detail": format!("提供商 '{default_provider}' 不存在或未启用") }),
|
||||
)));
|
||||
}
|
||||
let _ = state
|
||||
.upsert_system_config_json_value(
|
||||
"password_policy_level",
|
||||
&json!(password_policy_level.trim()),
|
||||
None,
|
||||
"default_provider",
|
||||
&json!(default_provider),
|
||||
Some("系统默认提供商,当用户未设置个人提供商时使用"),
|
||||
)
|
||||
.await?;
|
||||
} else if !password_policy_level.is_null() {
|
||||
return Ok(Err((
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
json!({ "detail": "请求数据验证失败" }),
|
||||
)));
|
||||
} else {
|
||||
let _ = state
|
||||
.upsert_system_config_json_value("default_provider", &serde_json::Value::Null, None)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Ok(json!({ "message": "系统设置更新成功" })))
|
||||
if let Some(default_model) = update.default_model {
|
||||
let config_value = default_model
|
||||
.map(|value| json!(value))
|
||||
.unwrap_or(serde_json::Value::Null);
|
||||
let _ = state
|
||||
.upsert_system_config_json_value("default_model", &config_value, None)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if let Some(enable_usage_tracking) = update.enable_usage_tracking {
|
||||
let _ = state
|
||||
.upsert_system_config_json_value(
|
||||
"enable_usage_tracking",
|
||||
&json!(enable_usage_tracking),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if let Some(password_policy_level) = update.password_policy_level {
|
||||
let _ = state
|
||||
.upsert_system_config_json_value(
|
||||
"password_policy_level",
|
||||
&json!(password_policy_level),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(Ok(build_admin_system_settings_updated_payload()))
|
||||
}
|
||||
|
||||
pub(crate) fn build_admin_api_formats_payload() -> serde_json::Value {
|
||||
json!({
|
||||
"formats": ADMIN_API_FORMAT_DEFINITIONS
|
||||
.iter()
|
||||
.map(|definition| json!({
|
||||
"value": definition.value,
|
||||
"label": definition.label,
|
||||
"default_path": definition.default_path,
|
||||
"aliases": definition.aliases,
|
||||
}))
|
||||
.collect::<Vec<_>>(),
|
||||
})
|
||||
build_admin_api_formats_payload_pure()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user