mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-09 02:47:45 +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,355 +0,0 @@
|
||||
use crate::api::ai::{admin_default_body_rules_for_signature, admin_endpoint_signature_parts};
|
||||
use crate::handlers::public::{admin_requested_force_stream, normalize_admin_base_url};
|
||||
use crate::provider_transport::provider_types::provider_type_is_fixed;
|
||||
use crate::AppState;
|
||||
use aether_data_contracts::repository::provider_catalog::{
|
||||
StoredProviderCatalogEndpoint, StoredProviderCatalogProvider,
|
||||
};
|
||||
use serde_json::json;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::payloads::{build_admin_provider_endpoint_response, endpoint_key_counts_by_format};
|
||||
use super::payloads::{AdminProviderEndpointCreateRequest, AdminProviderEndpointUpdateRequest};
|
||||
|
||||
pub(crate) async fn build_admin_provider_endpoints_payload(
|
||||
state: &AppState,
|
||||
provider_id: &str,
|
||||
skip: usize,
|
||||
limit: usize,
|
||||
) -> Option<serde_json::Value> {
|
||||
if !state.has_provider_catalog_data_reader() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let provider = state
|
||||
.read_provider_catalog_providers_by_ids(&[provider_id.to_string()])
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|mut providers| providers.drain(..).next())?;
|
||||
let mut endpoints = state
|
||||
.list_provider_catalog_endpoints_by_provider_ids(std::slice::from_ref(&provider.id))
|
||||
.await
|
||||
.ok()
|
||||
.unwrap_or_default();
|
||||
endpoints.sort_by(|left, right| {
|
||||
right
|
||||
.created_at_unix_secs
|
||||
.unwrap_or_default()
|
||||
.cmp(&left.created_at_unix_secs.unwrap_or_default())
|
||||
.then_with(|| left.id.cmp(&right.id))
|
||||
});
|
||||
let keys = state
|
||||
.list_provider_catalog_keys_by_provider_ids(std::slice::from_ref(&provider.id))
|
||||
.await
|
||||
.ok()
|
||||
.unwrap_or_default();
|
||||
let (total_keys_by_format, active_keys_by_format) = endpoint_key_counts_by_format(&keys);
|
||||
let now_unix_secs = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.ok()
|
||||
.map(|duration| duration.as_secs())
|
||||
.unwrap_or(0);
|
||||
|
||||
Some(serde_json::Value::Array(
|
||||
endpoints
|
||||
.into_iter()
|
||||
.skip(skip)
|
||||
.take(limit)
|
||||
.map(|endpoint| {
|
||||
build_admin_provider_endpoint_response(
|
||||
&endpoint,
|
||||
&provider.name,
|
||||
total_keys_by_format
|
||||
.get(endpoint.api_format.as_str())
|
||||
.copied()
|
||||
.unwrap_or(0),
|
||||
active_keys_by_format
|
||||
.get(endpoint.api_format.as_str())
|
||||
.copied()
|
||||
.unwrap_or(0),
|
||||
now_unix_secs,
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) async fn build_admin_endpoint_payload(
|
||||
state: &AppState,
|
||||
endpoint_id: &str,
|
||||
) -> Option<serde_json::Value> {
|
||||
if !state.has_provider_catalog_data_reader() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let endpoint = state
|
||||
.read_provider_catalog_endpoints_by_ids(&[endpoint_id.to_string()])
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|mut endpoints| endpoints.drain(..).next())?;
|
||||
let provider = state
|
||||
.read_provider_catalog_providers_by_ids(std::slice::from_ref(&endpoint.provider_id))
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|mut providers| providers.drain(..).next())?;
|
||||
let keys = state
|
||||
.list_provider_catalog_keys_by_provider_ids(std::slice::from_ref(&endpoint.provider_id))
|
||||
.await
|
||||
.ok()
|
||||
.unwrap_or_default();
|
||||
let (total_keys_by_format, active_keys_by_format) = endpoint_key_counts_by_format(&keys);
|
||||
let now_unix_secs = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.ok()
|
||||
.map(|duration| duration.as_secs())
|
||||
.unwrap_or(0);
|
||||
|
||||
Some(build_admin_provider_endpoint_response(
|
||||
&endpoint,
|
||||
&provider.name,
|
||||
total_keys_by_format
|
||||
.get(endpoint.api_format.as_str())
|
||||
.copied()
|
||||
.unwrap_or(0),
|
||||
active_keys_by_format
|
||||
.get(endpoint.api_format.as_str())
|
||||
.copied()
|
||||
.unwrap_or(0),
|
||||
now_unix_secs,
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) async fn build_admin_create_provider_endpoint_record(
|
||||
state: &AppState,
|
||||
provider: &StoredProviderCatalogProvider,
|
||||
payload: AdminProviderEndpointCreateRequest,
|
||||
) -> Result<StoredProviderCatalogEndpoint, String> {
|
||||
if payload.provider_id.trim() != provider.id {
|
||||
return Err("provider_id 不匹配".to_string());
|
||||
}
|
||||
if provider_type_is_fixed(&provider.provider_type) {
|
||||
return Err("固定类型 Provider 不允许手动新增 Endpoint".to_string());
|
||||
}
|
||||
if !(0..=999).contains(&payload.max_retries) {
|
||||
return Err("max_retries 必须在 0 到 999 之间".to_string());
|
||||
}
|
||||
|
||||
let (normalized_api_format, api_family, endpoint_kind) =
|
||||
admin_endpoint_signature_parts(&payload.api_format)
|
||||
.ok_or_else(|| format!("无效的 api_format: {}", payload.api_format))?;
|
||||
let base_url = normalize_admin_base_url(&payload.base_url)?;
|
||||
|
||||
let existing_endpoints = state
|
||||
.list_provider_catalog_endpoints_by_provider_ids(std::slice::from_ref(&provider.id))
|
||||
.await
|
||||
.map_err(|err| format!("{err:?}"))?;
|
||||
if existing_endpoints
|
||||
.iter()
|
||||
.any(|endpoint| endpoint.api_format == normalized_api_format)
|
||||
{
|
||||
return Err(format!(
|
||||
"Provider {} 已存在 {} 格式的 Endpoint",
|
||||
provider.name, normalized_api_format
|
||||
));
|
||||
}
|
||||
|
||||
let body_rules = match payload.body_rules {
|
||||
Some(value) => Some(value),
|
||||
None => admin_default_body_rules_for_signature(
|
||||
normalized_api_format,
|
||||
Some(provider.provider_type.as_str()),
|
||||
)
|
||||
.and_then(|(_, rules)| (!rules.is_empty()).then_some(serde_json::Value::Array(rules))),
|
||||
};
|
||||
|
||||
let now_unix_secs = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.ok()
|
||||
.map(|duration| duration.as_secs())
|
||||
.unwrap_or(0);
|
||||
|
||||
StoredProviderCatalogEndpoint::new(
|
||||
Uuid::new_v4().to_string(),
|
||||
provider.id.clone(),
|
||||
normalized_api_format.to_string(),
|
||||
Some(api_family.to_string()),
|
||||
Some(endpoint_kind.to_string()),
|
||||
true,
|
||||
)
|
||||
.map_err(|err| err.to_string())?
|
||||
.with_timestamps(Some(now_unix_secs), Some(now_unix_secs))
|
||||
.with_transport_fields(
|
||||
base_url,
|
||||
payload.header_rules,
|
||||
body_rules,
|
||||
Some(payload.max_retries),
|
||||
payload.custom_path.and_then(|value| {
|
||||
let trimmed = value.trim().to_string();
|
||||
(!trimmed.is_empty()).then_some(trimmed)
|
||||
}),
|
||||
payload.config,
|
||||
payload.format_acceptance_config,
|
||||
payload.proxy,
|
||||
)
|
||||
.map_err(|err| err.to_string())
|
||||
}
|
||||
|
||||
pub(crate) async fn build_admin_update_provider_endpoint_record(
|
||||
state: &AppState,
|
||||
provider: &StoredProviderCatalogProvider,
|
||||
existing_endpoint: &StoredProviderCatalogEndpoint,
|
||||
raw_payload: &serde_json::Map<String, serde_json::Value>,
|
||||
payload: AdminProviderEndpointUpdateRequest,
|
||||
) -> Result<StoredProviderCatalogEndpoint, String> {
|
||||
let mut updated = existing_endpoint.clone();
|
||||
|
||||
if provider_type_is_fixed(&provider.provider_type)
|
||||
&& (raw_payload.contains_key("base_url") || raw_payload.contains_key("custom_path"))
|
||||
{
|
||||
return Err("固定类型 Provider 的 Endpoint 不允许修改 base_url/custom_path".to_string());
|
||||
}
|
||||
|
||||
if let Some(value) = raw_payload.get("base_url") {
|
||||
let Some(base_url) = payload.base_url.as_deref() else {
|
||||
return Err(if value.is_null() {
|
||||
"base_url 不能为空".to_string()
|
||||
} else {
|
||||
"base_url 必须是字符串".to_string()
|
||||
});
|
||||
};
|
||||
updated.base_url = normalize_admin_base_url(base_url)?;
|
||||
}
|
||||
|
||||
if raw_payload.contains_key("custom_path") {
|
||||
updated.custom_path = payload.custom_path;
|
||||
}
|
||||
|
||||
if let Some(value) = raw_payload.get("header_rules") {
|
||||
if !value.is_null() && !value.is_array() {
|
||||
return Err("header_rules 必须是数组或 null".to_string());
|
||||
}
|
||||
updated.header_rules = if value.is_null() {
|
||||
None
|
||||
} else {
|
||||
payload.header_rules
|
||||
};
|
||||
}
|
||||
|
||||
if let Some(value) = raw_payload.get("body_rules") {
|
||||
if !value.is_null() && !value.is_array() {
|
||||
return Err("body_rules 必须是数组或 null".to_string());
|
||||
}
|
||||
updated.body_rules = if value.is_null() {
|
||||
None
|
||||
} else {
|
||||
payload.body_rules
|
||||
};
|
||||
}
|
||||
|
||||
if let Some(value) = raw_payload.get("max_retries") {
|
||||
let Some(max_retries) = payload.max_retries else {
|
||||
return Err(if value.is_null() {
|
||||
"max_retries 必须是 0 到 999 之间的整数".to_string()
|
||||
} else {
|
||||
"max_retries 必须是整数".to_string()
|
||||
});
|
||||
};
|
||||
if !(0..=999).contains(&max_retries) {
|
||||
return Err("max_retries 必须在 0 到 999 之间".to_string());
|
||||
}
|
||||
updated.max_retries = Some(max_retries);
|
||||
}
|
||||
|
||||
if raw_payload.contains_key("is_active") {
|
||||
let Some(is_active) = payload.is_active else {
|
||||
return Err("is_active 必须是布尔值".to_string());
|
||||
};
|
||||
updated.is_active = is_active;
|
||||
}
|
||||
|
||||
if let Some(value) = raw_payload.get("config") {
|
||||
if !value.is_null() && !value.is_object() {
|
||||
return Err("config 必须是对象或 null".to_string());
|
||||
}
|
||||
updated.config = if value.is_null() {
|
||||
None
|
||||
} else {
|
||||
payload.config
|
||||
};
|
||||
}
|
||||
|
||||
if let Some(value) = raw_payload.get("proxy") {
|
||||
if value.is_null() {
|
||||
updated.proxy = None;
|
||||
} else {
|
||||
let Some(mut proxy) = payload.proxy.and_then(|value| value.as_object().cloned()) else {
|
||||
return Err("proxy 必须是对象或 null".to_string());
|
||||
};
|
||||
if !proxy.contains_key("password") {
|
||||
if let Some(old_password) = existing_endpoint
|
||||
.proxy
|
||||
.as_ref()
|
||||
.and_then(serde_json::Value::as_object)
|
||||
.and_then(|proxy| proxy.get("password"))
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.filter(|value| !value.is_empty())
|
||||
{
|
||||
proxy.insert("password".to_string(), json!(old_password));
|
||||
}
|
||||
}
|
||||
updated.proxy = Some(serde_json::Value::Object(proxy));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(value) = raw_payload.get("format_acceptance_config") {
|
||||
if !value.is_null() && !value.is_object() {
|
||||
return Err("format_acceptance_config 必须是对象或 null".to_string());
|
||||
}
|
||||
updated.format_acceptance_config = if value.is_null() {
|
||||
None
|
||||
} else {
|
||||
payload.format_acceptance_config
|
||||
};
|
||||
}
|
||||
|
||||
let provider_type = provider.provider_type.trim().to_ascii_lowercase();
|
||||
if provider_type == "codex" && existing_endpoint.api_format == "openai:cli" {
|
||||
let has_config_in_payload = raw_payload.contains_key("config");
|
||||
let config_payload = if has_config_in_payload {
|
||||
updated.config.clone().unwrap_or_else(|| json!({}))
|
||||
} else {
|
||||
existing_endpoint
|
||||
.config
|
||||
.clone()
|
||||
.unwrap_or_else(|| json!({}))
|
||||
};
|
||||
let mut config = config_payload.as_object().cloned().unwrap_or_default();
|
||||
let requested = config
|
||||
.get("upstream_stream_policy")
|
||||
.or_else(|| config.get("upstreamStreamPolicy"))
|
||||
.or_else(|| config.get("upstream_stream"));
|
||||
if has_config_in_payload
|
||||
&& requested.is_some()
|
||||
&& !admin_requested_force_stream(requested.expect("checked above"))
|
||||
{
|
||||
return Err("Codex OpenAI CLI 端点固定为强制流式,不允许修改".to_string());
|
||||
}
|
||||
config.remove("upstreamStreamPolicy");
|
||||
config.remove("upstream_stream");
|
||||
config.insert("upstream_stream_policy".to_string(), json!("force_stream"));
|
||||
updated.config = Some(serde_json::Value::Object(config));
|
||||
}
|
||||
|
||||
let (_, api_family, endpoint_kind) = admin_endpoint_signature_parts(&updated.api_format)
|
||||
.ok_or_else(|| format!("无效的 api_format: {}", updated.api_format))?;
|
||||
updated.api_family = Some(api_family.to_string());
|
||||
updated.endpoint_kind = Some(endpoint_kind.to_string());
|
||||
updated.updated_at_unix_secs = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.ok()
|
||||
.map(|duration| duration.as_secs());
|
||||
|
||||
let _ = state;
|
||||
Ok(updated)
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
use super::builders::build_admin_create_provider_endpoint_record;
|
||||
use super::extractors::admin_provider_id_for_endpoints;
|
||||
use super::payloads::{build_admin_provider_endpoint_response, AdminProviderEndpointCreateRequest};
|
||||
use super::support::build_admin_endpoints_data_unavailable_response;
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::{AppState, GatewayError};
|
||||
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
|
||||
use crate::GatewayError;
|
||||
use axum::{
|
||||
body::{Body, Bytes},
|
||||
http,
|
||||
@@ -14,21 +13,21 @@ use serde_json::json;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
pub(super) async fn maybe_handle(
|
||||
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("endpoints_manage")
|
||||
|| decision.route_kind.as_deref() != Some("create_endpoint")
|
||||
|| request_context.request_method != http::Method::POST
|
||||
|| request_context.method() != http::Method::POST
|
||||
|| !request_context
|
||||
.request_path
|
||||
.path()
|
||||
.starts_with("/api/admin/endpoints/providers/")
|
||||
|| !request_context.request_path.ends_with("/endpoints")
|
||||
|| !request_context.path().ends_with("/endpoints")
|
||||
{
|
||||
return Ok(None);
|
||||
}
|
||||
@@ -37,7 +36,7 @@ pub(super) async fn maybe_handle(
|
||||
return Ok(Some(build_admin_endpoints_data_unavailable_response()));
|
||||
}
|
||||
|
||||
let Some(provider_id) = admin_provider_id_for_endpoints(&request_context.request_path) else {
|
||||
let Some(provider_id) = admin_provider_id_for_endpoints(request_context.path()) else {
|
||||
return Ok(Some(
|
||||
(
|
||||
http::StatusCode::NOT_FOUND,
|
||||
@@ -81,7 +80,9 @@ pub(super) async fn maybe_handle(
|
||||
.into_response(),
|
||||
));
|
||||
};
|
||||
let record = match build_admin_create_provider_endpoint_record(state, &provider, payload).await
|
||||
let record = match state
|
||||
.build_admin_create_provider_endpoint_record(&provider, payload)
|
||||
.await
|
||||
{
|
||||
Ok(record) => record,
|
||||
Err(detail) => {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use super::extractors::admin_default_body_rules_api_format;
|
||||
use crate::api::ai::admin_default_body_rules_for_signature;
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
|
||||
use crate::handlers::admin::shared::query_param_value;
|
||||
use crate::{AppState, GatewayError};
|
||||
use crate::GatewayError;
|
||||
use axum::{
|
||||
body::{Body, Bytes},
|
||||
http,
|
||||
@@ -12,26 +12,25 @@ use axum::{
|
||||
use serde_json::json;
|
||||
|
||||
pub(super) async fn maybe_handle(
|
||||
_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("endpoints_manage")
|
||||
|| decision.route_kind.as_deref() != Some("default_body_rules")
|
||||
|| !request_context
|
||||
.request_path
|
||||
.path()
|
||||
.starts_with("/api/admin/endpoints/defaults/")
|
||||
|| !request_context.request_path.ends_with("/body-rules")
|
||||
|| !request_context.path().ends_with("/body-rules")
|
||||
{
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let Some(api_format) = admin_default_body_rules_api_format(&request_context.request_path)
|
||||
else {
|
||||
let Some(api_format) = admin_default_body_rules_api_format(request_context.path()) else {
|
||||
return Ok(Some(
|
||||
(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
@@ -40,10 +39,7 @@ pub(super) async fn maybe_handle(
|
||||
.into_response(),
|
||||
));
|
||||
};
|
||||
let provider_type = query_param_value(
|
||||
request_context.request_query_string.as_deref(),
|
||||
"provider_type",
|
||||
);
|
||||
let provider_type = query_param_value(request_context.query_string(), "provider_type");
|
||||
|
||||
Ok(Some(
|
||||
match admin_default_body_rules_for_signature(&api_format, provider_type.as_deref()) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use super::extractors::admin_endpoint_id;
|
||||
use super::payloads::key_api_formats_without_entry;
|
||||
use super::support::build_admin_endpoints_data_unavailable_response;
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::{AppState, GatewayError};
|
||||
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
|
||||
use crate::GatewayError;
|
||||
use axum::{
|
||||
body::{Body, Bytes},
|
||||
http,
|
||||
@@ -13,20 +13,18 @@ use serde_json::json;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
pub(super) async fn maybe_handle(
|
||||
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("endpoints_manage")
|
||||
|| decision.route_kind.as_deref() != Some("delete_endpoint")
|
||||
|| request_context.request_method != http::Method::DELETE
|
||||
|| !request_context
|
||||
.request_path
|
||||
.starts_with("/api/admin/endpoints/")
|
||||
|| request_context.method() != http::Method::DELETE
|
||||
|| !request_context.path().starts_with("/api/admin/endpoints/")
|
||||
{
|
||||
return Ok(None);
|
||||
}
|
||||
@@ -35,7 +33,7 @@ pub(super) async fn maybe_handle(
|
||||
return Ok(Some(build_admin_endpoints_data_unavailable_response()));
|
||||
}
|
||||
|
||||
let Some(endpoint_id) = admin_endpoint_id(&request_context.request_path) else {
|
||||
let Some(endpoint_id) = admin_endpoint_id(request_context.path()) else {
|
||||
return Ok(Some(
|
||||
(
|
||||
http::StatusCode::NOT_FOUND,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use super::builders::build_admin_endpoint_payload;
|
||||
use super::extractors::admin_endpoint_id;
|
||||
use super::reads::build_admin_endpoint_payload;
|
||||
use super::support::build_admin_endpoints_data_unavailable_response;
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::{AppState, GatewayError};
|
||||
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
|
||||
use crate::GatewayError;
|
||||
use axum::{
|
||||
body::{Body, Bytes},
|
||||
http,
|
||||
@@ -12,19 +12,17 @@ use axum::{
|
||||
use serde_json::json;
|
||||
|
||||
pub(super) async fn maybe_handle(
|
||||
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("endpoints_manage")
|
||||
|| decision.route_kind.as_deref() != Some("get_endpoint")
|
||||
|| !request_context
|
||||
.request_path
|
||||
.starts_with("/api/admin/endpoints/")
|
||||
|| !request_context.path().starts_with("/api/admin/endpoints/")
|
||||
{
|
||||
return Ok(None);
|
||||
}
|
||||
@@ -33,7 +31,7 @@ pub(super) async fn maybe_handle(
|
||||
return Ok(Some(build_admin_endpoints_data_unavailable_response()));
|
||||
}
|
||||
|
||||
let Some(endpoint_id) = admin_endpoint_id(&request_context.request_path) else {
|
||||
let Some(endpoint_id) = admin_endpoint_id(request_context.path()) else {
|
||||
return Ok(Some(
|
||||
(
|
||||
http::StatusCode::NOT_FOUND,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use super::builders::build_admin_provider_endpoints_payload;
|
||||
use super::extractors::admin_provider_id_for_endpoints;
|
||||
use super::reads::build_admin_provider_endpoints_payload;
|
||||
use super::support::build_admin_endpoints_data_unavailable_response;
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
|
||||
use crate::handlers::admin::shared::query_param_value;
|
||||
use crate::{AppState, GatewayError};
|
||||
use crate::GatewayError;
|
||||
use axum::{
|
||||
body::{Body, Bytes},
|
||||
http,
|
||||
@@ -13,20 +13,20 @@ use axum::{
|
||||
use serde_json::json;
|
||||
|
||||
pub(super) async fn maybe_handle(
|
||||
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("endpoints_manage")
|
||||
|| decision.route_kind.as_deref() != Some("list_provider_endpoints")
|
||||
|| !request_context
|
||||
.request_path
|
||||
.path()
|
||||
.starts_with("/api/admin/endpoints/providers/")
|
||||
|| !request_context.request_path.ends_with("/endpoints")
|
||||
|| !request_context.path().ends_with("/endpoints")
|
||||
{
|
||||
return Ok(None);
|
||||
}
|
||||
@@ -35,7 +35,7 @@ pub(super) async fn maybe_handle(
|
||||
return Ok(Some(build_admin_endpoints_data_unavailable_response()));
|
||||
}
|
||||
|
||||
let Some(provider_id) = admin_provider_id_for_endpoints(&request_context.request_path) else {
|
||||
let Some(provider_id) = admin_provider_id_for_endpoints(request_context.path()) else {
|
||||
return Ok(Some(
|
||||
(
|
||||
http::StatusCode::NOT_FOUND,
|
||||
@@ -44,10 +44,10 @@ pub(super) async fn maybe_handle(
|
||||
.into_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)
|
||||
.unwrap_or(100);
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
mod builders;
|
||||
mod create;
|
||||
mod defaults;
|
||||
mod delete;
|
||||
mod detail;
|
||||
mod extractors;
|
||||
mod list;
|
||||
mod payloads;
|
||||
pub(crate) mod payloads;
|
||||
mod reads;
|
||||
mod support;
|
||||
mod update;
|
||||
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::{AppState, GatewayError};
|
||||
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
|
||||
use crate::GatewayError;
|
||||
use axum::{
|
||||
body::{Body, Bytes},
|
||||
response::Response,
|
||||
};
|
||||
|
||||
pub(crate) async fn maybe_build_local_admin_endpoints_routes_response(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
state: &AdminAppState<'_>,
|
||||
request_context: &AdminRequestContext<'_>,
|
||||
request_body: Option<&Bytes>,
|
||||
) -> Result<Option<Response<Body>>, GatewayError> {
|
||||
if let Some(response) = create::maybe_handle(state, request_context, request_body).await? {
|
||||
|
||||
@@ -1,52 +1,23 @@
|
||||
use crate::handlers::admin::shared::unix_secs_to_rfc3339;
|
||||
use aether_admin::provider::endpoints as admin_provider_endpoints_pure;
|
||||
use aether_data_contracts::repository::provider_catalog::{
|
||||
StoredProviderCatalogEndpoint, StoredProviderCatalogKey,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
pub(super) fn key_api_formats_without_entry(
|
||||
key: &StoredProviderCatalogKey,
|
||||
api_format: &str,
|
||||
) -> Option<Vec<String>> {
|
||||
let current_formats =
|
||||
crate::handlers::admin::shared::json_string_list(key.api_formats.as_ref());
|
||||
if !current_formats
|
||||
.iter()
|
||||
.any(|candidate| candidate == api_format)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
Some(
|
||||
current_formats
|
||||
.into_iter()
|
||||
.filter(|candidate| candidate != api_format)
|
||||
.collect(),
|
||||
)
|
||||
admin_provider_endpoints_pure::key_api_formats_without_entry(key, api_format)
|
||||
}
|
||||
|
||||
pub(super) fn endpoint_key_counts_by_format(
|
||||
keys: &[StoredProviderCatalogKey],
|
||||
) -> (BTreeMap<String, usize>, BTreeMap<String, usize>) {
|
||||
let mut total = BTreeMap::new();
|
||||
let mut active = BTreeMap::new();
|
||||
for key in keys {
|
||||
let Some(formats) = key
|
||||
.api_formats
|
||||
.as_ref()
|
||||
.and_then(serde_json::Value::as_array)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
for api_format in formats.iter().filter_map(serde_json::Value::as_str) {
|
||||
*total.entry(api_format.to_string()).or_insert(0) += 1;
|
||||
if key.is_active {
|
||||
*active.entry(api_format.to_string()).or_insert(0) += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
(total, active)
|
||||
) -> (
|
||||
std::collections::BTreeMap<String, usize>,
|
||||
std::collections::BTreeMap<String, usize>,
|
||||
) {
|
||||
admin_provider_endpoints_pure::endpoint_key_counts_by_format(keys)
|
||||
}
|
||||
|
||||
pub(super) fn build_admin_provider_endpoint_response(
|
||||
@@ -56,46 +27,13 @@ pub(super) fn build_admin_provider_endpoint_response(
|
||||
active_keys: usize,
|
||||
now_unix_secs: u64,
|
||||
) -> serde_json::Value {
|
||||
json!({
|
||||
"id": endpoint.id,
|
||||
"provider_id": endpoint.provider_id,
|
||||
"provider_name": provider_name,
|
||||
"api_format": endpoint.api_format,
|
||||
"base_url": endpoint.base_url,
|
||||
"custom_path": endpoint.custom_path,
|
||||
"header_rules": endpoint.header_rules,
|
||||
"body_rules": endpoint.body_rules,
|
||||
"max_retries": endpoint.max_retries.unwrap_or(2),
|
||||
"is_active": endpoint.is_active,
|
||||
"config": endpoint.config,
|
||||
"proxy": masked_proxy_value(endpoint.proxy.as_ref()),
|
||||
"format_acceptance_config": endpoint.format_acceptance_config,
|
||||
"total_keys": total_keys,
|
||||
"active_keys": active_keys,
|
||||
"created_at": endpoint_timestamp_or_now(endpoint.created_at_unix_secs, now_unix_secs),
|
||||
"updated_at": endpoint_timestamp_or_now(endpoint.updated_at_unix_secs, now_unix_secs),
|
||||
})
|
||||
}
|
||||
|
||||
fn masked_proxy_value(proxy: Option<&serde_json::Value>) -> serde_json::Value {
|
||||
let Some(proxy) = proxy.and_then(serde_json::Value::as_object) else {
|
||||
return serde_json::Value::Null;
|
||||
};
|
||||
let mut masked = proxy.clone();
|
||||
if masked
|
||||
.get("password")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.is_some_and(|value| !value.trim().is_empty())
|
||||
{
|
||||
masked.insert("password".to_string(), json!("***"));
|
||||
}
|
||||
serde_json::Value::Object(masked)
|
||||
}
|
||||
|
||||
fn endpoint_timestamp_or_now(value: Option<u64>, now_unix_secs: u64) -> serde_json::Value {
|
||||
unix_secs_to_rfc3339(value.unwrap_or(now_unix_secs))
|
||||
.map(serde_json::Value::String)
|
||||
.unwrap_or(serde_json::Value::Null)
|
||||
admin_provider_endpoints_pure::build_admin_provider_endpoint_response(
|
||||
endpoint,
|
||||
provider_name,
|
||||
total_keys,
|
||||
active_keys,
|
||||
now_unix_secs,
|
||||
)
|
||||
}
|
||||
|
||||
fn default_admin_endpoint_max_retries() -> i32 {
|
||||
@@ -103,44 +41,44 @@ fn default_admin_endpoint_max_retries() -> i32 {
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(super) struct AdminProviderEndpointCreateRequest {
|
||||
pub(super) provider_id: String,
|
||||
pub(super) api_format: String,
|
||||
pub(super) base_url: String,
|
||||
pub(crate) struct AdminProviderEndpointCreateRequest {
|
||||
pub(crate) provider_id: String,
|
||||
pub(crate) api_format: String,
|
||||
pub(crate) base_url: String,
|
||||
#[serde(default)]
|
||||
pub(super) custom_path: Option<String>,
|
||||
pub(crate) custom_path: Option<String>,
|
||||
#[serde(default)]
|
||||
pub(super) header_rules: Option<serde_json::Value>,
|
||||
pub(crate) header_rules: Option<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
pub(super) body_rules: Option<serde_json::Value>,
|
||||
pub(crate) body_rules: Option<serde_json::Value>,
|
||||
#[serde(default = "default_admin_endpoint_max_retries")]
|
||||
pub(super) max_retries: i32,
|
||||
pub(crate) max_retries: i32,
|
||||
#[serde(default)]
|
||||
pub(super) config: Option<serde_json::Value>,
|
||||
pub(crate) config: Option<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
pub(super) proxy: Option<serde_json::Value>,
|
||||
pub(crate) proxy: Option<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
pub(super) format_acceptance_config: Option<serde_json::Value>,
|
||||
pub(crate) format_acceptance_config: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(super) struct AdminProviderEndpointUpdateRequest {
|
||||
pub(crate) struct AdminProviderEndpointUpdateRequest {
|
||||
#[serde(default)]
|
||||
pub(super) base_url: Option<String>,
|
||||
pub(crate) base_url: Option<String>,
|
||||
#[serde(default)]
|
||||
pub(super) custom_path: Option<String>,
|
||||
pub(crate) custom_path: Option<String>,
|
||||
#[serde(default)]
|
||||
pub(super) header_rules: Option<serde_json::Value>,
|
||||
pub(crate) header_rules: Option<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
pub(super) body_rules: Option<serde_json::Value>,
|
||||
pub(crate) body_rules: Option<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
pub(super) max_retries: Option<i32>,
|
||||
pub(crate) max_retries: Option<i32>,
|
||||
#[serde(default)]
|
||||
pub(super) is_active: Option<bool>,
|
||||
pub(crate) is_active: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub(super) config: Option<serde_json::Value>,
|
||||
pub(crate) config: Option<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
pub(super) proxy: Option<serde_json::Value>,
|
||||
pub(crate) proxy: Option<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
pub(super) format_acceptance_config: Option<serde_json::Value>,
|
||||
pub(crate) format_acceptance_config: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
use crate::handlers::admin::request::AdminAppState;
|
||||
use aether_data_contracts::repository::provider_catalog::{
|
||||
StoredProviderCatalogEndpoint, StoredProviderCatalogProvider,
|
||||
};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use super::payloads::{build_admin_provider_endpoint_response, endpoint_key_counts_by_format};
|
||||
|
||||
pub(crate) async fn build_admin_provider_endpoints_payload(
|
||||
state: &AdminAppState<'_>,
|
||||
provider_id: &str,
|
||||
skip: usize,
|
||||
limit: usize,
|
||||
) -> Option<serde_json::Value> {
|
||||
if !state.has_provider_catalog_data_reader() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let provider = state
|
||||
.read_provider_catalog_providers_by_ids(&[provider_id.to_string()])
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|mut providers| providers.drain(..).next())?;
|
||||
let mut endpoints = state
|
||||
.list_provider_catalog_endpoints_by_provider_ids(std::slice::from_ref(&provider.id))
|
||||
.await
|
||||
.ok()
|
||||
.unwrap_or_default();
|
||||
endpoints.sort_by(|left, right| {
|
||||
right
|
||||
.created_at_unix_secs
|
||||
.unwrap_or_default()
|
||||
.cmp(&left.created_at_unix_secs.unwrap_or_default())
|
||||
.then_with(|| left.id.cmp(&right.id))
|
||||
});
|
||||
let keys = state
|
||||
.list_provider_catalog_keys_by_provider_ids(std::slice::from_ref(&provider.id))
|
||||
.await
|
||||
.ok()
|
||||
.unwrap_or_default();
|
||||
let (total_keys_by_format, active_keys_by_format) = endpoint_key_counts_by_format(&keys);
|
||||
let now_unix_secs = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.ok()
|
||||
.map(|duration| duration.as_secs())
|
||||
.unwrap_or(0);
|
||||
|
||||
Some(serde_json::Value::Array(
|
||||
endpoints
|
||||
.into_iter()
|
||||
.skip(skip)
|
||||
.take(limit)
|
||||
.map(|endpoint| {
|
||||
build_admin_provider_endpoint_response(
|
||||
&endpoint,
|
||||
&provider.name,
|
||||
total_keys_by_format
|
||||
.get(endpoint.api_format.as_str())
|
||||
.copied()
|
||||
.unwrap_or(0),
|
||||
active_keys_by_format
|
||||
.get(endpoint.api_format.as_str())
|
||||
.copied()
|
||||
.unwrap_or(0),
|
||||
now_unix_secs,
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) async fn build_admin_endpoint_payload(
|
||||
state: &AdminAppState<'_>,
|
||||
endpoint_id: &str,
|
||||
) -> Option<serde_json::Value> {
|
||||
if !state.has_provider_catalog_data_reader() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let endpoint = state
|
||||
.read_provider_catalog_endpoints_by_ids(&[endpoint_id.to_string()])
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|mut endpoints| endpoints.drain(..).next())?;
|
||||
let provider = state
|
||||
.read_provider_catalog_providers_by_ids(std::slice::from_ref(&endpoint.provider_id))
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|mut providers| providers.drain(..).next())?;
|
||||
let keys = state
|
||||
.list_provider_catalog_keys_by_provider_ids(std::slice::from_ref(&endpoint.provider_id))
|
||||
.await
|
||||
.ok()
|
||||
.unwrap_or_default();
|
||||
let (total_keys_by_format, active_keys_by_format) = endpoint_key_counts_by_format(&keys);
|
||||
let now_unix_secs = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.ok()
|
||||
.map(|duration| duration.as_secs())
|
||||
.unwrap_or(0);
|
||||
|
||||
Some(build_admin_provider_endpoint_response(
|
||||
&endpoint,
|
||||
&provider.name,
|
||||
total_keys_by_format
|
||||
.get(endpoint.api_format.as_str())
|
||||
.copied()
|
||||
.unwrap_or(0),
|
||||
active_keys_by_format
|
||||
.get(endpoint.api_format.as_str())
|
||||
.copied()
|
||||
.unwrap_or(0),
|
||||
now_unix_secs,
|
||||
))
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
use super::builders::build_admin_update_provider_endpoint_record;
|
||||
use super::extractors::admin_endpoint_id;
|
||||
use super::payloads::{
|
||||
build_admin_provider_endpoint_response, endpoint_key_counts_by_format,
|
||||
AdminProviderEndpointUpdateRequest,
|
||||
};
|
||||
use super::support::build_admin_endpoints_data_unavailable_response;
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::{AppState, GatewayError};
|
||||
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
|
||||
use crate::GatewayError;
|
||||
use axum::{
|
||||
body::{Body, Bytes},
|
||||
http,
|
||||
@@ -17,20 +16,18 @@ use serde_json::json;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
pub(super) async fn maybe_handle(
|
||||
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("endpoints_manage")
|
||||
|| decision.route_kind.as_deref() != Some("update_endpoint")
|
||||
|| request_context.request_method != http::Method::PUT
|
||||
|| !request_context
|
||||
.request_path
|
||||
.starts_with("/api/admin/endpoints/")
|
||||
|| request_context.method() != http::Method::PUT
|
||||
|| !request_context.path().starts_with("/api/admin/endpoints/")
|
||||
{
|
||||
return Ok(None);
|
||||
}
|
||||
@@ -39,7 +36,7 @@ pub(super) async fn maybe_handle(
|
||||
return Ok(Some(build_admin_endpoints_data_unavailable_response()));
|
||||
}
|
||||
|
||||
let Some(endpoint_id) = admin_endpoint_id(&request_context.request_path) else {
|
||||
let Some(endpoint_id) = admin_endpoint_id(request_context.path()) else {
|
||||
return Ok(Some(
|
||||
(
|
||||
http::StatusCode::NOT_FOUND,
|
||||
@@ -120,14 +117,14 @@ pub(super) async fn maybe_handle(
|
||||
.into_response(),
|
||||
));
|
||||
};
|
||||
let updated_record = match build_admin_update_provider_endpoint_record(
|
||||
state,
|
||||
&provider,
|
||||
&existing_endpoint,
|
||||
&raw_payload,
|
||||
payload,
|
||||
)
|
||||
.await
|
||||
let updated_record = match state
|
||||
.build_admin_update_provider_endpoint_record(
|
||||
&provider,
|
||||
&existing_endpoint,
|
||||
&raw_payload,
|
||||
payload,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(record) => record,
|
||||
Err(detail) => {
|
||||
|
||||
Reference in New Issue
Block a user