mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 09:50:21 +08:00
refactor: 移除 Python upstream 依赖,清理全部 legacy/Python 兼容层
- 移除 upstream_base_url 参数及 AETHER_GATEWAY_UPSTREAM 环境变量,gateway 不再需要指向 Python 宿主 - 删除所有 LEGACY_*/PYTHON_* 常量、路由组、header 定义及 sunset/phaseout 机制 - 将 legacy_gateway_bridge 重命名为 internal_gateway,executor 相关命名统一为 execution_runtime - dev.sh 新增 Postgres/Redis 预检查,移除 upstream 相关启动参数和提示 - 新增 ai_public 路由处理器 - 全量适配 handler、test、state、control 等模块的命名和接口变更
This commit is contained in:
545
apps/aether-gateway/src/handlers/public/ai_public.rs
Normal file
545
apps/aether-gateway/src/handlers/public/ai_public.rs
Normal file
@@ -0,0 +1,545 @@
|
||||
use crate::gateway::async_task::CancelVideoTaskError;
|
||||
use crate::gateway::{AppState, GatewayControlDecision, GatewayError, GatewayPublicRequestContext};
|
||||
use aether_data::repository::video_tasks::{
|
||||
StoredVideoTask, VideoTaskQueryFilter, VideoTaskStatus,
|
||||
};
|
||||
use axum::body::{Body, Bytes};
|
||||
use axum::http::{self, Response};
|
||||
use axum::response::IntoResponse;
|
||||
use axum::Json;
|
||||
use serde_json::json;
|
||||
|
||||
const CLAUDE_COUNT_TOKENS_INVALID_PAYLOAD_DETAIL: &str = "Invalid token count payload";
|
||||
const CLAUDE_COUNT_TOKENS_MISSING_BODY_DETAIL: &str = "请求体不能为空";
|
||||
const GEMINI_VIDEO_TASK_NOT_FOUND_DETAIL: &str = "Video task not found";
|
||||
const AI_PUBLIC_METHOD_NOT_ALLOWED_DETAIL: &str = "Method not allowed";
|
||||
const AI_PUBLIC_UNAUTHORIZED_DETAIL: &str = "Unauthorized";
|
||||
|
||||
pub(crate) fn ai_public_local_requires_buffered_body(
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
) -> bool {
|
||||
request_context
|
||||
.control_decision
|
||||
.as_ref()
|
||||
.is_some_and(|decision| {
|
||||
decision.route_class.as_deref() == Some("ai_public")
|
||||
&& decision.route_family.as_deref() == Some("claude")
|
||||
&& decision.route_kind.as_deref() == Some("count_tokens")
|
||||
&& request_context.request_method == http::Method::POST
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn maybe_build_local_ai_public_response(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
request_body: Option<&Bytes>,
|
||||
) -> Option<Response<Body>> {
|
||||
if let Some(response) = maybe_build_local_ai_public_route_guard_response(request_context) {
|
||||
return Some(response);
|
||||
}
|
||||
|
||||
let decision = request_context.control_decision.as_ref()?;
|
||||
if decision.route_class.as_deref() != Some("ai_public") {
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some(response) =
|
||||
maybe_build_local_claude_count_tokens_response(request_context, request_body)
|
||||
{
|
||||
return Some(response);
|
||||
}
|
||||
|
||||
maybe_build_local_gemini_video_operations_response(state, request_context, decision).await
|
||||
}
|
||||
|
||||
fn maybe_build_local_ai_public_route_guard_response(
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
) -> Option<Response<Body>> {
|
||||
if request_context.request_path == "/upload/v1beta/files"
|
||||
&& request_context.request_method != http::Method::POST
|
||||
{
|
||||
return Some(build_ai_public_error_response(
|
||||
http::StatusCode::METHOD_NOT_ALLOWED,
|
||||
AI_PUBLIC_METHOD_NOT_ALLOWED_DETAIL,
|
||||
));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn maybe_build_local_claude_count_tokens_response(
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
request_body: Option<&Bytes>,
|
||||
) -> Option<Response<Body>> {
|
||||
let decision = request_context.control_decision.as_ref()?;
|
||||
if decision.route_family.as_deref() != Some("claude")
|
||||
|| decision.route_kind.as_deref() != Some("count_tokens")
|
||||
|| request_context.request_method != http::Method::POST
|
||||
|| request_context.request_path != "/v1/messages/count_tokens"
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let Some(request_body) = request_body else {
|
||||
return Some(build_ai_public_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
CLAUDE_COUNT_TOKENS_MISSING_BODY_DETAIL,
|
||||
));
|
||||
};
|
||||
|
||||
let payload = match serde_json::from_slice::<serde_json::Value>(request_body) {
|
||||
Ok(payload) => payload,
|
||||
Err(_) => {
|
||||
return Some(build_ai_public_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
CLAUDE_COUNT_TOKENS_INVALID_PAYLOAD_DETAIL,
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let input_tokens = match estimate_claude_count_tokens(&payload) {
|
||||
Ok(tokens) => tokens,
|
||||
Err(_) => {
|
||||
return Some(build_ai_public_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
CLAUDE_COUNT_TOKENS_INVALID_PAYLOAD_DETAIL,
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
Some(Json(json!({ "input_tokens": input_tokens })).into_response())
|
||||
}
|
||||
|
||||
async fn maybe_build_local_gemini_video_operations_response(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
decision: &GatewayControlDecision,
|
||||
) -> Option<Response<Body>> {
|
||||
if decision.route_family.as_deref() != Some("gemini")
|
||||
|| decision.route_kind.as_deref() != Some("video")
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
if request_context.request_path == "/v1beta/operations" {
|
||||
return Some(match request_context.request_method {
|
||||
http::Method::GET => {
|
||||
build_local_gemini_video_operations_list_response(state, decision).await
|
||||
}
|
||||
_ => build_ai_public_error_response(
|
||||
http::StatusCode::METHOD_NOT_ALLOWED,
|
||||
AI_PUBLIC_METHOD_NOT_ALLOWED_DETAIL,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
let Some(operation_path) = request_context
|
||||
.request_path
|
||||
.strip_prefix("/v1beta/operations/")
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
|
||||
Some(match request_context.request_method {
|
||||
http::Method::GET => {
|
||||
build_local_gemini_video_operation_detail_response(state, decision, operation_path)
|
||||
.await
|
||||
}
|
||||
http::Method::POST if operation_path.ends_with(":cancel") => {
|
||||
build_local_gemini_video_operation_cancel_response(state, decision, operation_path)
|
||||
.await
|
||||
}
|
||||
_ => build_ai_public_error_response(
|
||||
http::StatusCode::METHOD_NOT_ALLOWED,
|
||||
AI_PUBLIC_METHOD_NOT_ALLOWED_DETAIL,
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
async fn build_local_gemini_video_operations_list_response(
|
||||
state: &AppState,
|
||||
decision: &GatewayControlDecision,
|
||||
) -> Response<Body> {
|
||||
let Some(user_id) = decision
|
||||
.auth_context
|
||||
.as_ref()
|
||||
.map(|auth_context| auth_context.user_id.trim())
|
||||
.filter(|value| !value.is_empty())
|
||||
else {
|
||||
return build_ai_public_error_response(
|
||||
http::StatusCode::UNAUTHORIZED,
|
||||
AI_PUBLIC_UNAUTHORIZED_DETAIL,
|
||||
);
|
||||
};
|
||||
|
||||
let filter = VideoTaskQueryFilter {
|
||||
user_id: Some(user_id.to_string()),
|
||||
status: None,
|
||||
model_substring: None,
|
||||
client_api_format: Some("gemini:video".to_string()),
|
||||
};
|
||||
let tasks = match state.list_video_task_page(&filter, 0, 100).await {
|
||||
Ok(tasks) => tasks,
|
||||
Err(err) => {
|
||||
return build_ai_public_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("{err:?}"),
|
||||
);
|
||||
}
|
||||
};
|
||||
let operations = tasks
|
||||
.into_iter()
|
||||
.filter(is_gemini_video_task)
|
||||
.map(|task| build_gemini_video_operation_payload(&task))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Json(json!({ "operations": operations })).into_response()
|
||||
}
|
||||
|
||||
async fn build_local_gemini_video_operation_detail_response(
|
||||
state: &AppState,
|
||||
decision: &GatewayControlDecision,
|
||||
operation_path: &str,
|
||||
) -> Response<Body> {
|
||||
let task =
|
||||
match find_user_gemini_video_task_for_operation(state, decision, operation_path).await {
|
||||
Ok(Some(task)) => task,
|
||||
Ok(None) => {
|
||||
return build_ai_public_error_response(
|
||||
http::StatusCode::NOT_FOUND,
|
||||
GEMINI_VIDEO_TASK_NOT_FOUND_DETAIL,
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
return build_ai_public_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("{err:?}"),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
Json(build_gemini_video_operation_payload(&task)).into_response()
|
||||
}
|
||||
|
||||
async fn build_local_gemini_video_operation_cancel_response(
|
||||
state: &AppState,
|
||||
decision: &GatewayControlDecision,
|
||||
operation_path: &str,
|
||||
) -> Response<Body> {
|
||||
let task =
|
||||
match find_user_gemini_video_task_for_operation(state, decision, operation_path).await {
|
||||
Ok(Some(task)) => task,
|
||||
Ok(None) => {
|
||||
return build_ai_public_error_response(
|
||||
http::StatusCode::NOT_FOUND,
|
||||
GEMINI_VIDEO_TASK_NOT_FOUND_DETAIL,
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
return build_ai_public_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("{err:?}"),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
match crate::gateway::async_task::cancel_video_task_record(state, &task.id).await {
|
||||
Ok(_) => Json(json!({})).into_response(),
|
||||
Err(CancelVideoTaskError::NotFound) => build_ai_public_error_response(
|
||||
http::StatusCode::NOT_FOUND,
|
||||
GEMINI_VIDEO_TASK_NOT_FOUND_DETAIL,
|
||||
),
|
||||
Err(CancelVideoTaskError::InvalidStatus(status)) => build_ai_public_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
format!(
|
||||
"Cannot cancel task with status: {}",
|
||||
video_task_status_name(status)
|
||||
),
|
||||
),
|
||||
Err(CancelVideoTaskError::Response(response)) => response,
|
||||
Err(CancelVideoTaskError::Gateway(err)) => build_ai_public_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("{err:?}"),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
async fn find_user_gemini_video_task_for_operation(
|
||||
state: &AppState,
|
||||
decision: &GatewayControlDecision,
|
||||
operation_path: &str,
|
||||
) -> Result<Option<StoredVideoTask>, GatewayError> {
|
||||
let Some(user_id) = decision
|
||||
.auth_context
|
||||
.as_ref()
|
||||
.map(|auth_context| auth_context.user_id.trim())
|
||||
.filter(|value| !value.is_empty())
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
let Some(short_id) = extract_short_id_from_gemini_operation_path(operation_path) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let Some(task) = state.find_video_task_by_short_id(short_id).await? else {
|
||||
return Ok(None);
|
||||
};
|
||||
if task.user_id.as_deref().map(str::trim) != Some(user_id) || !is_gemini_video_task(&task) {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(Some(task))
|
||||
}
|
||||
|
||||
fn extract_short_id_from_gemini_operation_path(operation_path: &str) -> Option<&str> {
|
||||
let trimmed = operation_path.trim_matches('/');
|
||||
if trimmed.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let short_id = trimmed
|
||||
.strip_suffix(":cancel")
|
||||
.unwrap_or(trimmed)
|
||||
.rsplit('/')
|
||||
.next()?;
|
||||
(!short_id.is_empty()).then_some(short_id)
|
||||
}
|
||||
|
||||
fn is_gemini_video_task(task: &StoredVideoTask) -> bool {
|
||||
matches!(
|
||||
task.provider_api_format
|
||||
.as_deref()
|
||||
.or(task.client_api_format.as_deref())
|
||||
.map(str::trim),
|
||||
Some("gemini:video")
|
||||
)
|
||||
}
|
||||
|
||||
fn build_gemini_video_operation_payload(task: &StoredVideoTask) -> serde_json::Value {
|
||||
match task.status {
|
||||
VideoTaskStatus::Completed => json!({
|
||||
"name": gemini_video_operation_name(task),
|
||||
"done": true,
|
||||
"response": {
|
||||
"generateVideoResponse": {
|
||||
"generatedSamples": [
|
||||
{
|
||||
"video": {
|
||||
"uri": format!(
|
||||
"/v1beta/files/aev_{}:download?alt=media",
|
||||
gemini_operation_short_id(task)
|
||||
),
|
||||
"mimeType": "video/mp4",
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}),
|
||||
VideoTaskStatus::Failed | VideoTaskStatus::Expired => json!({
|
||||
"name": gemini_video_operation_name(task),
|
||||
"done": true,
|
||||
"error": {
|
||||
"code": task.error_code.clone().unwrap_or_else(|| "UNKNOWN".to_string()),
|
||||
"message": task
|
||||
.error_message
|
||||
.clone()
|
||||
.unwrap_or_else(|| "Video generation failed".to_string()),
|
||||
}
|
||||
}),
|
||||
_ => json!({
|
||||
"name": gemini_video_operation_name(task),
|
||||
"done": false,
|
||||
"metadata": gemini_video_operation_metadata(task),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn gemini_video_operation_name(task: &StoredVideoTask) -> String {
|
||||
format!(
|
||||
"models/{}/operations/{}",
|
||||
gemini_operation_model(task),
|
||||
gemini_operation_short_id(task)
|
||||
)
|
||||
}
|
||||
|
||||
fn gemini_operation_model(task: &StoredVideoTask) -> String {
|
||||
task.model
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
.or_else(|| {
|
||||
task.external_task_id.as_deref().and_then(|external_id| {
|
||||
let parts = external_id.split('/').collect::<Vec<_>>();
|
||||
if parts.len() >= 2 && parts[0] == "models" && !parts[1].trim().is_empty() {
|
||||
Some(parts[1].trim().to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|| "unknown".to_string())
|
||||
}
|
||||
|
||||
fn gemini_operation_short_id(task: &StoredVideoTask) -> String {
|
||||
task.short_id
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or(task.id.as_str())
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn gemini_video_operation_metadata(task: &StoredVideoTask) -> serde_json::Value {
|
||||
task.request_metadata
|
||||
.as_ref()
|
||||
.and_then(|metadata| metadata.get("rust_local_snapshot"))
|
||||
.and_then(|snapshot| snapshot.get("Gemini"))
|
||||
.and_then(|gemini| gemini.get("metadata"))
|
||||
.cloned()
|
||||
.unwrap_or_else(|| json!({}))
|
||||
}
|
||||
|
||||
fn video_task_status_name(status: VideoTaskStatus) -> &'static str {
|
||||
match status {
|
||||
VideoTaskStatus::Pending => "pending",
|
||||
VideoTaskStatus::Submitted => "submitted",
|
||||
VideoTaskStatus::Queued => "queued",
|
||||
VideoTaskStatus::Processing => "processing",
|
||||
VideoTaskStatus::Completed => "completed",
|
||||
VideoTaskStatus::Failed => "failed",
|
||||
VideoTaskStatus::Cancelled => "cancelled",
|
||||
VideoTaskStatus::Expired => "expired",
|
||||
VideoTaskStatus::Deleted => "deleted",
|
||||
}
|
||||
}
|
||||
|
||||
fn build_ai_public_error_response(
|
||||
status: http::StatusCode,
|
||||
detail: impl Into<String>,
|
||||
) -> Response<Body> {
|
||||
(status, Json(json!({ "detail": detail.into() }))).into_response()
|
||||
}
|
||||
|
||||
fn estimate_claude_count_tokens(payload: &serde_json::Value) -> Result<u64, ()> {
|
||||
let object = payload.as_object().ok_or(())?;
|
||||
let model = object
|
||||
.get("model")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.ok_or(())?;
|
||||
if model.trim().is_empty() {
|
||||
return Err(());
|
||||
}
|
||||
|
||||
let messages = object
|
||||
.get("messages")
|
||||
.and_then(serde_json::Value::as_array)
|
||||
.ok_or(())?;
|
||||
|
||||
let system_tokens = estimate_claude_system_tokens(object.get("system"))?;
|
||||
let message_tokens = estimate_claude_message_tokens(messages)?;
|
||||
Ok(system_tokens.saturating_add(message_tokens))
|
||||
}
|
||||
|
||||
fn estimate_claude_system_tokens(system: Option<&serde_json::Value>) -> Result<u64, ()> {
|
||||
let Some(system) = system else {
|
||||
return Ok(0);
|
||||
};
|
||||
|
||||
match system {
|
||||
serde_json::Value::Null => Ok(0),
|
||||
serde_json::Value::String(text) => Ok(estimate_text_tokens(text)),
|
||||
serde_json::Value::Array(blocks) => {
|
||||
let mut total = 0_u64;
|
||||
for block in blocks {
|
||||
let block = block.as_object().ok_or(())?;
|
||||
if let Some(text) = block.get("text").and_then(serde_json::Value::as_str) {
|
||||
total = total.saturating_add(estimate_text_tokens(text));
|
||||
}
|
||||
}
|
||||
Ok(total)
|
||||
}
|
||||
serde_json::Value::Object(_) => Ok(0),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
|
||||
fn estimate_claude_message_tokens(messages: &[serde_json::Value]) -> Result<u64, ()> {
|
||||
let mut total = 0_u64;
|
||||
|
||||
for message in messages {
|
||||
let message = message.as_object().ok_or(())?;
|
||||
let role = message
|
||||
.get("role")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.ok_or(())?;
|
||||
if !matches!(role, "user" | "assistant") {
|
||||
return Err(());
|
||||
}
|
||||
|
||||
total = total.saturating_add(4);
|
||||
let content = message.get("content").ok_or(())?;
|
||||
match content {
|
||||
serde_json::Value::String(text) => {
|
||||
total = total.saturating_add(estimate_text_tokens(text));
|
||||
}
|
||||
serde_json::Value::Array(items) => {
|
||||
for item in items {
|
||||
let item = item.as_object().ok_or(())?;
|
||||
if let Some(text) = item.get("text").and_then(serde_json::Value::as_str) {
|
||||
total = total.saturating_add(estimate_text_tokens(text));
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => return Err(()),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(total)
|
||||
}
|
||||
|
||||
fn estimate_text_tokens(text: &str) -> u64 {
|
||||
if text.is_empty() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let char_count = text.chars().count() as u64;
|
||||
std::cmp::max(1, char_count / 4)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::estimate_claude_count_tokens;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn estimates_claude_count_tokens_from_system_and_messages() {
|
||||
let payload = json!({
|
||||
"model": "claude-sonnet-4-5",
|
||||
"system": [{"type": "text", "text": "abcdefghijklmnop"}],
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "abcdefghijkl"
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{"type": "text", "text": "abcdefgh"},
|
||||
{"type": "tool_use", "name": "ignored", "input": {"city": "SF"}}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
assert_eq!(estimate_claude_count_tokens(&payload), Ok(17));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_claude_count_tokens_payload() {
|
||||
let payload = json!({
|
||||
"model": "claude-sonnet-4-5",
|
||||
"messages": [{"role": "system", "content": "bad"}]
|
||||
});
|
||||
|
||||
assert_eq!(estimate_claude_count_tokens(&payload), Err(()));
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,15 @@
|
||||
use super::*;
|
||||
use crate::gateway::api::ai::public_api_format_local_path;
|
||||
use crate::gateway::handlers::{
|
||||
query_param_optional_bool, query_param_value, unix_secs_to_rfc3339,
|
||||
};
|
||||
use crate::gateway::AppState;
|
||||
use aether_data::repository::candidates::{
|
||||
PublicHealthTimelineBucket, RequestCandidateStatus, StoredRequestCandidate,
|
||||
};
|
||||
use aether_data::repository::provider_catalog::StoredProviderCatalogKey;
|
||||
use serde_json::json;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
pub(crate) fn request_candidate_status_label(status: RequestCandidateStatus) -> &'static str {
|
||||
match status {
|
||||
|
||||
@@ -1,9 +1,40 @@
|
||||
pub(crate) use super::*;
|
||||
|
||||
mod ai_public;
|
||||
mod catalog_helpers;
|
||||
mod support;
|
||||
mod system_modules_helpers;
|
||||
|
||||
pub(crate) use self::catalog_helpers::*;
|
||||
pub(crate) use self::support::*;
|
||||
pub(crate) use self::system_modules_helpers::*;
|
||||
pub(crate) use self::ai_public::{
|
||||
ai_public_local_requires_buffered_body, maybe_build_local_ai_public_response,
|
||||
};
|
||||
pub(crate) use self::catalog_helpers::{
|
||||
admin_requested_force_stream, api_format_display_name, build_api_format_health_monitor_payload,
|
||||
build_public_catalog_models_payload, build_public_catalog_search_models_payload,
|
||||
build_public_health_timeline, build_public_providers_payload, normalize_admin_base_url,
|
||||
provider_key_api_formats, request_candidate_event_unix_secs, request_candidate_status_label,
|
||||
ApiFormatHealthMonitorOptions,
|
||||
};
|
||||
pub(crate) use self::system_modules_helpers::{
|
||||
admin_module_by_name, admin_module_name_from_enabled_path, admin_module_name_from_status_path,
|
||||
apply_admin_email_template_update, apply_admin_system_config_update,
|
||||
apply_admin_system_settings_update, build_admin_api_formats_payload,
|
||||
build_admin_email_template_payload, build_admin_email_templates_payload,
|
||||
build_admin_keys_grouped_by_format_payload, build_admin_module_runtime_state,
|
||||
build_admin_module_status_payload, build_admin_module_validation_result,
|
||||
build_admin_modules_status_payload, build_admin_system_check_update_payload,
|
||||
build_admin_system_config_detail_payload, build_admin_system_config_export_payload,
|
||||
build_admin_system_configs_payload, build_admin_system_settings_payload,
|
||||
build_admin_system_stats_payload, build_admin_system_users_export_payload,
|
||||
build_public_auth_modules_status_payload, capability_detail_by_name, current_aether_version,
|
||||
delete_admin_system_config, enabled_key_capability_short_names,
|
||||
escape_admin_email_template_html, ldap_module_config_is_valid, module_available_from_env,
|
||||
preview_admin_email_template, read_admin_email_template_payload,
|
||||
render_admin_email_template_html, reset_admin_email_template,
|
||||
serialize_admin_system_users_export_wallet, serialize_public_capability,
|
||||
supported_capability_names, system_config_bool, system_config_string,
|
||||
AdminSetModuleEnabledRequest, PUBLIC_CAPABILITY_DEFINITIONS,
|
||||
};
|
||||
|
||||
pub(crate) use self::support::{
|
||||
build_unhandled_public_support_response, matches_model_mapping_for_models,
|
||||
maybe_build_local_admin_announcements_response, maybe_build_local_public_support_response,
|
||||
};
|
||||
|
||||
@@ -1,4 +1,23 @@
|
||||
pub(crate) use super::*;
|
||||
use super::{
|
||||
build_api_format_health_monitor_payload, build_public_auth_modules_status_payload,
|
||||
build_public_catalog_models_payload, build_public_catalog_search_models_payload,
|
||||
build_public_providers_payload, capability_detail_by_name, escape_admin_email_template_html,
|
||||
ldap_module_config_is_valid, module_available_from_env, read_admin_email_template_payload,
|
||||
render_admin_email_template_html, serialize_public_capability, supported_capability_names,
|
||||
system_config_bool, system_config_string, ApiFormatHealthMonitorOptions,
|
||||
PUBLIC_CAPABILITY_DEFINITIONS,
|
||||
};
|
||||
use crate::gateway::handlers::{
|
||||
decrypt_catalog_secret_with_fallbacks, encrypt_catalog_secret_with_fallbacks, query_param_bool,
|
||||
query_param_optional_bool, query_param_value, unix_secs_to_rfc3339,
|
||||
};
|
||||
use crate::gateway::{AppState, GatewayError, GatewayPublicRequestContext};
|
||||
use axum::body::{Body, Bytes};
|
||||
use axum::http::{self, Response};
|
||||
use axum::response::IntoResponse;
|
||||
use axum::Json;
|
||||
use serde_json::json;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
#[path = "support/announcements.rs"]
|
||||
mod support_announcements;
|
||||
@@ -34,25 +53,36 @@ use self::support_auth::auth_session::{
|
||||
};
|
||||
use self::support_auth::{
|
||||
build_auth_error_response, build_auth_json_response, build_auth_registration_settings_payload,
|
||||
build_auth_settings_payload, maybe_build_local_auth_legacy_response,
|
||||
build_auth_settings_payload, maybe_build_local_auth_response,
|
||||
};
|
||||
use self::support_dashboard::maybe_build_local_dashboard_legacy_response;
|
||||
use self::support_dashboard::maybe_build_local_dashboard_response;
|
||||
use self::support_models::{
|
||||
build_models_auth_error_response, maybe_build_local_models_response, models_api_format,
|
||||
};
|
||||
use self::support_monitoring::maybe_build_local_user_monitoring_response;
|
||||
use self::support_payment::maybe_build_local_payment_callback_response;
|
||||
use self::support_test_connection::maybe_build_local_test_connection_response;
|
||||
use self::support_user_me::maybe_build_local_users_me_legacy_response;
|
||||
use self::support_user_me::maybe_build_local_users_me_response;
|
||||
use self::support_wallet::{
|
||||
maybe_build_local_wallet_legacy_response, sanitize_wallet_gateway_response,
|
||||
maybe_build_local_wallet_response, sanitize_wallet_gateway_response,
|
||||
wallet_normalize_optional_string_field, wallet_payment_order_payload_from_row,
|
||||
};
|
||||
|
||||
fn build_public_support_maintenance_response(detail: &str) -> Response<Body> {
|
||||
pub(crate) fn build_unhandled_public_support_response(
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
) -> Response<Body> {
|
||||
let decision = request_context
|
||||
.control_decision
|
||||
.as_ref()
|
||||
.expect("public support response requires control decision");
|
||||
(
|
||||
http::StatusCode::SERVICE_UNAVAILABLE,
|
||||
Json(json!({ "detail": detail })),
|
||||
http::StatusCode::NOT_IMPLEMENTED,
|
||||
Json(json!({
|
||||
"detail": "public support route not implemented in rust frontdoor",
|
||||
"route_family": decision.route_family,
|
||||
"route_kind": decision.route_kind,
|
||||
"request_path": request_context.request_path,
|
||||
})),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
@@ -61,58 +91,27 @@ pub(crate) async fn maybe_build_local_public_support_response(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
headers: &http::HeaderMap,
|
||||
request_body: Option<&axum::body::Bytes>,
|
||||
request_body: Option<&Bytes>,
|
||||
) -> Option<Response<Body>> {
|
||||
let decision = request_context.control_decision.as_ref()?;
|
||||
if decision.route_class.as_deref() != Some("public_support") {
|
||||
return None;
|
||||
}
|
||||
|
||||
if decision.route_family.as_deref() == Some("oauth_public_legacy") {
|
||||
return Some(
|
||||
(
|
||||
http::StatusCode::SERVICE_UNAVAILABLE,
|
||||
Json(json!({
|
||||
"detail": "OAuth public routes are retired; use Rust maintenance backend",
|
||||
})),
|
||||
)
|
||||
.into_response(),
|
||||
);
|
||||
if decision.route_family.as_deref() == Some("auth") {
|
||||
return maybe_build_local_auth_response(state, request_context, headers, request_body)
|
||||
.await;
|
||||
}
|
||||
|
||||
if decision.route_family.as_deref() == Some("oauth_user_legacy") {
|
||||
return Some(
|
||||
(
|
||||
http::StatusCode::SERVICE_UNAVAILABLE,
|
||||
Json(json!({
|
||||
"detail": "OAuth user routes are retired; use Rust maintenance backend",
|
||||
})),
|
||||
)
|
||||
.into_response(),
|
||||
);
|
||||
if decision.route_family.as_deref() == Some("dashboard") {
|
||||
return Some(maybe_build_local_dashboard_response(state, request_context, headers).await);
|
||||
}
|
||||
|
||||
if decision.route_family.as_deref() == Some("auth_legacy") {
|
||||
return maybe_build_local_auth_legacy_response(
|
||||
state,
|
||||
request_context,
|
||||
headers,
|
||||
request_body,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
if decision.route_family.as_deref() == Some("dashboard_legacy") {
|
||||
return Some(
|
||||
maybe_build_local_dashboard_legacy_response(state, request_context, headers).await,
|
||||
);
|
||||
}
|
||||
|
||||
if decision.route_family.as_deref() == Some("monitoring_user_legacy") {
|
||||
if decision.route_family.as_deref() == Some("monitoring_user") {
|
||||
return maybe_build_local_user_monitoring_response(state, request_context, headers).await;
|
||||
}
|
||||
|
||||
if decision.route_family.as_deref() == Some("announcement_user_legacy") {
|
||||
if decision.route_family.as_deref() == Some("announcement_user") {
|
||||
return maybe_build_local_announcement_user_response(
|
||||
state,
|
||||
request_context,
|
||||
@@ -122,29 +121,21 @@ pub(crate) async fn maybe_build_local_public_support_response(
|
||||
.await;
|
||||
}
|
||||
|
||||
if decision.route_family.as_deref() == Some("wallet_legacy") {
|
||||
if decision.route_family.as_deref() == Some("wallet") {
|
||||
if let Some(response) =
|
||||
maybe_build_local_wallet_legacy_response(state, request_context, headers, request_body)
|
||||
.await
|
||||
maybe_build_local_wallet_response(state, request_context, headers, request_body).await
|
||||
{
|
||||
return Some(response);
|
||||
}
|
||||
return Some(build_public_support_maintenance_response(
|
||||
"Wallet routes require Rust maintenance backend",
|
||||
));
|
||||
return Some(build_unhandled_public_support_response(request_context));
|
||||
}
|
||||
|
||||
if decision.route_family.as_deref() == Some("users_me_legacy") {
|
||||
return maybe_build_local_users_me_legacy_response(
|
||||
state,
|
||||
request_context,
|
||||
headers,
|
||||
request_body,
|
||||
)
|
||||
.await;
|
||||
if decision.route_family.as_deref() == Some("users_me") {
|
||||
return maybe_build_local_users_me_response(state, request_context, headers, request_body)
|
||||
.await;
|
||||
}
|
||||
|
||||
if decision.route_family.as_deref() == Some("payment_callback_legacy") {
|
||||
if decision.route_family.as_deref() == Some("payment_callback") {
|
||||
return maybe_build_local_payment_callback_response(
|
||||
state,
|
||||
request_context,
|
||||
|
||||
@@ -1,8 +1,3 @@
|
||||
use super::*;
|
||||
|
||||
const ANNOUNCEMENTS_MAINTENANCE_DETAIL: &str =
|
||||
"Announcement routes require Rust maintenance backend";
|
||||
|
||||
#[path = "announcements/admin_routes.rs"]
|
||||
mod announcements_admin_routes;
|
||||
#[path = "announcements/public_routes.rs"]
|
||||
|
||||
@@ -1,11 +1,28 @@
|
||||
use axum::{
|
||||
body::Body,
|
||||
http,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::gateway::{AppState, GatewayError, GatewayPublicRequestContext};
|
||||
|
||||
use super::announcements_shared::{
|
||||
announcements_bad_request_response, announcements_not_found_response,
|
||||
build_public_announcement_payload, parse_optional_rfc3339_unix_secs,
|
||||
public_announcement_id_from_path,
|
||||
};
|
||||
use super::*;
|
||||
use aether_data::repository::announcements::{CreateAnnouncementRecord, UpdateAnnouncementRecord};
|
||||
|
||||
fn build_admin_announcement_writer_unavailable_response() -> Response<Body> {
|
||||
(
|
||||
http::StatusCode::SERVICE_UNAVAILABLE,
|
||||
Json(json!({ "detail": "公告写入暂不可用" })),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
struct AdminAnnouncementCreateRequest {
|
||||
title: String,
|
||||
@@ -51,6 +68,9 @@ pub(crate) async fn maybe_build_local_admin_announcements_response(
|
||||
"/api/announcements" | "/api/announcements/"
|
||||
) =>
|
||||
{
|
||||
if !state.has_announcement_data_writer() {
|
||||
return Ok(Some(build_admin_announcement_writer_unavailable_response()));
|
||||
}
|
||||
let Some(request_body) = request_body else {
|
||||
return Ok(Some(announcements_bad_request_response("请求体不能为空")));
|
||||
};
|
||||
@@ -75,9 +95,7 @@ pub(crate) async fn maybe_build_local_admin_announcements_response(
|
||||
})?;
|
||||
let record = build_create_record(payload, operator_id)?;
|
||||
let Some(created) = state.create_announcement(record).await? else {
|
||||
return Ok(Some(build_public_support_maintenance_response(
|
||||
super::ANNOUNCEMENTS_MAINTENANCE_DETAIL,
|
||||
)));
|
||||
return Ok(Some(build_admin_announcement_writer_unavailable_response()));
|
||||
};
|
||||
let mut response = build_public_announcement_payload(&created);
|
||||
response["message"] = json!("公告创建成功");
|
||||
@@ -89,6 +107,9 @@ pub(crate) async fn maybe_build_local_admin_announcements_response(
|
||||
else {
|
||||
return Ok(Some(announcements_not_found_response()));
|
||||
};
|
||||
if !state.has_announcement_data_writer() {
|
||||
return Ok(Some(build_admin_announcement_writer_unavailable_response()));
|
||||
}
|
||||
let Some(request_body) = request_body else {
|
||||
return Ok(Some(announcements_bad_request_response("请求体不能为空")));
|
||||
};
|
||||
@@ -104,7 +125,7 @@ pub(crate) async fn maybe_build_local_admin_announcements_response(
|
||||
let record = build_update_record(announcement_id, payload)?;
|
||||
return Ok(Some(match state.update_announcement(record).await? {
|
||||
Some(_) => Json(json!({ "message": "公告更新成功" })).into_response(),
|
||||
None => announcements_not_found_response(),
|
||||
None => build_admin_announcement_writer_unavailable_response(),
|
||||
}));
|
||||
}
|
||||
Some("delete_announcement") if request_context.request_method == http::Method::DELETE => {
|
||||
@@ -113,6 +134,9 @@ pub(crate) async fn maybe_build_local_admin_announcements_response(
|
||||
else {
|
||||
return Ok(Some(announcements_not_found_response()));
|
||||
};
|
||||
if !state.has_announcement_data_writer() {
|
||||
return Ok(Some(build_admin_announcement_writer_unavailable_response()));
|
||||
}
|
||||
if state
|
||||
.find_announcement_by_id(announcement_id)
|
||||
.await?
|
||||
@@ -124,7 +148,7 @@ pub(crate) async fn maybe_build_local_admin_announcements_response(
|
||||
return Ok(Some(if deleted {
|
||||
Json(json!({ "message": "公告已删除" })).into_response()
|
||||
} else {
|
||||
build_public_support_maintenance_response(super::ANNOUNCEMENTS_MAINTENANCE_DETAIL)
|
||||
build_admin_announcement_writer_unavailable_response()
|
||||
}));
|
||||
}
|
||||
_ => {}
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
use axum::{
|
||||
body::Body,
|
||||
http,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
|
||||
use crate::gateway::{AppState, GatewayPublicRequestContext};
|
||||
|
||||
use super::super::build_unhandled_public_support_response;
|
||||
use super::announcements_shared::{
|
||||
announcements_bad_request_response, announcements_internal_detail,
|
||||
announcements_internal_error_response, announcements_not_found_response,
|
||||
build_public_announcement_list_payload, build_public_announcement_payload,
|
||||
parse_public_announcements_query, public_announcement_id_from_path,
|
||||
};
|
||||
use super::*;
|
||||
|
||||
pub(crate) async fn maybe_build_local_public_announcements_response(
|
||||
state: &AppState,
|
||||
@@ -69,7 +78,11 @@ pub(crate) async fn maybe_build_local_public_announcements_response(
|
||||
Some(Json(build_public_announcement_list_payload(page)).into_response())
|
||||
}
|
||||
Some("detail") => {
|
||||
let announcement_id = public_announcement_id_from_path(&request_context.request_path)?;
|
||||
let Some(announcement_id) =
|
||||
public_announcement_id_from_path(&request_context.request_path)
|
||||
else {
|
||||
return Some(build_unhandled_public_support_response(request_context));
|
||||
};
|
||||
let announcement = match state.find_announcement_by_id(announcement_id).await {
|
||||
Ok(Some(value)) => value,
|
||||
Ok(None) => return Some(announcements_not_found_response()),
|
||||
@@ -81,6 +94,6 @@ pub(crate) async fn maybe_build_local_public_announcements_response(
|
||||
};
|
||||
Some(Json(build_public_announcement_payload(&announcement)).into_response())
|
||||
}
|
||||
_ => None,
|
||||
_ => Some(build_unhandled_public_support_response(request_context)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
use super::*;
|
||||
use axum::{
|
||||
body::Body,
|
||||
http,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
use aether_data::repository::announcements::{
|
||||
AnnouncementListQuery, StoredAnnouncement, StoredAnnouncementPage,
|
||||
};
|
||||
|
||||
use crate::gateway::handlers::{query_param_optional_bool, query_param_value};
|
||||
use crate::gateway::GatewayError;
|
||||
|
||||
pub(super) fn parse_public_announcements_query(
|
||||
query: Option<&str>,
|
||||
default_active_only: bool,
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
use axum::{
|
||||
body::Body,
|
||||
http,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::gateway::{AppState, GatewayPublicRequestContext};
|
||||
|
||||
use super::super::{build_unhandled_public_support_response, resolve_authenticated_local_user};
|
||||
use super::announcements_shared::{
|
||||
announcements_bad_request_response, announcements_internal_detail,
|
||||
announcements_internal_error_response, announcements_not_found_response,
|
||||
read_status_announcement_id_from_path,
|
||||
};
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
struct AnnouncementReadStatusRequest {
|
||||
@@ -29,7 +39,7 @@ pub(crate) async fn maybe_build_local_announcement_user_response(
|
||||
request_body: Option<&axum::body::Bytes>,
|
||||
) -> Option<Response<Body>> {
|
||||
let decision = request_context.control_decision.as_ref()?;
|
||||
if decision.route_family.as_deref() != Some("announcement_user_legacy") {
|
||||
if decision.route_family.as_deref() != Some("announcement_user") {
|
||||
return None;
|
||||
}
|
||||
if !state.has_announcement_data_reader() {
|
||||
@@ -91,7 +101,7 @@ pub(crate) async fn maybe_build_local_announcement_user_response(
|
||||
let announcement_id =
|
||||
match read_status_announcement_id_from_path(&request_context.request_path) {
|
||||
Some(value) => value,
|
||||
None => return None,
|
||||
None => return Some(build_unhandled_public_support_response(request_context)),
|
||||
};
|
||||
match state.find_announcement_by_id(announcement_id).await {
|
||||
Ok(Some(_)) => {}
|
||||
@@ -112,9 +122,7 @@ pub(crate) async fn maybe_build_local_announcement_user_response(
|
||||
}
|
||||
Some(Json(json!({ "message": "公告已标记为已读" })).into_response())
|
||||
}
|
||||
_ => Some(build_public_support_maintenance_response(
|
||||
super::ANNOUNCEMENTS_MAINTENANCE_DETAIL,
|
||||
)),
|
||||
_ => Some(build_unhandled_public_support_response(request_context)),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,18 @@
|
||||
use super::*;
|
||||
pub(super) use super::{
|
||||
build_unhandled_public_support_response, decrypt_catalog_secret_with_fallbacks,
|
||||
escape_admin_email_template_html, ldap_module_config_is_valid, module_available_from_env,
|
||||
read_admin_email_template_payload, render_admin_email_template_html, system_config_bool,
|
||||
system_config_string, AppState, GatewayError, GatewayPublicRequestContext,
|
||||
};
|
||||
pub(super) use axum::{
|
||||
body::Body,
|
||||
http,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
pub(super) use regex::Regex;
|
||||
use serde::Deserialize;
|
||||
pub(super) use serde_json::json;
|
||||
|
||||
#[path = "auth_helpers.rs"]
|
||||
mod auth_helpers;
|
||||
@@ -229,8 +243,10 @@ async fn handle_auth_login(
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return build_public_support_maintenance_response(
|
||||
"Non-local auth login requires Rust maintenance backend",
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"不支持的认证类型",
|
||||
false,
|
||||
)
|
||||
}
|
||||
};
|
||||
@@ -238,14 +254,14 @@ async fn handle_auth_login(
|
||||
build_auth_login_success_response(state, headers, client_device_id, user).await
|
||||
}
|
||||
|
||||
pub(super) async fn maybe_build_local_auth_legacy_response(
|
||||
pub(super) async fn maybe_build_local_auth_response(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
headers: &http::HeaderMap,
|
||||
request_body: Option<&axum::body::Bytes>,
|
||||
) -> Option<Response<Body>> {
|
||||
let decision = request_context.control_decision.as_ref()?;
|
||||
if decision.route_family.as_deref() != Some("auth_legacy") {
|
||||
if decision.route_family.as_deref() != Some("auth") {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -278,8 +294,54 @@ pub(super) async fn maybe_build_local_auth_legacy_response(
|
||||
Some("logout") if request_context.request_path == "/api/auth/logout" => {
|
||||
Some(handle_auth_logout(state, request_context, headers).await)
|
||||
}
|
||||
_ => Some(build_public_support_maintenance_response(
|
||||
"Auth routes require Rust maintenance backend",
|
||||
)),
|
||||
_ => Some(build_unhandled_public_support_response(request_context)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{maybe_build_local_auth_response, AppState, GatewayPublicRequestContext};
|
||||
use crate::gateway::GatewayControlDecision;
|
||||
use axum::body::to_bytes;
|
||||
use axum::http::{HeaderMap, Method, StatusCode, Uri};
|
||||
|
||||
fn request_context(method: Method, uri: &str, route_kind: &str) -> GatewayPublicRequestContext {
|
||||
GatewayPublicRequestContext::from_request_parts(
|
||||
"trace-auth-unhandled",
|
||||
&method,
|
||||
&uri.parse::<Uri>().expect("uri should parse"),
|
||||
&HeaderMap::new(),
|
||||
Some(GatewayControlDecision::synthetic(
|
||||
uri,
|
||||
Some("public_support".to_string()),
|
||||
Some("auth".to_string()),
|
||||
Some(route_kind.to_string()),
|
||||
Some("user:auth".to_string()),
|
||||
)),
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn auth_unhandled_route_returns_local_not_implemented_response() {
|
||||
let state = AppState::new().expect("gateway should build");
|
||||
let request_context = request_context(Method::POST, "/api/auth/login/history", "login");
|
||||
let response =
|
||||
maybe_build_local_auth_response(&state, &request_context, &HeaderMap::new(), None)
|
||||
.await
|
||||
.expect("auth handler should return response");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::NOT_IMPLEMENTED);
|
||||
let body = to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("body should read");
|
||||
let payload: serde_json::Value =
|
||||
serde_json::from_slice(&body).expect("json body should parse");
|
||||
assert_eq!(
|
||||
payload["detail"],
|
||||
"public support route not implemented in rust frontdoor"
|
||||
);
|
||||
assert_eq!(payload["route_family"], "auth");
|
||||
assert_eq!(payload["route_kind"], "login");
|
||||
assert_eq!(payload["request_path"], "/api/auth/login/history");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
use super::*;
|
||||
use super::{
|
||||
decrypt_catalog_secret_with_fallbacks, escape_admin_email_template_html, json,
|
||||
read_admin_email_template_payload, render_admin_email_template_html, system_config_bool,
|
||||
system_config_string, system_config_u16, AppState, GatewayError,
|
||||
AUTH_EMAIL_VERIFICATION_PREFIX, AUTH_EMAIL_VERIFIED_PREFIX, AUTH_EMAIL_VERIFIED_TTL_SECS,
|
||||
AUTH_SMTP_TIMEOUT_SECS,
|
||||
};
|
||||
use base64::Engine;
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub(super) struct StoredAuthEmailVerificationCode {
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
use super::*;
|
||||
use super::{
|
||||
http, json, ldap_module_config_is_valid, module_available_from_env, system_config_bool,
|
||||
system_config_string, AppState, Body, GatewayError, GatewayPublicRequestContext, IntoResponse,
|
||||
Json, Response,
|
||||
};
|
||||
|
||||
pub(crate) async fn build_auth_registration_settings_payload(
|
||||
state: &AppState,
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use super::*;
|
||||
use super::{
|
||||
decrypt_catalog_secret_with_fallbacks, ldap_config_is_enabled, module_available_from_env,
|
||||
normalize_auth_login_identifier, system_config_bool, AppState, GatewayError,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(super) struct AuthLdapRuntimeConfig {
|
||||
|
||||
@@ -1,4 +1,15 @@
|
||||
use super::*;
|
||||
use super::{
|
||||
auth_email_is_verified, auth_now, auth_registration_email_configured,
|
||||
auth_verification_code_expire_minutes, auth_verification_send_cooldown_seconds,
|
||||
build_auth_error_response, build_auth_json_response, build_auth_verification_email,
|
||||
clear_auth_email_pending_code, clear_auth_email_verification, generate_auth_verification_code,
|
||||
http, json, mark_auth_email_verified, read_auth_email_verification_code, read_auth_smtp_config,
|
||||
send_auth_email, store_auth_email_verification_code, system_config_bool, system_config_f64,
|
||||
system_config_string, system_config_string_list, AppState, Body, GatewayError, Regex, Response,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
|
||||
const AUTH_REGISTRATION_STORAGE_UNAVAILABLE_DETAIL: &str = "注册数据存储暂不可用";
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct AuthRegisterRequest {
|
||||
@@ -496,8 +507,10 @@ pub(super) async fn handle_auth_register(
|
||||
);
|
||||
}
|
||||
}) else {
|
||||
return build_public_support_maintenance_response(
|
||||
"Auth registration requires Rust data backend",
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::SERVICE_UNAVAILABLE,
|
||||
AUTH_REGISTRATION_STORAGE_UNAVAILABLE_DETAIL,
|
||||
false,
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
use super::*;
|
||||
use super::{
|
||||
auth_access_token_expiry_hours, auth_client_ip, auth_jwt_secret, auth_now,
|
||||
auth_refresh_cookie_name, auth_user_agent, build_auth_error_response, build_auth_json_response,
|
||||
build_auth_refresh_cookie_clear_header, build_auth_refresh_cookie_header, extract_bearer_token,
|
||||
extract_client_device_id, extract_cookie_value, http, json, AppState, Body,
|
||||
GatewayPublicRequestContext, Response, AUTH_REFRESH_TOKEN_EXPIRATION_DAYS,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
fn base64url_encode(bytes: &[u8]) -> String {
|
||||
use base64::Engine;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use super::*;
|
||||
|
||||
const DASHBOARD_MAINTENANCE_DETAIL: &str = "Dashboard routes require Rust maintenance backend";
|
||||
pub(super) use super::{
|
||||
build_auth_error_response, build_unhandled_public_support_response, query_param_value,
|
||||
resolve_authenticated_local_user, AppState, GatewayError, GatewayPublicRequestContext,
|
||||
};
|
||||
use axum::{body::Body, http, response::Response};
|
||||
|
||||
#[path = "dashboard_filters.rs"]
|
||||
mod dashboard_helpers;
|
||||
@@ -10,7 +12,7 @@ use self::dashboard_helpers::{
|
||||
handle_dashboard_recent_requests_get, handle_dashboard_stats_get,
|
||||
};
|
||||
|
||||
pub(super) async fn maybe_build_local_dashboard_legacy_response(
|
||||
pub(super) async fn maybe_build_local_dashboard_response(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
headers: &http::HeaderMap,
|
||||
@@ -40,6 +42,54 @@ pub(super) async fn maybe_build_local_dashboard_legacy_response(
|
||||
{
|
||||
handle_dashboard_daily_stats_get(state, request_context, headers).await
|
||||
}
|
||||
_ => build_public_support_maintenance_response(DASHBOARD_MAINTENANCE_DETAIL),
|
||||
_ => build_unhandled_public_support_response(request_context),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
maybe_build_local_dashboard_response, AppState, GatewayPublicRequestContext,
|
||||
};
|
||||
use crate::gateway::GatewayControlDecision;
|
||||
use axum::body::to_bytes;
|
||||
use axum::http::{HeaderMap, Method, StatusCode, Uri};
|
||||
|
||||
fn request_context(method: Method, uri: &str, route_kind: &str) -> GatewayPublicRequestContext {
|
||||
GatewayPublicRequestContext::from_request_parts(
|
||||
"trace-dashboard-unhandled",
|
||||
&method,
|
||||
&uri.parse::<Uri>().expect("uri should parse"),
|
||||
&HeaderMap::new(),
|
||||
Some(GatewayControlDecision::synthetic(
|
||||
uri,
|
||||
Some("public_support".to_string()),
|
||||
Some("dashboard".to_string()),
|
||||
Some(route_kind.to_string()),
|
||||
Some("user:dashboard".to_string()),
|
||||
)),
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn dashboard_unhandled_route_returns_local_not_implemented_response() {
|
||||
let state = AppState::new().expect("gateway should build");
|
||||
let request_context = request_context(Method::GET, "/api/dashboard/stats/history", "stats");
|
||||
let response =
|
||||
maybe_build_local_dashboard_response(&state, &request_context, &HeaderMap::new()).await;
|
||||
|
||||
assert_eq!(response.status(), StatusCode::NOT_IMPLEMENTED);
|
||||
let body = to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("body should read");
|
||||
let payload: serde_json::Value =
|
||||
serde_json::from_slice(&body).expect("json body should parse");
|
||||
assert_eq!(
|
||||
payload["detail"],
|
||||
"public support route not implemented in rust frontdoor"
|
||||
);
|
||||
assert_eq!(payload["route_family"], "dashboard");
|
||||
assert_eq!(payload["route_kind"], "stats");
|
||||
assert_eq!(payload["request_path"], "/api/dashboard/stats/history");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
use super::*;
|
||||
use super::{
|
||||
build_auth_error_response, query_param_value, resolve_authenticated_local_user, AppState,
|
||||
GatewayError, GatewayPublicRequestContext,
|
||||
};
|
||||
use axum::{
|
||||
body::Body,
|
||||
http,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use chrono::Datelike;
|
||||
use serde_json::json;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct DashboardDateRange {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use super::*;
|
||||
use axum::{body::Body, response::Response};
|
||||
|
||||
pub(super) use super::{query_param_value, AppState, GatewayPublicRequestContext};
|
||||
|
||||
#[path = "models/responses.rs"]
|
||||
mod models_responses;
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
use super::*;
|
||||
use axum::response::IntoResponse;
|
||||
use aether_data::repository::candidate_selection::StoredMinimalCandidateSelectionRow;
|
||||
use axum::{
|
||||
body::Body,
|
||||
http,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
pub(crate) fn build_models_auth_error_response(api_format: &str) -> Response<Body> {
|
||||
match api_format {
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use axum::{body::Body, response::Response};
|
||||
|
||||
use super::models_responses::{
|
||||
build_claude_model_detail_response, build_claude_models_list_response,
|
||||
build_empty_models_list_response, build_gemini_model_detail_response,
|
||||
@@ -6,7 +10,7 @@ use super::models_responses::{
|
||||
build_openai_models_list_response,
|
||||
};
|
||||
use super::models_shared::{filter_rows_for_models, models_api_format, models_detail_id};
|
||||
use super::*;
|
||||
use super::{query_param_value, AppState, GatewayPublicRequestContext};
|
||||
|
||||
pub(super) async fn maybe_build_local_models_route_response(
|
||||
state: &AppState,
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use super::*;
|
||||
use aether_data::repository::candidate_selection::StoredMinimalCandidateSelectionRow;
|
||||
use regex::Regex;
|
||||
|
||||
use super::GatewayPublicRequestContext;
|
||||
|
||||
pub(crate) fn models_api_format(request_context: &GatewayPublicRequestContext) -> Option<&str> {
|
||||
request_context
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use super::*;
|
||||
use super::{build_auth_error_response, resolve_authenticated_local_user};
|
||||
use axum::{body::Body, http, response::Response};
|
||||
|
||||
const USER_MONITORING_MAINTENANCE_DETAIL: &str =
|
||||
"User monitoring routes require Rust maintenance backend";
|
||||
pub(super) use super::{
|
||||
build_auth_error_response, build_unhandled_public_support_response,
|
||||
resolve_authenticated_local_user, AppState, GatewayPublicRequestContext,
|
||||
};
|
||||
|
||||
#[path = "monitoring/audit_logs.rs"]
|
||||
mod user_monitoring_audit_logs;
|
||||
@@ -18,7 +19,7 @@ pub(super) async fn maybe_build_local_user_monitoring_response(
|
||||
headers: &http::HeaderMap,
|
||||
) -> Option<Response<Body>> {
|
||||
let decision = request_context.control_decision.as_ref()?;
|
||||
if decision.route_family.as_deref() != Some("monitoring_user_legacy") {
|
||||
if decision.route_family.as_deref() != Some("monitoring_user") {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -35,8 +36,63 @@ pub(super) async fn maybe_build_local_user_monitoring_response(
|
||||
{
|
||||
Some(handle_user_rate_limit_status(state, request_context, headers).await)
|
||||
}
|
||||
_ => Some(build_public_support_maintenance_response(
|
||||
USER_MONITORING_MAINTENANCE_DETAIL,
|
||||
)),
|
||||
_ => Some(build_unhandled_public_support_response(request_context)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
maybe_build_local_user_monitoring_response, AppState, GatewayPublicRequestContext,
|
||||
};
|
||||
use crate::gateway::GatewayControlDecision;
|
||||
use axum::body::to_bytes;
|
||||
use axum::http::{HeaderMap, Method, StatusCode, Uri};
|
||||
|
||||
fn request_context(method: Method, uri: &str, route_kind: &str) -> GatewayPublicRequestContext {
|
||||
GatewayPublicRequestContext::from_request_parts(
|
||||
"trace-monitoring-unhandled",
|
||||
&method,
|
||||
&uri.parse::<Uri>().expect("uri should parse"),
|
||||
&HeaderMap::new(),
|
||||
Some(GatewayControlDecision::synthetic(
|
||||
uri,
|
||||
Some("public_support".to_string()),
|
||||
Some("monitoring_user".to_string()),
|
||||
Some(route_kind.to_string()),
|
||||
Some("user:monitoring".to_string()),
|
||||
)),
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn monitoring_unhandled_route_returns_local_not_implemented_response() {
|
||||
let state = AppState::new().expect("gateway should build");
|
||||
let request_context = request_context(
|
||||
Method::GET,
|
||||
"/api/monitoring/my-audit-logs/history",
|
||||
"audit_logs",
|
||||
);
|
||||
let response =
|
||||
maybe_build_local_user_monitoring_response(&state, &request_context, &HeaderMap::new())
|
||||
.await
|
||||
.expect("monitoring handler should return response");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::NOT_IMPLEMENTED);
|
||||
let body = to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("body should read");
|
||||
let payload: serde_json::Value =
|
||||
serde_json::from_slice(&body).expect("json body should parse");
|
||||
assert_eq!(
|
||||
payload["detail"],
|
||||
"public support route not implemented in rust frontdoor"
|
||||
);
|
||||
assert_eq!(payload["route_family"], "monitoring_user");
|
||||
assert_eq!(payload["route_kind"], "audit_logs");
|
||||
assert_eq!(
|
||||
payload["request_path"],
|
||||
"/api/monitoring/my-audit-logs/history"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
use axum::{http, response::IntoResponse};
|
||||
use axum::{
|
||||
body::Body,
|
||||
http,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use chrono::Utc;
|
||||
use serde_json::{json, Value};
|
||||
use sqlx::Row;
|
||||
|
||||
use crate::gateway::handlers::shared::query_param_value;
|
||||
|
||||
use super::{
|
||||
build_auth_error_response, query_param_value, resolve_authenticated_local_user, AppState, Body,
|
||||
GatewayPublicRequestContext, Json, Response,
|
||||
build_auth_error_response, resolve_authenticated_local_user, AppState,
|
||||
GatewayPublicRequestContext,
|
||||
};
|
||||
|
||||
fn parse_user_monitoring_limit(query: Option<&str>) -> Result<usize, String> {
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
use axum::{http, response::IntoResponse};
|
||||
use axum::{
|
||||
body::Body,
|
||||
http,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use chrono::Utc;
|
||||
use serde_json::json;
|
||||
|
||||
use super::{
|
||||
build_auth_error_response, resolve_authenticated_local_user, AppState, Body,
|
||||
GatewayPublicRequestContext, Json, Response,
|
||||
build_auth_error_response, resolve_authenticated_local_user, AppState,
|
||||
GatewayPublicRequestContext,
|
||||
};
|
||||
|
||||
fn normalize_rate_limit_value(value: Option<i32>) -> u32 {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use super::*;
|
||||
use axum::{body::Body, http, response::Response};
|
||||
|
||||
pub(super) use super::{build_auth_error_response, AppState, GatewayPublicRequestContext};
|
||||
|
||||
#[path = "payment/postgres.rs"]
|
||||
mod payment_postgres;
|
||||
@@ -10,11 +12,19 @@ mod payment_shared;
|
||||
#[path = "payment/test_support.rs"]
|
||||
mod payment_test_support;
|
||||
|
||||
pub(super) use self::payment_postgres::*;
|
||||
pub(super) use self::payment_shared::*;
|
||||
|
||||
use self::payment_postgres::handle_payment_callback_with_postgres;
|
||||
use self::payment_shared::NormalizedPaymentCallbackRequest;
|
||||
|
||||
const PAYMENT_CALLBACK_STORAGE_UNAVAILABLE_DETAIL: &str = "支付回调存储暂不可用";
|
||||
|
||||
fn build_payment_callback_storage_unavailable_response() -> Response<Body> {
|
||||
build_auth_error_response(
|
||||
http::StatusCode::SERVICE_UNAVAILABLE,
|
||||
PAYMENT_CALLBACK_STORAGE_UNAVAILABLE_DETAIL,
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) async fn maybe_build_local_payment_callback_response(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
@@ -29,3 +39,30 @@ pub(super) async fn maybe_build_local_payment_callback_response(
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
build_payment_callback_storage_unavailable_response,
|
||||
PAYMENT_CALLBACK_STORAGE_UNAVAILABLE_DETAIL,
|
||||
};
|
||||
use axum::body::to_bytes;
|
||||
use axum::http;
|
||||
use serde_json::json;
|
||||
|
||||
#[tokio::test]
|
||||
async fn payment_callback_storage_unavailable_response_is_explicit_local_503() {
|
||||
let response = build_payment_callback_storage_unavailable_response();
|
||||
|
||||
assert_eq!(response.status(), http::StatusCode::SERVICE_UNAVAILABLE);
|
||||
let body = to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("body should read");
|
||||
let payload: serde_json::Value =
|
||||
serde_json::from_slice(&body).expect("json body should parse");
|
||||
assert_eq!(
|
||||
payload,
|
||||
json!({ "detail": PAYMENT_CALLBACK_STORAGE_UNAVAILABLE_DETAIL })
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,17 @@ use super::payment_shared::{
|
||||
payment_callback_mark_failed_response, payment_callback_payload_hash,
|
||||
NormalizedPaymentCallbackRequest,
|
||||
};
|
||||
use super::*;
|
||||
use axum::{body::Body, http, response::Response};
|
||||
use chrono::Utc;
|
||||
use serde_json::json;
|
||||
use sqlx::Row;
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::super::{build_auth_json_response, wallet_payment_order_payload_from_row};
|
||||
use super::{
|
||||
build_auth_error_response, build_payment_callback_storage_unavailable_response, AppState,
|
||||
GatewayPublicRequestContext,
|
||||
};
|
||||
|
||||
async fn update_payment_callback_failure(
|
||||
tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
||||
@@ -46,9 +55,7 @@ pub(super) async fn handle_payment_callback_with_postgres(
|
||||
signature_valid: bool,
|
||||
) -> Response<Body> {
|
||||
let Some(pool) = state.postgres_pool() else {
|
||||
return build_public_support_maintenance_response(
|
||||
"Payment callback routes require Rust maintenance backend",
|
||||
);
|
||||
return build_payment_callback_storage_unavailable_response();
|
||||
};
|
||||
let mut tx = match pool.begin().await {
|
||||
Ok(value) => value,
|
||||
@@ -323,6 +330,10 @@ FOR UPDATE
|
||||
.try_get::<String, _>("wallet_id")
|
||||
.ok()
|
||||
.unwrap_or_default();
|
||||
let order_payment_method = order_row
|
||||
.try_get::<String, _>("payment_method")
|
||||
.ok()
|
||||
.unwrap_or_default();
|
||||
let order_amount_usd = order_row
|
||||
.try_get::<f64, _>("amount_usd")
|
||||
.ok()
|
||||
@@ -353,6 +364,24 @@ FOR UPDATE
|
||||
&request_context.request_path,
|
||||
);
|
||||
}
|
||||
if !order_payment_method.eq_ignore_ascii_case(payment_method) {
|
||||
update_payment_callback_failure(
|
||||
&mut tx,
|
||||
&callback_id,
|
||||
payload,
|
||||
&callback_payload_hash,
|
||||
signature_valid,
|
||||
"payment method mismatch",
|
||||
)
|
||||
.await;
|
||||
let _ = tx.commit().await;
|
||||
return payment_callback_mark_failed_response(
|
||||
duplicate,
|
||||
"payment method mismatch",
|
||||
payment_method,
|
||||
&request_context.request_path,
|
||||
);
|
||||
}
|
||||
if order_status == "credited" {
|
||||
let _ = sqlx::query(
|
||||
r#"
|
||||
@@ -726,3 +755,57 @@ WHERE id = $1
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{handle_payment_callback_with_postgres, AppState, NormalizedPaymentCallbackRequest};
|
||||
use crate::gateway::handlers::public::support::support_payment::PAYMENT_CALLBACK_STORAGE_UNAVAILABLE_DETAIL;
|
||||
use crate::gateway::GatewayPublicRequestContext;
|
||||
use axum::body::to_bytes;
|
||||
use axum::http::{HeaderMap, Method, Uri};
|
||||
use serde_json::json;
|
||||
|
||||
#[tokio::test]
|
||||
async fn payment_callback_postgres_handler_returns_explicit_503_without_pool() {
|
||||
let state = AppState::new().expect("state should build");
|
||||
let request_context = GatewayPublicRequestContext::from_request_parts(
|
||||
"trace-payment-callback-postgres-missing",
|
||||
&Method::POST,
|
||||
&"/api/payment/callback/alipay"
|
||||
.parse::<Uri>()
|
||||
.expect("uri should parse"),
|
||||
&HeaderMap::new(),
|
||||
None,
|
||||
);
|
||||
let payload = NormalizedPaymentCallbackRequest {
|
||||
callback_key: "callback-key-1".to_string(),
|
||||
order_no: Some("order-no-1".to_string()),
|
||||
gateway_order_id: Some("gateway-order-1".to_string()),
|
||||
amount_usd: 10.0,
|
||||
pay_amount: Some(10.0),
|
||||
pay_currency: Some("USD".to_string()),
|
||||
exchange_rate: Some(1.0),
|
||||
payload: json!({ "status": "paid" }),
|
||||
};
|
||||
|
||||
let response = handle_payment_callback_with_postgres(
|
||||
&state,
|
||||
"alipay",
|
||||
&request_context,
|
||||
&payload,
|
||||
true,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(response.status(), http::StatusCode::SERVICE_UNAVAILABLE);
|
||||
let body = to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("body should read");
|
||||
let payload: serde_json::Value =
|
||||
serde_json::from_slice(&body).expect("json body should parse");
|
||||
assert_eq!(
|
||||
payload,
|
||||
json!({ "detail": PAYMENT_CALLBACK_STORAGE_UNAVAILABLE_DETAIL })
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
use axum::{body::Body, http, response::Response};
|
||||
|
||||
use super::payment_shared::{
|
||||
normalize_payment_callback_request, payment_callback_payment_method_from_path,
|
||||
payment_callback_secret, payment_callback_signature_matches, PaymentCallbackRequest,
|
||||
PAYMENT_CALLBACK_SIGNATURE_HEADER, PAYMENT_CALLBACK_TOKEN_HEADER,
|
||||
};
|
||||
use super::*;
|
||||
use super::{
|
||||
build_auth_error_response, build_payment_callback_storage_unavailable_response,
|
||||
handle_payment_callback_with_postgres, AppState, GatewayPublicRequestContext,
|
||||
};
|
||||
|
||||
pub(super) async fn maybe_build_local_payment_callback_route_response(
|
||||
state: &AppState,
|
||||
@@ -12,7 +17,7 @@ pub(super) async fn maybe_build_local_payment_callback_route_response(
|
||||
request_body: Option<&axum::body::Bytes>,
|
||||
) -> Option<Response<Body>> {
|
||||
let decision = request_context.control_decision.as_ref()?;
|
||||
if decision.route_family.as_deref() != Some("payment_callback_legacy")
|
||||
if decision.route_family.as_deref() != Some("payment_callback")
|
||||
|| decision.route_kind.as_deref() != Some("callback")
|
||||
{
|
||||
return None;
|
||||
@@ -114,7 +119,7 @@ pub(super) async fn maybe_build_local_payment_callback_route_response(
|
||||
#[cfg(test)]
|
||||
{
|
||||
return Some(
|
||||
payment_test_support::handle_payment_callback_with_test_store(
|
||||
super::payment_test_support::handle_payment_callback_with_test_store(
|
||||
&payment_method,
|
||||
request_context,
|
||||
&payload,
|
||||
@@ -126,8 +131,6 @@ pub(super) async fn maybe_build_local_payment_callback_route_response(
|
||||
|
||||
#[cfg(not(test))]
|
||||
{
|
||||
Some(build_public_support_maintenance_response(
|
||||
"Payment callback routes require Rust maintenance backend",
|
||||
))
|
||||
Some(build_payment_callback_storage_unavailable_response())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
use super::*;
|
||||
use axum::{body::Body, http, response::Response};
|
||||
use hmac::{Hmac, Mac};
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
use super::super::{build_auth_json_response, wallet_normalize_optional_string_field};
|
||||
|
||||
pub(super) const PAYMENT_CALLBACK_TOKEN_HEADER: &str = "x-payment-callback-token";
|
||||
pub(super) const PAYMENT_CALLBACK_SIGNATURE_HEADER: &str = "x-payment-callback-signature";
|
||||
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
use super::super::support_wallet::wallet_test_recharge_store;
|
||||
use super::*;
|
||||
use axum::{body::Body, http, response::Response};
|
||||
use chrono::Utc;
|
||||
use serde_json::json;
|
||||
|
||||
use super::super::{build_auth_json_response, sanitize_wallet_gateway_response};
|
||||
use super::payment_shared::{
|
||||
payment_callback_mark_failed_response, NormalizedPaymentCallbackRequest,
|
||||
};
|
||||
use super::GatewayPublicRequestContext;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct PaymentTestCallbackRecord {
|
||||
@@ -77,6 +85,23 @@ pub(super) async fn handle_payment_callback_with_test_store(
|
||||
&request_context.request_path,
|
||||
);
|
||||
};
|
||||
let order_payment_method = order.payload["payment_method"]
|
||||
.as_str()
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
if !order_payment_method.eq_ignore_ascii_case(payment_method) {
|
||||
callback_store.push(PaymentTestCallbackRecord {
|
||||
callback_key: payload.callback_key.clone(),
|
||||
payment_order_id: order.payload["id"].as_str().map(ToOwned::to_owned),
|
||||
status: "failed".to_string(),
|
||||
});
|
||||
return payment_callback_mark_failed_response(
|
||||
duplicate,
|
||||
"payment method mismatch",
|
||||
payment_method,
|
||||
&request_context.request_path,
|
||||
);
|
||||
}
|
||||
|
||||
let order_amount = order.payload["amount_usd"].as_f64().unwrap_or_default();
|
||||
if (payload.amount_usd - order_amount).abs() > f64::EPSILON {
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use super::*;
|
||||
use axum::{body::Body, response::Response};
|
||||
|
||||
pub(super) use super::{query_param_value, AppState, GatewayPublicRequestContext};
|
||||
use crate::gateway::handlers::admin::provider_catalog_key_supports_format;
|
||||
|
||||
#[path = "test_connection/route.rs"]
|
||||
mod test_connection_route;
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
use axum::{
|
||||
body::Body,
|
||||
http,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
use super::test_connection_shared::select_test_connection_provider;
|
||||
use super::*;
|
||||
use super::{
|
||||
provider_catalog_key_supports_format, query_param_value, AppState, GatewayPublicRequestContext,
|
||||
};
|
||||
|
||||
pub(super) async fn maybe_build_local_test_connection_route_response(
|
||||
state: &AppState,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::*;
|
||||
use aether_data::repository::provider_catalog::StoredProviderCatalogProvider;
|
||||
|
||||
pub(super) fn select_test_connection_provider(
|
||||
providers: Vec<StoredProviderCatalogProvider>,
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
use super::*;
|
||||
use super::{
|
||||
auth_password_policy_level, build_auth_error_response, build_auth_wallet_summary_payload,
|
||||
decrypt_catalog_secret_with_fallbacks, encrypt_catalog_secret_with_fallbacks, handle_auth_me,
|
||||
query_param_optional_bool, query_param_value, resolve_authenticated_local_user,
|
||||
unix_secs_to_rfc3339, validate_auth_register_password, AppState, AuthenticatedLocalUserContext,
|
||||
GatewayPublicRequestContext, PUBLIC_CAPABILITY_DEFINITIONS,
|
||||
};
|
||||
use crate::gateway::handlers::admin::{
|
||||
admin_stats_bad_request_response, build_admin_endpoint_health_status_payload,
|
||||
list_usage_for_optional_range, parse_bounded_u32, round_to, AdminStatsTimeRange,
|
||||
AdminStatsUsageFilter,
|
||||
};
|
||||
use crate::gateway::handlers::internal::build_management_token_payload;
|
||||
|
||||
const USERS_ME_MAINTENANCE_DETAIL: &str =
|
||||
"User self-service routes require Rust maintenance backend";
|
||||
const USERS_ME_AVAILABLE_MODELS_FETCH_LIMIT: usize = 1000;
|
||||
|
||||
#[path = "user_me_management_tokens.rs"]
|
||||
@@ -33,4 +43,4 @@ use user_me_shared::*;
|
||||
mod user_me_routes;
|
||||
use user_me_routes::*;
|
||||
|
||||
pub(super) use self::user_me_routes::maybe_build_local_users_me_legacy_response;
|
||||
pub(super) use self::user_me_routes::maybe_build_local_users_me_response;
|
||||
|
||||
@@ -1,4 +1,23 @@
|
||||
use super::*;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use axum::{
|
||||
body::Body,
|
||||
http,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
|
||||
use super::{
|
||||
build_auth_error_response, decrypt_catalog_secret_with_fallbacks,
|
||||
encrypt_catalog_secret_with_fallbacks, format_users_me_optional_unix_secs_iso8601,
|
||||
known_capability_names, normalize_user_model_capability_settings_input,
|
||||
query_param_optional_bool, resolve_authenticated_local_user,
|
||||
user_configurable_capability_names, AppState, GatewayPublicRequestContext,
|
||||
};
|
||||
|
||||
const USERS_ME_API_KEY_WRITE_UNAVAILABLE_DETAIL: &str = "用户 API 密钥写入暂不可用";
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct UsersMeCreateApiKeyRequest {
|
||||
@@ -52,17 +71,42 @@ struct UsersMeUpdateApiKeyCapabilitiesRequest {
|
||||
capabilities: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
fn users_me_api_key_id_from_path(request_path: &str) -> Option<String> {
|
||||
request_path
|
||||
fn users_me_api_key_path_segments(request_path: &str) -> Option<Vec<&str>> {
|
||||
let raw = request_path
|
||||
.strip_prefix("/api/users/me/api-keys/")?
|
||||
.trim()
|
||||
.trim_matches('/')
|
||||
.trim_matches('/');
|
||||
if raw.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let segments = raw
|
||||
.split('/')
|
||||
.next()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.filter(|value| !value.contains('/'))
|
||||
.map(ToOwned::to_owned)
|
||||
.collect::<Vec<_>>();
|
||||
(!segments.is_empty()).then_some(segments)
|
||||
}
|
||||
|
||||
fn users_me_api_key_detail_id_from_path(request_path: &str) -> Option<String> {
|
||||
let segments = users_me_api_key_path_segments(request_path)?;
|
||||
(segments.len() == 1).then(|| segments[0].to_string())
|
||||
}
|
||||
|
||||
fn users_me_api_key_nested_id_from_path(request_path: &str, suffix: &str) -> Option<String> {
|
||||
let segments = users_me_api_key_path_segments(request_path)?;
|
||||
(segments.len() == 2 && segments[1] == suffix).then(|| segments[0].to_string())
|
||||
}
|
||||
|
||||
pub(super) fn users_me_api_key_detail_path_matches(request_path: &str) -> bool {
|
||||
users_me_api_key_detail_id_from_path(request_path).is_some()
|
||||
}
|
||||
|
||||
pub(super) fn users_me_api_key_providers_path_matches(request_path: &str) -> bool {
|
||||
users_me_api_key_nested_id_from_path(request_path, "providers").is_some()
|
||||
}
|
||||
|
||||
pub(super) fn users_me_api_key_capabilities_path_matches(request_path: &str) -> bool {
|
||||
users_me_api_key_nested_id_from_path(request_path, "capabilities").is_some()
|
||||
}
|
||||
|
||||
fn users_me_masked_api_key_display(state: &AppState, ciphertext: Option<&str>) -> String {
|
||||
@@ -83,6 +127,14 @@ fn users_me_masked_api_key_display(state: &AppState, ciphertext: Option<&str>) -
|
||||
format!("{prefix}...{suffix}")
|
||||
}
|
||||
|
||||
fn build_users_me_api_key_writer_unavailable_response() -> Response<Body> {
|
||||
build_auth_error_response(
|
||||
http::StatusCode::SERVICE_UNAVAILABLE,
|
||||
USERS_ME_API_KEY_WRITE_UNAVAILABLE_DETAIL,
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
fn build_users_me_api_key_list_payload(
|
||||
state: &AppState,
|
||||
record: &aether_data::repository::auth::StoredAuthApiKeyExportRecord,
|
||||
@@ -291,8 +343,9 @@ pub(super) async fn handle_users_me_api_key_detail_get(
|
||||
Ok(value) => value,
|
||||
Err(response) => return response,
|
||||
};
|
||||
let Some(api_key_id) = users_me_api_key_id_from_path(&request_context.request_path) else {
|
||||
return build_public_support_maintenance_response(USERS_ME_MAINTENANCE_DETAIL);
|
||||
let Some(api_key_id) = users_me_api_key_detail_id_from_path(&request_context.request_path)
|
||||
else {
|
||||
return build_auth_error_response(http::StatusCode::NOT_FOUND, "API密钥不存在", false);
|
||||
};
|
||||
let include_key = query_param_optional_bool(
|
||||
request_context.request_query_string.as_deref(),
|
||||
@@ -372,20 +425,15 @@ pub(super) async fn handle_users_me_api_key_detail_get(
|
||||
.into_response()
|
||||
}
|
||||
|
||||
async fn resolve_users_me_api_key_snapshot(
|
||||
async fn resolve_users_me_api_key_snapshot_by_id(
|
||||
state: &AppState,
|
||||
user_id: &str,
|
||||
request_path: &str,
|
||||
api_key_id: &str,
|
||||
) -> Result<crate::gateway::gateway_data::StoredGatewayAuthApiKeySnapshot, Response<Body>> {
|
||||
let Some(api_key_id) = users_me_api_key_id_from_path(request_path) else {
|
||||
return Err(build_public_support_maintenance_response(
|
||||
USERS_ME_MAINTENANCE_DETAIL,
|
||||
));
|
||||
};
|
||||
let snapshot = match state
|
||||
.read_auth_api_key_snapshot(
|
||||
user_id,
|
||||
&api_key_id,
|
||||
api_key_id,
|
||||
chrono::Utc::now().timestamp().max(0) as u64,
|
||||
)
|
||||
.await
|
||||
@@ -436,7 +484,7 @@ pub(super) async fn handle_users_me_api_key_create(
|
||||
request_body: Option<&axum::body::Bytes>,
|
||||
) -> Response<Body> {
|
||||
if !state.has_auth_api_key_writer() {
|
||||
return build_public_support_maintenance_response(USERS_ME_MAINTENANCE_DETAIL);
|
||||
return build_users_me_api_key_writer_unavailable_response();
|
||||
}
|
||||
let auth = match resolve_authenticated_local_user(state, request_context, headers).await {
|
||||
Ok(value) => value,
|
||||
@@ -497,7 +545,7 @@ pub(super) async fn handle_users_me_api_key_create(
|
||||
)
|
||||
}
|
||||
}) else {
|
||||
return build_public_support_maintenance_response(USERS_ME_MAINTENANCE_DETAIL);
|
||||
return build_users_me_api_key_writer_unavailable_response();
|
||||
};
|
||||
|
||||
Json(json!({
|
||||
@@ -518,22 +566,21 @@ pub(super) async fn handle_users_me_api_key_update(
|
||||
request_body: Option<&axum::body::Bytes>,
|
||||
) -> Response<Body> {
|
||||
if !state.has_auth_api_key_writer() {
|
||||
return build_public_support_maintenance_response(USERS_ME_MAINTENANCE_DETAIL);
|
||||
return build_users_me_api_key_writer_unavailable_response();
|
||||
}
|
||||
let auth = match resolve_authenticated_local_user(state, request_context, headers).await {
|
||||
Ok(value) => value,
|
||||
Err(response) => return response,
|
||||
};
|
||||
let snapshot = match resolve_users_me_api_key_snapshot(
|
||||
state,
|
||||
&auth.user.id,
|
||||
&request_context.request_path,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(value) => value,
|
||||
Err(response) => return response,
|
||||
let Some(api_key_id) = users_me_api_key_detail_id_from_path(&request_context.request_path)
|
||||
else {
|
||||
return build_auth_error_response(http::StatusCode::NOT_FOUND, "API密钥不存在", false);
|
||||
};
|
||||
let snapshot =
|
||||
match resolve_users_me_api_key_snapshot_by_id(state, &auth.user.id, &api_key_id).await {
|
||||
Ok(value) => value,
|
||||
Err(response) => return response,
|
||||
};
|
||||
if let Err(response) = ensure_users_me_api_key_mutable(&snapshot) {
|
||||
return response;
|
||||
}
|
||||
@@ -586,7 +633,7 @@ pub(super) async fn handle_users_me_api_key_update(
|
||||
)
|
||||
}
|
||||
}) else {
|
||||
return build_public_support_maintenance_response(USERS_ME_MAINTENANCE_DETAIL);
|
||||
return build_users_me_api_key_writer_unavailable_response();
|
||||
};
|
||||
|
||||
let mut payload =
|
||||
@@ -602,22 +649,21 @@ pub(super) async fn handle_users_me_api_key_patch(
|
||||
request_body: Option<&axum::body::Bytes>,
|
||||
) -> Response<Body> {
|
||||
if !state.has_auth_api_key_writer() {
|
||||
return build_public_support_maintenance_response(USERS_ME_MAINTENANCE_DETAIL);
|
||||
return build_users_me_api_key_writer_unavailable_response();
|
||||
}
|
||||
let auth = match resolve_authenticated_local_user(state, request_context, headers).await {
|
||||
Ok(value) => value,
|
||||
Err(response) => return response,
|
||||
};
|
||||
let snapshot = match resolve_users_me_api_key_snapshot(
|
||||
state,
|
||||
&auth.user.id,
|
||||
&request_context.request_path,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(value) => value,
|
||||
Err(response) => return response,
|
||||
let Some(api_key_id) = users_me_api_key_detail_id_from_path(&request_context.request_path)
|
||||
else {
|
||||
return build_auth_error_response(http::StatusCode::NOT_FOUND, "API密钥不存在", false);
|
||||
};
|
||||
let snapshot =
|
||||
match resolve_users_me_api_key_snapshot_by_id(state, &auth.user.id, &api_key_id).await {
|
||||
Ok(value) => value,
|
||||
Err(response) => return response,
|
||||
};
|
||||
if let Err(response) = ensure_users_me_api_key_mutable(&snapshot) {
|
||||
return response;
|
||||
}
|
||||
@@ -649,7 +695,7 @@ pub(super) async fn handle_users_me_api_key_patch(
|
||||
)
|
||||
}
|
||||
}) else {
|
||||
return build_public_support_maintenance_response(USERS_ME_MAINTENANCE_DETAIL);
|
||||
return build_users_me_api_key_writer_unavailable_response();
|
||||
};
|
||||
|
||||
Json(json!({
|
||||
@@ -666,22 +712,21 @@ pub(super) async fn handle_users_me_api_key_delete(
|
||||
headers: &http::HeaderMap,
|
||||
) -> Response<Body> {
|
||||
if !state.has_auth_api_key_writer() {
|
||||
return build_public_support_maintenance_response(USERS_ME_MAINTENANCE_DETAIL);
|
||||
return build_users_me_api_key_writer_unavailable_response();
|
||||
}
|
||||
let auth = match resolve_authenticated_local_user(state, request_context, headers).await {
|
||||
Ok(value) => value,
|
||||
Err(response) => return response,
|
||||
};
|
||||
let snapshot = match resolve_users_me_api_key_snapshot(
|
||||
state,
|
||||
&auth.user.id,
|
||||
&request_context.request_path,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(value) => value,
|
||||
Err(response) => return response,
|
||||
let Some(api_key_id) = users_me_api_key_detail_id_from_path(&request_context.request_path)
|
||||
else {
|
||||
return build_auth_error_response(http::StatusCode::NOT_FOUND, "API密钥不存在", false);
|
||||
};
|
||||
let snapshot =
|
||||
match resolve_users_me_api_key_snapshot_by_id(state, &auth.user.id, &api_key_id).await {
|
||||
Ok(value) => value,
|
||||
Err(response) => return response,
|
||||
};
|
||||
if let Err(response) = ensure_users_me_api_key_mutable(&snapshot) {
|
||||
return response;
|
||||
}
|
||||
@@ -707,22 +752,22 @@ pub(super) async fn handle_users_me_api_key_providers_put(
|
||||
request_body: Option<&axum::body::Bytes>,
|
||||
) -> Response<Body> {
|
||||
if !state.has_auth_api_key_writer() {
|
||||
return build_public_support_maintenance_response(USERS_ME_MAINTENANCE_DETAIL);
|
||||
return build_users_me_api_key_writer_unavailable_response();
|
||||
}
|
||||
let auth = match resolve_authenticated_local_user(state, request_context, headers).await {
|
||||
Ok(value) => value,
|
||||
Err(response) => return response,
|
||||
};
|
||||
let snapshot = match resolve_users_me_api_key_snapshot(
|
||||
state,
|
||||
&auth.user.id,
|
||||
&request_context.request_path,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(value) => value,
|
||||
Err(response) => return response,
|
||||
let Some(api_key_id) =
|
||||
users_me_api_key_nested_id_from_path(&request_context.request_path, "providers")
|
||||
else {
|
||||
return build_auth_error_response(http::StatusCode::NOT_FOUND, "API密钥不存在", false);
|
||||
};
|
||||
let snapshot =
|
||||
match resolve_users_me_api_key_snapshot_by_id(state, &auth.user.id, &api_key_id).await {
|
||||
Ok(value) => value,
|
||||
Err(response) => return response,
|
||||
};
|
||||
if let Err(response) = ensure_users_me_api_key_mutable(&snapshot) {
|
||||
return response;
|
||||
}
|
||||
@@ -825,7 +870,7 @@ pub(super) async fn handle_users_me_api_key_providers_put(
|
||||
)
|
||||
}
|
||||
}) else {
|
||||
return build_public_support_maintenance_response(USERS_ME_MAINTENANCE_DETAIL);
|
||||
return build_users_me_api_key_writer_unavailable_response();
|
||||
};
|
||||
|
||||
Json(json!({
|
||||
@@ -842,22 +887,22 @@ pub(super) async fn handle_users_me_api_key_capabilities_put(
|
||||
request_body: Option<&axum::body::Bytes>,
|
||||
) -> Response<Body> {
|
||||
if !state.has_auth_api_key_writer() {
|
||||
return build_public_support_maintenance_response(USERS_ME_MAINTENANCE_DETAIL);
|
||||
return build_users_me_api_key_writer_unavailable_response();
|
||||
}
|
||||
let auth = match resolve_authenticated_local_user(state, request_context, headers).await {
|
||||
Ok(value) => value,
|
||||
Err(response) => return response,
|
||||
};
|
||||
let snapshot = match resolve_users_me_api_key_snapshot(
|
||||
state,
|
||||
&auth.user.id,
|
||||
&request_context.request_path,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(value) => value,
|
||||
Err(response) => return response,
|
||||
let Some(api_key_id) =
|
||||
users_me_api_key_nested_id_from_path(&request_context.request_path, "capabilities")
|
||||
else {
|
||||
return build_auth_error_response(http::StatusCode::NOT_FOUND, "API密钥不存在", false);
|
||||
};
|
||||
let snapshot =
|
||||
match resolve_users_me_api_key_snapshot_by_id(state, &auth.user.id, &api_key_id).await {
|
||||
Ok(value) => value,
|
||||
Err(response) => return response,
|
||||
};
|
||||
if let Err(response) = ensure_users_me_api_key_mutable(&snapshot) {
|
||||
return response;
|
||||
}
|
||||
@@ -899,7 +944,7 @@ pub(super) async fn handle_users_me_api_key_capabilities_put(
|
||||
)
|
||||
}
|
||||
}) else {
|
||||
return build_public_support_maintenance_response(USERS_ME_MAINTENANCE_DETAIL);
|
||||
return build_users_me_api_key_writer_unavailable_response();
|
||||
};
|
||||
|
||||
Json(json!({
|
||||
|
||||
@@ -1,4 +1,22 @@
|
||||
use super::*;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use axum::{
|
||||
body::Body,
|
||||
http,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
use super::{
|
||||
build_admin_endpoint_health_status_payload, build_auth_error_response, query_param_value,
|
||||
resolve_authenticated_local_user, AppState, GatewayPublicRequestContext,
|
||||
USERS_ME_AVAILABLE_MODELS_FETCH_LIMIT,
|
||||
};
|
||||
|
||||
const USERS_ME_MODEL_CATALOG_UNAVAILABLE_DETAIL: &str = "用户模型目录暂不可用";
|
||||
const USERS_ME_PROVIDER_CATALOG_UNAVAILABLE_DETAIL: &str = "用户提供商目录暂不可用";
|
||||
const USERS_ME_ENDPOINT_STATUS_UNAVAILABLE_DETAIL: &str = "用户端点健康数据暂不可用";
|
||||
|
||||
fn build_users_me_available_model_payload(
|
||||
model: aether_data::repository::global_models::StoredPublicGlobalModel,
|
||||
@@ -60,8 +78,10 @@ async fn resolve_users_me_allowed_global_model_ids(
|
||||
};
|
||||
|
||||
if !state.has_provider_catalog_data_reader() {
|
||||
return Err(build_public_support_maintenance_response(
|
||||
USERS_ME_MAINTENANCE_DETAIL,
|
||||
return Err(build_auth_error_response(
|
||||
http::StatusCode::SERVICE_UNAVAILABLE,
|
||||
USERS_ME_PROVIDER_CATALOG_UNAVAILABLE_DETAIL,
|
||||
false,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -118,7 +138,11 @@ pub(super) async fn handle_users_me_available_models(
|
||||
headers: &http::HeaderMap,
|
||||
) -> Response<Body> {
|
||||
if !state.has_global_model_data_reader() {
|
||||
return build_public_support_maintenance_response(USERS_ME_MAINTENANCE_DETAIL);
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::SERVICE_UNAVAILABLE,
|
||||
USERS_ME_MODEL_CATALOG_UNAVAILABLE_DETAIL,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
let auth = match resolve_authenticated_local_user(state, request_context, headers).await {
|
||||
@@ -237,7 +261,11 @@ pub(super) async fn handle_users_me_providers_get(
|
||||
headers: &http::HeaderMap,
|
||||
) -> Response<Body> {
|
||||
if !state.has_provider_catalog_data_reader() {
|
||||
return build_public_support_maintenance_response(USERS_ME_MAINTENANCE_DETAIL);
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::SERVICE_UNAVAILABLE,
|
||||
USERS_ME_PROVIDER_CATALOG_UNAVAILABLE_DETAIL,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
let auth = match resolve_authenticated_local_user(state, request_context, headers).await {
|
||||
@@ -379,7 +407,11 @@ pub(super) async fn handle_users_me_endpoint_status_get(
|
||||
};
|
||||
|
||||
let Some(payload) = build_admin_endpoint_health_status_payload(state, 6).await else {
|
||||
return build_public_support_maintenance_response(USERS_ME_MAINTENANCE_DETAIL);
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::SERVICE_UNAVAILABLE,
|
||||
USERS_ME_ENDPOINT_STATUS_UNAVAILABLE_DETAIL,
|
||||
false,
|
||||
);
|
||||
};
|
||||
let Some(items) = payload.as_array() else {
|
||||
return build_auth_error_response(
|
||||
|
||||
@@ -1,4 +1,24 @@
|
||||
use super::*;
|
||||
use axum::{
|
||||
body::Body,
|
||||
http,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use chrono::Utc;
|
||||
use serde_json::json;
|
||||
use sha2::{Digest, Sha256};
|
||||
use uuid::Uuid;
|
||||
|
||||
use aether_data::repository::management_tokens::{
|
||||
CreateManagementTokenRecord, ManagementTokenListQuery, RegenerateManagementTokenSecret,
|
||||
StoredManagementTokenUserSummary, UpdateManagementTokenRecord,
|
||||
};
|
||||
|
||||
use super::{
|
||||
build_auth_error_response, build_management_token_payload, query_param_optional_bool,
|
||||
query_param_value, resolve_authenticated_local_user, AppState, AuthenticatedLocalUserContext,
|
||||
GatewayPublicRequestContext,
|
||||
};
|
||||
use crate::gateway::LocalMutationOutcome;
|
||||
|
||||
const USERS_ME_MANAGEMENT_TOKEN_PREFIX: &str = "ae_";
|
||||
@@ -7,6 +27,10 @@ const USERS_ME_MANAGEMENT_TOKEN_DISPLAY_PREFIX_LEN: usize = 7;
|
||||
const USERS_ME_MANAGEMENT_TOKEN_FETCH_LIMIT: usize = 10_000;
|
||||
const USERS_ME_MANAGEMENT_TOKEN_DEFAULT_MAX_PER_USER: usize = 20;
|
||||
const USERS_ME_MANAGEMENT_TOKEN_MAX_PER_USER_ENV: &str = "MANAGEMENT_TOKEN_MAX_PER_USER";
|
||||
const USERS_ME_MANAGEMENT_TOKEN_READ_UNAVAILABLE_DETAIL: &str =
|
||||
"用户 Management Token 数据暂不可用";
|
||||
const USERS_ME_MANAGEMENT_TOKEN_WRITE_UNAVAILABLE_DETAIL: &str =
|
||||
"用户 Management Token 写入暂不可用";
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct UsersMeManagementTokenCreateInput {
|
||||
@@ -46,39 +70,47 @@ pub(super) fn users_me_management_tokens_root(request_path: &str) -> bool {
|
||||
)
|
||||
}
|
||||
|
||||
fn users_me_management_token_id_from_path(request_path: &str) -> Option<String> {
|
||||
fn users_me_management_token_path_segments(request_path: &str) -> Option<Vec<&str>> {
|
||||
let raw = request_path
|
||||
.strip_prefix("/api/me/management-tokens/")?
|
||||
.trim()
|
||||
.trim_matches('/');
|
||||
if raw.is_empty() || raw.contains('/') {
|
||||
if raw.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some(raw.to_string())
|
||||
let segments = raw
|
||||
.split('/')
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.collect::<Vec<_>>();
|
||||
(!segments.is_empty()).then_some(segments)
|
||||
}
|
||||
|
||||
fn users_me_management_token_id_from_path(request_path: &str) -> Option<String> {
|
||||
let segments = users_me_management_token_path_segments(request_path)?;
|
||||
(segments.len() == 1).then(|| segments[0].to_string())
|
||||
}
|
||||
|
||||
fn users_me_management_token_status_id_from_path(request_path: &str) -> Option<String> {
|
||||
let raw = request_path
|
||||
.strip_prefix("/api/me/management-tokens/")?
|
||||
.trim()
|
||||
.trim_matches('/');
|
||||
let token_id = raw.strip_suffix("/status")?.trim_matches('/');
|
||||
if token_id.is_empty() || token_id.contains('/') {
|
||||
return None;
|
||||
}
|
||||
Some(token_id.to_string())
|
||||
let segments = users_me_management_token_path_segments(request_path)?;
|
||||
(segments.len() == 2 && segments[1] == "status").then(|| segments[0].to_string())
|
||||
}
|
||||
|
||||
fn users_me_management_token_regenerate_id_from_path(request_path: &str) -> Option<String> {
|
||||
let raw = request_path
|
||||
.strip_prefix("/api/me/management-tokens/")?
|
||||
.trim()
|
||||
.trim_matches('/');
|
||||
let token_id = raw.strip_suffix("/regenerate")?.trim_matches('/');
|
||||
if token_id.is_empty() || token_id.contains('/') {
|
||||
return None;
|
||||
}
|
||||
Some(token_id.to_string())
|
||||
let segments = users_me_management_token_path_segments(request_path)?;
|
||||
(segments.len() == 2 && segments[1] == "regenerate").then(|| segments[0].to_string())
|
||||
}
|
||||
|
||||
pub(super) fn users_me_management_token_detail_path_matches(request_path: &str) -> bool {
|
||||
users_me_management_token_id_from_path(request_path).is_some()
|
||||
}
|
||||
|
||||
pub(super) fn users_me_management_token_toggle_path_matches(request_path: &str) -> bool {
|
||||
users_me_management_token_status_id_from_path(request_path).is_some()
|
||||
}
|
||||
|
||||
pub(super) fn users_me_management_token_regenerate_path_matches(request_path: &str) -> bool {
|
||||
users_me_management_token_regenerate_id_from_path(request_path).is_some()
|
||||
}
|
||||
|
||||
fn users_me_management_token_max_per_user() -> usize {
|
||||
@@ -114,6 +146,22 @@ fn users_me_management_token_prefix(value: &str) -> Option<String> {
|
||||
})
|
||||
}
|
||||
|
||||
fn build_users_me_management_token_reader_unavailable_response() -> Response<Body> {
|
||||
build_auth_error_response(
|
||||
http::StatusCode::SERVICE_UNAVAILABLE,
|
||||
USERS_ME_MANAGEMENT_TOKEN_READ_UNAVAILABLE_DETAIL,
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
fn build_users_me_management_token_writer_unavailable_response() -> Response<Body> {
|
||||
build_auth_error_response(
|
||||
http::StatusCode::SERVICE_UNAVAILABLE,
|
||||
USERS_ME_MANAGEMENT_TOKEN_WRITE_UNAVAILABLE_DETAIL,
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
fn users_me_management_token_limit(query: Option<&str>) -> usize {
|
||||
query_param_value(query, "limit")
|
||||
.and_then(|value| value.parse::<usize>().ok())
|
||||
@@ -326,9 +374,7 @@ async fn list_users_me_management_tokens_for_user(
|
||||
) -> Result<aether_data::repository::management_tokens::StoredManagementTokenListPage, Response<Body>>
|
||||
{
|
||||
if !state.has_management_token_reader() {
|
||||
return Err(build_public_support_maintenance_response(
|
||||
USERS_ME_MAINTENANCE_DETAIL,
|
||||
));
|
||||
return Err(build_users_me_management_token_reader_unavailable_response());
|
||||
}
|
||||
state
|
||||
.list_management_tokens(&ManagementTokenListQuery {
|
||||
@@ -354,9 +400,7 @@ async fn resolve_users_me_management_token(
|
||||
) -> Result<aether_data::repository::management_tokens::StoredManagementTokenWithUser, Response<Body>>
|
||||
{
|
||||
if !state.has_management_token_reader() {
|
||||
return Err(build_public_support_maintenance_response(
|
||||
USERS_ME_MAINTENANCE_DETAIL,
|
||||
));
|
||||
return Err(build_users_me_management_token_reader_unavailable_response());
|
||||
}
|
||||
match state.get_management_token_with_user(token_id).await {
|
||||
Ok(Some(token)) if token.token.user_id == user_id => Ok(token),
|
||||
@@ -379,7 +423,7 @@ pub(super) async fn handle_users_me_management_tokens_list(
|
||||
headers: &http::HeaderMap,
|
||||
) -> Response<Body> {
|
||||
if !state.has_management_token_reader() {
|
||||
return build_public_support_maintenance_response(USERS_ME_MAINTENANCE_DETAIL);
|
||||
return build_users_me_management_token_reader_unavailable_response();
|
||||
}
|
||||
|
||||
let auth = match resolve_authenticated_local_user(state, request_context, headers).await {
|
||||
@@ -433,7 +477,7 @@ pub(super) async fn handle_users_me_management_token_create(
|
||||
request_body: Option<&axum::body::Bytes>,
|
||||
) -> Response<Body> {
|
||||
if !state.has_management_token_writer() {
|
||||
return build_public_support_maintenance_response(USERS_ME_MAINTENANCE_DETAIL);
|
||||
return build_users_me_management_token_writer_unavailable_response();
|
||||
}
|
||||
|
||||
let auth = match resolve_authenticated_local_user(state, request_context, headers).await {
|
||||
@@ -512,7 +556,7 @@ pub(super) async fn handle_users_me_management_token_create(
|
||||
build_auth_error_response(http::StatusCode::BAD_REQUEST, detail, false)
|
||||
}
|
||||
Ok(LocalMutationOutcome::Unavailable) => {
|
||||
build_public_support_maintenance_response(USERS_ME_MAINTENANCE_DETAIL)
|
||||
build_users_me_management_token_writer_unavailable_response()
|
||||
}
|
||||
Ok(LocalMutationOutcome::NotFound) => build_auth_error_response(
|
||||
http::StatusCode::NOT_FOUND,
|
||||
@@ -557,7 +601,7 @@ pub(super) async fn handle_users_me_management_token_update(
|
||||
request_body: Option<&axum::body::Bytes>,
|
||||
) -> Response<Body> {
|
||||
if !state.has_management_token_writer() {
|
||||
return build_public_support_maintenance_response(USERS_ME_MAINTENANCE_DETAIL);
|
||||
return build_users_me_management_token_writer_unavailable_response();
|
||||
}
|
||||
|
||||
let auth = match resolve_authenticated_local_user(state, request_context, headers).await {
|
||||
@@ -646,7 +690,7 @@ pub(super) async fn handle_users_me_management_token_update(
|
||||
build_auth_error_response(http::StatusCode::BAD_REQUEST, detail, false)
|
||||
}
|
||||
Ok(LocalMutationOutcome::Unavailable) => {
|
||||
build_public_support_maintenance_response(USERS_ME_MAINTENANCE_DETAIL)
|
||||
build_users_me_management_token_writer_unavailable_response()
|
||||
}
|
||||
Err(err) => build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
@@ -662,7 +706,7 @@ pub(super) async fn handle_users_me_management_token_delete(
|
||||
headers: &http::HeaderMap,
|
||||
) -> Response<Body> {
|
||||
if !state.has_management_token_writer() {
|
||||
return build_public_support_maintenance_response(USERS_ME_MAINTENANCE_DETAIL);
|
||||
return build_users_me_management_token_writer_unavailable_response();
|
||||
}
|
||||
|
||||
let auth = match resolve_authenticated_local_user(state, request_context, headers).await {
|
||||
@@ -702,7 +746,7 @@ pub(super) async fn handle_users_me_management_token_toggle(
|
||||
headers: &http::HeaderMap,
|
||||
) -> Response<Body> {
|
||||
if !state.has_management_token_writer() {
|
||||
return build_public_support_maintenance_response(USERS_ME_MAINTENANCE_DETAIL);
|
||||
return build_users_me_management_token_writer_unavailable_response();
|
||||
}
|
||||
|
||||
let auth = match resolve_authenticated_local_user(state, request_context, headers).await {
|
||||
@@ -750,7 +794,7 @@ pub(super) async fn handle_users_me_management_token_regenerate(
|
||||
headers: &http::HeaderMap,
|
||||
) -> Response<Body> {
|
||||
if !state.has_management_token_writer() {
|
||||
return build_public_support_maintenance_response(USERS_ME_MAINTENANCE_DETAIL);
|
||||
return build_users_me_management_token_writer_unavailable_response();
|
||||
}
|
||||
|
||||
let auth = match resolve_authenticated_local_user(state, request_context, headers).await {
|
||||
@@ -793,7 +837,7 @@ pub(super) async fn handle_users_me_management_token_regenerate(
|
||||
build_auth_error_response(http::StatusCode::BAD_REQUEST, detail, false)
|
||||
}
|
||||
Ok(LocalMutationOutcome::Unavailable) => {
|
||||
build_public_support_maintenance_response(USERS_ME_MAINTENANCE_DETAIL)
|
||||
build_users_me_management_token_writer_unavailable_response()
|
||||
}
|
||||
Err(err) => build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
|
||||
@@ -1,4 +1,20 @@
|
||||
use super::*;
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use axum::{
|
||||
body::Body,
|
||||
http,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
use super::{
|
||||
build_auth_error_response, resolve_authenticated_local_user, AppState,
|
||||
GatewayPublicRequestContext, PUBLIC_CAPABILITY_DEFINITIONS,
|
||||
};
|
||||
|
||||
const USERS_ME_PREFERENCES_STORAGE_UNAVAILABLE_DETAIL: &str = "用户偏好设置存储暂不可用";
|
||||
const USERS_ME_MODEL_CAPABILITIES_STORAGE_UNAVAILABLE_DETAIL: &str = "用户模型能力配置存储暂不可用";
|
||||
|
||||
pub(super) fn user_configurable_capability_names() -> BTreeSet<&'static str> {
|
||||
PUBLIC_CAPABILITY_DEFINITIONS
|
||||
@@ -328,7 +344,11 @@ pub(super) async fn handle_users_me_preferences_put(
|
||||
|
||||
match state.write_user_preferences(&preferences).await {
|
||||
Ok(Some(_)) => Json(json!({ "message": "偏好设置更新成功" })).into_response(),
|
||||
Ok(None) => build_public_support_maintenance_response(USERS_ME_MAINTENANCE_DETAIL),
|
||||
Ok(None) => build_auth_error_response(
|
||||
http::StatusCode::SERVICE_UNAVAILABLE,
|
||||
USERS_ME_PREFERENCES_STORAGE_UNAVAILABLE_DETAIL,
|
||||
false,
|
||||
),
|
||||
Err(err) => build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("user preference update failed: {err:?}"),
|
||||
@@ -371,7 +391,14 @@ pub(super) async fn handle_users_me_model_capabilities_put(
|
||||
.update_user_model_capability_settings(&auth.user.id, settings)
|
||||
.await
|
||||
{
|
||||
Ok(value) => value,
|
||||
Ok(Some(value)) => value,
|
||||
Ok(None) => {
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::SERVICE_UNAVAILABLE,
|
||||
USERS_ME_MODEL_CAPABILITIES_STORAGE_UNAVAILABLE_DETAIL,
|
||||
false,
|
||||
)
|
||||
}
|
||||
Err(err) => {
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
@@ -383,7 +410,7 @@ pub(super) async fn handle_users_me_model_capabilities_put(
|
||||
|
||||
Json(json!({
|
||||
"message": "模型能力配置已更新",
|
||||
"model_capability_settings": persisted.unwrap_or(serde_json::Value::Null),
|
||||
"model_capability_settings": persisted,
|
||||
}))
|
||||
.into_response()
|
||||
}
|
||||
|
||||
@@ -1,4 +1,19 @@
|
||||
use super::*;
|
||||
use axum::{
|
||||
body::Body,
|
||||
http,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
|
||||
use super::{
|
||||
auth_password_policy_level, build_auth_error_response, resolve_authenticated_local_user,
|
||||
validate_auth_register_password, AppState, GatewayPublicRequestContext,
|
||||
};
|
||||
|
||||
const USERS_ME_PROFILE_STORAGE_UNAVAILABLE_DETAIL: &str = "用户资料存储暂不可用";
|
||||
const USERS_ME_CREDENTIAL_STORAGE_UNAVAILABLE_DETAIL: &str = "用户凭证存储暂不可用";
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct UsersMeUpdateProfileRequest {
|
||||
@@ -97,7 +112,11 @@ pub(super) async fn handle_users_me_detail_put(
|
||||
.await
|
||||
{
|
||||
Ok(Some(_)) => Json(json!({ "message": "个人信息更新成功" })).into_response(),
|
||||
Ok(None) => build_public_support_maintenance_response(USERS_ME_MAINTENANCE_DETAIL),
|
||||
Ok(None) => build_auth_error_response(
|
||||
http::StatusCode::SERVICE_UNAVAILABLE,
|
||||
USERS_ME_PROFILE_STORAGE_UNAVAILABLE_DETAIL,
|
||||
false,
|
||||
),
|
||||
Err(err) => build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("user profile update failed: {err:?}"),
|
||||
@@ -197,7 +216,13 @@ pub(super) async fn handle_users_me_password_patch(
|
||||
.await
|
||||
{
|
||||
Ok(Some(_)) => {}
|
||||
Ok(None) => return build_public_support_maintenance_response(USERS_ME_MAINTENANCE_DETAIL),
|
||||
Ok(None) => {
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::SERVICE_UNAVAILABLE,
|
||||
USERS_ME_CREDENTIAL_STORAGE_UNAVAILABLE_DETAIL,
|
||||
false,
|
||||
)
|
||||
}
|
||||
Err(err) => {
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
|
||||
@@ -1,13 +1,37 @@
|
||||
use super::*;
|
||||
use crate::gateway::handlers::public::support::build_unhandled_public_support_response;
|
||||
use axum::{body::Body, http, response::Response};
|
||||
|
||||
pub(crate) async fn maybe_build_local_users_me_legacy_response(
|
||||
use super::{
|
||||
handle_auth_me, handle_users_me_api_key_capabilities_put, handle_users_me_api_key_create,
|
||||
handle_users_me_api_key_delete, handle_users_me_api_key_detail_get,
|
||||
handle_users_me_api_key_patch, handle_users_me_api_key_providers_put,
|
||||
handle_users_me_api_key_update, handle_users_me_api_keys_get, handle_users_me_available_models,
|
||||
handle_users_me_delete_other_sessions, handle_users_me_delete_session,
|
||||
handle_users_me_detail_put, handle_users_me_endpoint_status_get,
|
||||
handle_users_me_management_token_create, handle_users_me_management_token_delete,
|
||||
handle_users_me_management_token_detail_get, handle_users_me_management_token_regenerate,
|
||||
handle_users_me_management_token_toggle, handle_users_me_management_token_update,
|
||||
handle_users_me_management_tokens_list, handle_users_me_model_capabilities_get,
|
||||
handle_users_me_model_capabilities_put, handle_users_me_password_patch,
|
||||
handle_users_me_preferences_get, handle_users_me_preferences_put,
|
||||
handle_users_me_providers_get, handle_users_me_sessions_get, handle_users_me_update_session,
|
||||
handle_users_me_usage_active_get, handle_users_me_usage_get, handle_users_me_usage_heatmap_get,
|
||||
handle_users_me_usage_interval_timeline_get, users_me_api_key_capabilities_path_matches,
|
||||
users_me_api_key_detail_path_matches, users_me_api_key_providers_path_matches,
|
||||
users_me_management_token_detail_path_matches,
|
||||
users_me_management_token_regenerate_path_matches,
|
||||
users_me_management_token_toggle_path_matches, users_me_management_tokens_root,
|
||||
users_me_session_detail_path_matches, AppState, GatewayPublicRequestContext,
|
||||
};
|
||||
|
||||
pub(crate) async fn maybe_build_local_users_me_response(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
headers: &http::HeaderMap,
|
||||
request_body: Option<&axum::body::Bytes>,
|
||||
) -> Option<Response<Body>> {
|
||||
let decision = request_context.control_decision.as_ref()?;
|
||||
if decision.route_family.as_deref() != Some("users_me_legacy") {
|
||||
if decision.route_family.as_deref() != Some("users_me") {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -30,16 +54,12 @@ pub(crate) async fn maybe_build_local_users_me_legacy_response(
|
||||
Some(handle_users_me_delete_other_sessions(state, request_context, headers).await)
|
||||
}
|
||||
Some("session_delete")
|
||||
if request_context
|
||||
.request_path
|
||||
.starts_with("/api/users/me/sessions/") =>
|
||||
if users_me_session_detail_path_matches(&request_context.request_path) =>
|
||||
{
|
||||
Some(handle_users_me_delete_session(state, request_context, headers).await)
|
||||
}
|
||||
Some("session_update")
|
||||
if request_context
|
||||
.request_path
|
||||
.starts_with("/api/users/me/sessions/") =>
|
||||
if users_me_session_detail_path_matches(&request_context.request_path) =>
|
||||
{
|
||||
Some(
|
||||
handle_users_me_update_session(state, request_context, headers, request_body).await,
|
||||
@@ -72,32 +92,24 @@ pub(crate) async fn maybe_build_local_users_me_legacy_response(
|
||||
)
|
||||
}
|
||||
Some("api_key_detail")
|
||||
if request_context
|
||||
.request_path
|
||||
.starts_with("/api/users/me/api-keys/") =>
|
||||
if users_me_api_key_detail_path_matches(&request_context.request_path) =>
|
||||
{
|
||||
Some(handle_users_me_api_key_detail_get(state, request_context, headers).await)
|
||||
}
|
||||
Some("management_token_detail")
|
||||
if request_context
|
||||
.request_path
|
||||
.starts_with("/api/me/management-tokens/") =>
|
||||
if users_me_management_token_detail_path_matches(&request_context.request_path) =>
|
||||
{
|
||||
Some(handle_users_me_management_token_detail_get(state, request_context, headers).await)
|
||||
}
|
||||
Some("api_key_update")
|
||||
if request_context
|
||||
.request_path
|
||||
.starts_with("/api/users/me/api-keys/") =>
|
||||
if users_me_api_key_detail_path_matches(&request_context.request_path) =>
|
||||
{
|
||||
Some(
|
||||
handle_users_me_api_key_update(state, request_context, headers, request_body).await,
|
||||
)
|
||||
}
|
||||
Some("management_token_update")
|
||||
if request_context
|
||||
.request_path
|
||||
.starts_with("/api/me/management-tokens/") =>
|
||||
if users_me_management_token_detail_path_matches(&request_context.request_path) =>
|
||||
{
|
||||
Some(
|
||||
handle_users_me_management_token_update(
|
||||
@@ -110,37 +122,27 @@ pub(crate) async fn maybe_build_local_users_me_legacy_response(
|
||||
)
|
||||
}
|
||||
Some("api_key_patch")
|
||||
if request_context
|
||||
.request_path
|
||||
.starts_with("/api/users/me/api-keys/") =>
|
||||
if users_me_api_key_detail_path_matches(&request_context.request_path) =>
|
||||
{
|
||||
Some(handle_users_me_api_key_patch(state, request_context, headers, request_body).await)
|
||||
}
|
||||
Some("management_token_toggle")
|
||||
if request_context
|
||||
.request_path
|
||||
.starts_with("/api/me/management-tokens/") =>
|
||||
if users_me_management_token_toggle_path_matches(&request_context.request_path) =>
|
||||
{
|
||||
Some(handle_users_me_management_token_toggle(state, request_context, headers).await)
|
||||
}
|
||||
Some("api_key_delete")
|
||||
if request_context
|
||||
.request_path
|
||||
.starts_with("/api/users/me/api-keys/") =>
|
||||
if users_me_api_key_detail_path_matches(&request_context.request_path) =>
|
||||
{
|
||||
Some(handle_users_me_api_key_delete(state, request_context, headers).await)
|
||||
}
|
||||
Some("management_token_delete")
|
||||
if request_context
|
||||
.request_path
|
||||
.starts_with("/api/me/management-tokens/") =>
|
||||
if users_me_management_token_detail_path_matches(&request_context.request_path) =>
|
||||
{
|
||||
Some(handle_users_me_management_token_delete(state, request_context, headers).await)
|
||||
}
|
||||
Some("api_key_providers_update")
|
||||
if request_context
|
||||
.request_path
|
||||
.starts_with("/api/users/me/api-keys/") =>
|
||||
if users_me_api_key_providers_path_matches(&request_context.request_path) =>
|
||||
{
|
||||
Some(
|
||||
handle_users_me_api_key_providers_put(
|
||||
@@ -153,9 +155,7 @@ pub(crate) async fn maybe_build_local_users_me_legacy_response(
|
||||
)
|
||||
}
|
||||
Some("api_key_capabilities_update")
|
||||
if request_context
|
||||
.request_path
|
||||
.starts_with("/api/users/me/api-keys/") =>
|
||||
if users_me_api_key_capabilities_path_matches(&request_context.request_path) =>
|
||||
{
|
||||
Some(
|
||||
handle_users_me_api_key_capabilities_put(
|
||||
@@ -168,9 +168,7 @@ pub(crate) async fn maybe_build_local_users_me_legacy_response(
|
||||
)
|
||||
}
|
||||
Some("management_token_regenerate")
|
||||
if request_context
|
||||
.request_path
|
||||
.starts_with("/api/me/management-tokens/") =>
|
||||
if users_me_management_token_regenerate_path_matches(&request_context.request_path) =>
|
||||
{
|
||||
Some(handle_users_me_management_token_regenerate(state, request_context, headers).await)
|
||||
}
|
||||
@@ -230,8 +228,6 @@ pub(crate) async fn maybe_build_local_users_me_legacy_response(
|
||||
.await,
|
||||
)
|
||||
}
|
||||
_ => Some(build_public_support_maintenance_response(
|
||||
USERS_ME_MAINTENANCE_DETAIL,
|
||||
)),
|
||||
_ => Some(build_unhandled_public_support_response(request_context)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,17 @@
|
||||
use super::*;
|
||||
use axum::{
|
||||
body::Body,
|
||||
http,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
|
||||
use super::{
|
||||
build_auth_error_response, format_users_me_optional_datetime_iso8601,
|
||||
format_users_me_required_session_datetime_iso8601, resolve_authenticated_local_user, AppState,
|
||||
GatewayPublicRequestContext,
|
||||
};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct UsersMeUpdateSessionLabelRequest {
|
||||
@@ -6,16 +19,18 @@ struct UsersMeUpdateSessionLabelRequest {
|
||||
}
|
||||
|
||||
fn users_me_session_id_from_path(request_path: &str) -> Option<String> {
|
||||
request_path
|
||||
let raw = request_path
|
||||
.strip_prefix("/api/users/me/sessions/")?
|
||||
.trim()
|
||||
.trim_matches('/')
|
||||
.split('/')
|
||||
.next()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.filter(|value| !value.contains('/'))
|
||||
.map(ToOwned::to_owned)
|
||||
.trim_matches('/');
|
||||
if raw.is_empty() || raw.contains('/') {
|
||||
return None;
|
||||
}
|
||||
Some(raw.to_string())
|
||||
}
|
||||
|
||||
pub(super) fn users_me_session_detail_path_matches(request_path: &str) -> bool {
|
||||
users_me_session_id_from_path(request_path).is_some()
|
||||
}
|
||||
|
||||
fn build_users_me_session_payload(
|
||||
@@ -131,7 +146,7 @@ pub(super) async fn handle_users_me_delete_session(
|
||||
Err(response) => return response,
|
||||
};
|
||||
let Some(session_id) = users_me_session_id_from_path(&request_context.request_path) else {
|
||||
return build_public_support_maintenance_response(USERS_ME_MAINTENANCE_DETAIL);
|
||||
return build_auth_error_response(http::StatusCode::NOT_FOUND, "会话不存在", false);
|
||||
};
|
||||
|
||||
let session = match state.find_user_session(&auth.user.id, &session_id).await {
|
||||
@@ -181,7 +196,7 @@ pub(super) async fn handle_users_me_update_session(
|
||||
Err(response) => return response,
|
||||
};
|
||||
let Some(session_id) = users_me_session_id_from_path(&request_context.request_path) else {
|
||||
return build_public_support_maintenance_response(USERS_ME_MAINTENANCE_DETAIL);
|
||||
return build_auth_error_response(http::StatusCode::NOT_FOUND, "会话不存在", false);
|
||||
};
|
||||
let Some(request_body) = request_body else {
|
||||
return build_auth_error_response(http::StatusCode::BAD_REQUEST, "请求数据验证失败", false);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use super::*;
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
use crate::gateway::gateway_data::StoredUserSessionRecord;
|
||||
|
||||
pub(super) fn format_users_me_optional_datetime_iso8601(
|
||||
value: Option<chrono::DateTime<chrono::Utc>>,
|
||||
value: Option<DateTime<Utc>>,
|
||||
) -> Option<String> {
|
||||
value.map(|value| value.to_rfc3339())
|
||||
}
|
||||
@@ -9,11 +11,11 @@ pub(super) fn format_users_me_optional_datetime_iso8601(
|
||||
pub(super) fn format_users_me_optional_unix_secs_iso8601(value: Option<u64>) -> Option<String> {
|
||||
let secs = value?;
|
||||
let secs = i64::try_from(secs).ok()?;
|
||||
chrono::DateTime::<chrono::Utc>::from_timestamp(secs, 0).map(|value| value.to_rfc3339())
|
||||
DateTime::<Utc>::from_timestamp(secs, 0).map(|value| value.to_rfc3339())
|
||||
}
|
||||
|
||||
pub(super) fn format_users_me_required_session_datetime_iso8601(
|
||||
session: &crate::gateway::gateway_data::StoredUserSessionRecord,
|
||||
session: &StoredUserSessionRecord,
|
||||
) -> Option<String> {
|
||||
session
|
||||
.created_at
|
||||
|
||||
@@ -1,4 +1,30 @@
|
||||
use super::*;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use axum::{
|
||||
body::Body,
|
||||
http,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use chrono::Utc;
|
||||
use serde_json::json;
|
||||
|
||||
use super::{
|
||||
admin_stats_bad_request_response, build_auth_error_response, build_auth_wallet_summary_payload,
|
||||
list_usage_for_optional_range, parse_bounded_u32, query_param_value,
|
||||
resolve_authenticated_local_user, round_to, unix_secs_to_rfc3339, AdminStatsTimeRange,
|
||||
AdminStatsUsageFilter, AppState, GatewayPublicRequestContext,
|
||||
};
|
||||
|
||||
const USERS_ME_USAGE_DATA_UNAVAILABLE_DETAIL: &str = "用户用量数据暂不可用";
|
||||
|
||||
fn build_users_me_usage_reader_unavailable_response() -> Response<Body> {
|
||||
build_auth_error_response(
|
||||
http::StatusCode::SERVICE_UNAVAILABLE,
|
||||
USERS_ME_USAGE_DATA_UNAVAILABLE_DETAIL,
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
fn parse_users_me_usage_limit(query: Option<&str>) -> Result<usize, String> {
|
||||
match query_param_value(query, "limit") {
|
||||
@@ -522,7 +548,7 @@ pub(super) async fn handle_users_me_usage_get(
|
||||
headers: &http::HeaderMap,
|
||||
) -> Response<Body> {
|
||||
if !state.has_usage_data_reader() {
|
||||
return build_public_support_maintenance_response(USERS_ME_MAINTENANCE_DETAIL);
|
||||
return build_users_me_usage_reader_unavailable_response();
|
||||
}
|
||||
|
||||
let auth = match resolve_authenticated_local_user(state, request_context, headers).await {
|
||||
@@ -680,7 +706,7 @@ pub(super) async fn handle_users_me_usage_active_get(
|
||||
headers: &http::HeaderMap,
|
||||
) -> Response<Body> {
|
||||
if !state.has_usage_data_reader() {
|
||||
return build_public_support_maintenance_response(USERS_ME_MAINTENANCE_DETAIL);
|
||||
return build_users_me_usage_reader_unavailable_response();
|
||||
}
|
||||
|
||||
let auth = match resolve_authenticated_local_user(state, request_context, headers).await {
|
||||
@@ -740,7 +766,7 @@ pub(super) async fn handle_users_me_usage_interval_timeline_get(
|
||||
headers: &http::HeaderMap,
|
||||
) -> Response<Body> {
|
||||
if !state.has_usage_data_reader() {
|
||||
return build_public_support_maintenance_response(USERS_ME_MAINTENANCE_DETAIL);
|
||||
return build_users_me_usage_reader_unavailable_response();
|
||||
}
|
||||
|
||||
let auth = match resolve_authenticated_local_user(state, request_context, headers).await {
|
||||
@@ -819,7 +845,7 @@ pub(super) async fn handle_users_me_usage_heatmap_get(
|
||||
headers: &http::HeaderMap,
|
||||
) -> Response<Body> {
|
||||
if !state.has_usage_data_reader() {
|
||||
return build_public_support_maintenance_response(USERS_ME_MAINTENANCE_DETAIL);
|
||||
return build_users_me_usage_reader_unavailable_response();
|
||||
}
|
||||
|
||||
let auth = match resolve_authenticated_local_user(state, request_context, headers).await {
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
use super::*;
|
||||
use sqlx::Row;
|
||||
pub(super) use super::{
|
||||
build_auth_error_response, build_auth_json_response, build_auth_wallet_summary_payload,
|
||||
query_param_value, resolve_authenticated_local_user, unix_secs_to_rfc3339, AppState,
|
||||
GatewayError, GatewayPublicRequestContext,
|
||||
};
|
||||
pub(super) use axum::{body::Body, http, response::Response};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "wallet/test_support.rs"]
|
||||
@@ -7,7 +11,12 @@ mod test_support;
|
||||
#[cfg(test)]
|
||||
pub(crate) use self::test_support::wallet_test_recharge_store;
|
||||
#[cfg(test)]
|
||||
use self::test_support::*;
|
||||
use self::test_support::{
|
||||
record_wallet_test_recharge, record_wallet_test_refund, wallet_test_recharge_order_by_id,
|
||||
wallet_test_recharge_orders_for_user, wallet_test_refund_by_id,
|
||||
wallet_test_refund_by_idempotency, wallet_test_refunds_for_wallet,
|
||||
wallet_test_reserved_refund_amount,
|
||||
};
|
||||
#[path = "wallet/flow.rs"]
|
||||
mod flow;
|
||||
#[path = "wallet/reads.rs"]
|
||||
@@ -25,15 +34,19 @@ use self::reads::{
|
||||
};
|
||||
use self::recharge::{
|
||||
handle_wallet_create_recharge, handle_wallet_recharge_detail, handle_wallet_recharge_list,
|
||||
wallet_recharge_detail_path_matches,
|
||||
};
|
||||
pub(crate) use self::recharge::{
|
||||
sanitize_wallet_gateway_response, wallet_payment_order_payload_from_row,
|
||||
};
|
||||
use self::refunds::{
|
||||
handle_wallet_create_refund, handle_wallet_refund_detail, handle_wallet_refunds_list,
|
||||
wallet_refund_detail_path_matches,
|
||||
};
|
||||
|
||||
const WALLET_LEGACY_TIMEZONE: &str = "Asia/Shanghai";
|
||||
const WALLET_RECHARGE_STORAGE_UNAVAILABLE_DETAIL: &str = "钱包充值后端暂不可用";
|
||||
const WALLET_REFUND_STORAGE_UNAVAILABLE_DETAIL: &str = "钱包退款后端暂不可用";
|
||||
const WALLET_SAFE_GATEWAY_RESPONSE_KEYS: &[&str] = &[
|
||||
"gateway",
|
||||
"display_name",
|
||||
@@ -61,14 +74,30 @@ pub(super) fn wallet_normalize_optional_string_field(
|
||||
Ok(Some(trimmed.to_string()))
|
||||
}
|
||||
|
||||
pub(super) async fn maybe_build_local_wallet_legacy_response(
|
||||
pub(super) fn build_wallet_recharge_storage_unavailable_response() -> Response<Body> {
|
||||
build_auth_error_response(
|
||||
http::StatusCode::SERVICE_UNAVAILABLE,
|
||||
WALLET_RECHARGE_STORAGE_UNAVAILABLE_DETAIL,
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn build_wallet_refund_storage_unavailable_response() -> Response<Body> {
|
||||
build_auth_error_response(
|
||||
http::StatusCode::SERVICE_UNAVAILABLE,
|
||||
WALLET_REFUND_STORAGE_UNAVAILABLE_DETAIL,
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) async fn maybe_build_local_wallet_response(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
headers: &http::HeaderMap,
|
||||
request_body: Option<&axum::body::Bytes>,
|
||||
) -> Option<Response<Body>> {
|
||||
let decision = request_context.control_decision.as_ref()?;
|
||||
if decision.route_family.as_deref() != Some("wallet_legacy") {
|
||||
if decision.route_family.as_deref() != Some("wallet") {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -103,9 +132,7 @@ pub(super) async fn maybe_build_local_wallet_legacy_response(
|
||||
}
|
||||
|
||||
if decision.route_kind.as_deref() == Some("refund_detail")
|
||||
&& request_context
|
||||
.request_path
|
||||
.starts_with("/api/wallet/refunds/")
|
||||
&& wallet_refund_detail_path_matches(&request_context.request_path)
|
||||
{
|
||||
return Some(handle_wallet_refund_detail(state, request_context, headers).await);
|
||||
}
|
||||
@@ -133,12 +160,54 @@ pub(super) async fn maybe_build_local_wallet_legacy_response(
|
||||
}
|
||||
|
||||
if decision.route_kind.as_deref() == Some("recharge_detail")
|
||||
&& request_context
|
||||
.request_path
|
||||
.starts_with("/api/wallet/recharge/")
|
||||
&& wallet_recharge_detail_path_matches(&request_context.request_path)
|
||||
{
|
||||
return Some(handle_wallet_recharge_detail(state, request_context, headers).await);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
build_wallet_recharge_storage_unavailable_response,
|
||||
build_wallet_refund_storage_unavailable_response,
|
||||
WALLET_RECHARGE_STORAGE_UNAVAILABLE_DETAIL, WALLET_REFUND_STORAGE_UNAVAILABLE_DETAIL,
|
||||
};
|
||||
use axum::body::to_bytes;
|
||||
use axum::http;
|
||||
use serde_json::json;
|
||||
|
||||
#[tokio::test]
|
||||
async fn wallet_recharge_storage_unavailable_response_is_explicit_local_503() {
|
||||
let response = build_wallet_recharge_storage_unavailable_response();
|
||||
|
||||
assert_eq!(response.status(), http::StatusCode::SERVICE_UNAVAILABLE);
|
||||
let body = to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("body should read");
|
||||
let payload: serde_json::Value =
|
||||
serde_json::from_slice(&body).expect("json body should parse");
|
||||
assert_eq!(
|
||||
payload,
|
||||
json!({ "detail": WALLET_RECHARGE_STORAGE_UNAVAILABLE_DETAIL })
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn wallet_refund_storage_unavailable_response_is_explicit_local_503() {
|
||||
let response = build_wallet_refund_storage_unavailable_response();
|
||||
|
||||
assert_eq!(response.status(), http::StatusCode::SERVICE_UNAVAILABLE);
|
||||
let body = to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("body should read");
|
||||
let payload: serde_json::Value =
|
||||
serde_json::from_slice(&body).expect("json body should parse");
|
||||
assert_eq!(
|
||||
payload,
|
||||
json!({ "detail": WALLET_REFUND_STORAGE_UNAVAILABLE_DETAIL })
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
use super::*;
|
||||
use super::{
|
||||
build_auth_error_response, build_auth_json_response, build_wallet_daily_usage_payload,
|
||||
build_wallet_payload, build_wallet_zero_today_entry, http, parse_wallet_limit,
|
||||
parse_wallet_offset, resolve_authenticated_local_user, unix_secs_to_rfc3339,
|
||||
wallet_fixed_offset, wallet_today_billing_date_string, wallet_transaction_payload_from_row,
|
||||
AppState, Body, GatewayPublicRequestContext, Response, WALLET_LEGACY_TIMEZONE,
|
||||
};
|
||||
use serde_json::json;
|
||||
use sqlx::Row;
|
||||
|
||||
fn wallet_flow_sort_key(item_type: &str, payload: &serde_json::Value) -> (String, u8, String) {
|
||||
match item_type {
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
use super::*;
|
||||
use super::{
|
||||
build_auth_error_response, build_auth_json_response, build_auth_wallet_summary_payload, http,
|
||||
query_param_value, resolve_authenticated_local_user, unix_secs_to_rfc3339, AppState, Body,
|
||||
GatewayError, GatewayPublicRequestContext, Response, WALLET_LEGACY_TIMEZONE,
|
||||
};
|
||||
use crate::gateway::handlers::admin::round_to;
|
||||
use chrono::Utc;
|
||||
use serde_json::json;
|
||||
use sqlx::Row;
|
||||
|
||||
const WALLET_TODAY_COST_UNAVAILABLE_DETAIL: &str = "钱包今日费用数据暂不可用";
|
||||
|
||||
pub(super) fn build_wallet_payload(
|
||||
wallet: Option<&aether_data::repository::wallet::StoredWalletSnapshot>,
|
||||
@@ -179,8 +189,10 @@ pub(super) async fn handle_wallet_today_cost(
|
||||
headers: &http::HeaderMap,
|
||||
) -> Response<Body> {
|
||||
if !state.has_usage_data_reader() {
|
||||
return build_public_support_maintenance_response(
|
||||
"Wallet routes require Rust maintenance backend",
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::SERVICE_UNAVAILABLE,
|
||||
WALLET_TODAY_COST_UNAVAILABLE_DETAIL,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,20 @@
|
||||
use super::*;
|
||||
use super::{
|
||||
build_auth_error_response, build_auth_json_response, build_wallet_payload,
|
||||
build_wallet_recharge_storage_unavailable_response, http, parse_wallet_limit,
|
||||
parse_wallet_offset, resolve_authenticated_local_user, unix_secs_to_rfc3339,
|
||||
wallet_normalize_optional_string_field, AppState, Body, GatewayError,
|
||||
GatewayPublicRequestContext, Response, WALLET_SAFE_GATEWAY_RESPONSE_KEYS,
|
||||
};
|
||||
#[cfg(test)]
|
||||
use super::{
|
||||
record_wallet_test_recharge, wallet_test_recharge_order_by_id,
|
||||
wallet_test_recharge_orders_for_user,
|
||||
};
|
||||
use chrono::Utc;
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
use sqlx::Row;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct WalletCreateRechargeRequest {
|
||||
@@ -114,16 +130,17 @@ fn wallet_checkout_payload(
|
||||
}
|
||||
|
||||
fn wallet_order_id_from_path(request_path: &str) -> Option<String> {
|
||||
request_path
|
||||
.strip_prefix("/api/wallet/recharge/")?
|
||||
.trim()
|
||||
.trim_matches('/')
|
||||
.split('/')
|
||||
.next()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.filter(|value| !value.contains('/'))
|
||||
.map(ToOwned::to_owned)
|
||||
let trimmed = request_path.trim_end_matches('/');
|
||||
let order_id = trimmed.strip_prefix("/api/wallet/recharge/")?.trim();
|
||||
if order_id.is_empty() || order_id.contains('/') {
|
||||
None
|
||||
} else {
|
||||
Some(order_id.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn wallet_recharge_detail_path_matches(request_path: &str) -> bool {
|
||||
wallet_order_id_from_path(request_path).is_some()
|
||||
}
|
||||
|
||||
pub(crate) fn sanitize_wallet_gateway_response(
|
||||
@@ -357,9 +374,7 @@ pub(super) async fn handle_wallet_create_recharge(
|
||||
);
|
||||
}
|
||||
#[cfg(not(test))]
|
||||
return build_public_support_maintenance_response(
|
||||
"Wallet routes require Rust maintenance backend",
|
||||
);
|
||||
return build_wallet_recharge_storage_unavailable_response();
|
||||
};
|
||||
|
||||
let mut tx = match pool.begin().await {
|
||||
|
||||
@@ -1,4 +1,20 @@
|
||||
use super::*;
|
||||
use super::{
|
||||
build_auth_error_response, build_auth_json_response, build_wallet_payload,
|
||||
build_wallet_refund_storage_unavailable_response, http, parse_wallet_limit,
|
||||
parse_wallet_offset, resolve_authenticated_local_user, unix_secs_to_rfc3339,
|
||||
wallet_normalize_optional_string_field, AppState, Body, GatewayError,
|
||||
GatewayPublicRequestContext, Response,
|
||||
};
|
||||
#[cfg(test)]
|
||||
use super::{
|
||||
record_wallet_test_refund, wallet_test_refund_by_id, wallet_test_refund_by_idempotency,
|
||||
wallet_test_refunds_for_wallet, wallet_test_reserved_refund_amount,
|
||||
};
|
||||
use chrono::Utc;
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
use sqlx::Row;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct WalletCreateRefundRequest {
|
||||
@@ -65,16 +81,17 @@ fn wallet_build_refund_no(now: chrono::DateTime<chrono::Utc>) -> String {
|
||||
}
|
||||
|
||||
fn wallet_refund_id_from_path(request_path: &str) -> Option<String> {
|
||||
request_path
|
||||
.strip_prefix("/api/wallet/refunds/")?
|
||||
.trim()
|
||||
.trim_matches('/')
|
||||
.split('/')
|
||||
.next()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.filter(|value| !value.contains('/'))
|
||||
.map(ToOwned::to_owned)
|
||||
let trimmed = request_path.trim_end_matches('/');
|
||||
let refund_id = trimmed.strip_prefix("/api/wallet/refunds/")?.trim();
|
||||
if refund_id.is_empty() || refund_id.contains('/') {
|
||||
None
|
||||
} else {
|
||||
Some(refund_id.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn wallet_refund_detail_path_matches(request_path: &str) -> bool {
|
||||
wallet_refund_id_from_path(request_path).is_some()
|
||||
}
|
||||
|
||||
fn wallet_refund_payload_from_row(
|
||||
@@ -491,9 +508,7 @@ pub(super) async fn handle_wallet_create_refund(
|
||||
return build_auth_json_response(http::StatusCode::OK, created, None);
|
||||
}
|
||||
#[cfg(not(test))]
|
||||
return build_public_support_maintenance_response(
|
||||
"Wallet refund routes require Rust maintenance backend",
|
||||
);
|
||||
return build_wallet_refund_storage_unavailable_response();
|
||||
};
|
||||
|
||||
let mut tx = match pool.begin().await {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(super) struct WalletTestRefundRecord {
|
||||
pub(crate) wallet_id: String,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
pub(crate) use super::*;
|
||||
|
||||
#[path = "system_modules_helpers/capabilities.rs"]
|
||||
mod system_modules_capabilities;
|
||||
#[path = "system_modules_helpers/keys_grouped.rs"]
|
||||
@@ -9,7 +7,28 @@ mod system_modules_modules;
|
||||
#[path = "system_modules_helpers/system.rs"]
|
||||
mod system_modules_system;
|
||||
|
||||
pub(crate) use self::system_modules_capabilities::*;
|
||||
pub(crate) use self::system_modules_keys_grouped::*;
|
||||
pub(crate) use self::system_modules_modules::*;
|
||||
pub(crate) use self::system_modules_system::*;
|
||||
pub(crate) use self::system_modules_capabilities::{
|
||||
capability_detail_by_name, enabled_key_capability_short_names, serialize_public_capability,
|
||||
supported_capability_names, PUBLIC_CAPABILITY_DEFINITIONS,
|
||||
};
|
||||
pub(crate) use self::system_modules_keys_grouped::build_admin_keys_grouped_by_format_payload;
|
||||
pub(crate) use self::system_modules_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,
|
||||
build_public_auth_modules_status_payload, ldap_module_config_is_valid,
|
||||
AdminSetModuleEnabledRequest,
|
||||
};
|
||||
pub(crate) use self::system_modules_system::{
|
||||
apply_admin_email_template_update, apply_admin_system_config_update,
|
||||
apply_admin_system_settings_update, build_admin_api_formats_payload,
|
||||
build_admin_email_template_payload, build_admin_email_templates_payload,
|
||||
build_admin_system_check_update_payload, build_admin_system_config_detail_payload,
|
||||
build_admin_system_config_export_payload, build_admin_system_configs_payload,
|
||||
build_admin_system_settings_payload, build_admin_system_stats_payload,
|
||||
build_admin_system_users_export_payload, current_aether_version, delete_admin_system_config,
|
||||
escape_admin_email_template_html, module_available_from_env, preview_admin_email_template,
|
||||
read_admin_email_template_payload, render_admin_email_template_html,
|
||||
reset_admin_email_template, serialize_admin_system_users_export_wallet, system_config_bool,
|
||||
system_config_string,
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) struct PublicCapabilityDefinition {
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
use super::*;
|
||||
use super::enabled_key_capability_short_names;
|
||||
use crate::gateway::handlers::{json_string_list, masked_catalog_api_key, unix_secs_to_rfc3339};
|
||||
use crate::gateway::AppState;
|
||||
use serde_json::json;
|
||||
use std::collections::BTreeMap;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
pub(crate) async fn build_admin_keys_grouped_by_format_payload(
|
||||
state: &AppState,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use super::*;
|
||||
use super::{module_available_from_env, system_config_bool};
|
||||
use crate::gateway::{AppState, GatewayError};
|
||||
use serde_json::json;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct PublicAuthModuleDefinition {
|
||||
|
||||
@@ -1,4 +1,16 @@
|
||||
use super::*;
|
||||
use crate::gateway::api::ai::admin_endpoint_signature_parts;
|
||||
use crate::gateway::handlers::{decrypt_catalog_secret_with_fallbacks, unix_secs_to_rfc3339};
|
||||
use crate::gateway::{AppState, GatewayError};
|
||||
use aether_crypto::encrypt_python_fernet_plaintext;
|
||||
use aether_data::repository::global_models::{
|
||||
AdminGlobalModelListQuery, AdminProviderModelListQuery,
|
||||
};
|
||||
use axum::body::Bytes;
|
||||
use axum::http;
|
||||
use chrono::Utc;
|
||||
use serde_json::json;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::fs;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct AdminApiFormatDefinition {
|
||||
@@ -219,7 +231,7 @@ pub(crate) async fn build_admin_system_settings_payload(
|
||||
|
||||
pub(crate) async fn apply_admin_system_settings_update(
|
||||
state: &AppState,
|
||||
request_body: &axum::body::Bytes,
|
||||
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,
|
||||
@@ -603,7 +615,7 @@ pub(crate) fn render_admin_email_template_html(
|
||||
pub(crate) async fn apply_admin_email_template_update(
|
||||
state: &AppState,
|
||||
template_type: &str,
|
||||
request_body: &axum::body::Bytes,
|
||||
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((
|
||||
@@ -677,7 +689,7 @@ pub(crate) async fn apply_admin_email_template_update(
|
||||
pub(crate) async fn preview_admin_email_template(
|
||||
state: &AppState,
|
||||
template_type: &str,
|
||||
request_body: Option<&axum::body::Bytes>,
|
||||
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((
|
||||
@@ -1550,7 +1562,7 @@ pub(crate) async fn build_admin_system_config_detail_payload(
|
||||
pub(crate) async fn apply_admin_system_config_update(
|
||||
state: &AppState,
|
||||
requested_key: &str,
|
||||
request_body: &axum::body::Bytes,
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user