mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +08:00
fix(public): 修复用户可见性、额度、验证与 Codex 探测
This commit is contained in:
@@ -8,8 +8,7 @@ use self::invalid::{
|
||||
codex_structured_invalid_reason,
|
||||
};
|
||||
use self::parse::{
|
||||
build_codex_quota_exhausted_fallback_metadata, parse_codex_usage_headers,
|
||||
parse_codex_wham_usage_response,
|
||||
parse_codex_backend_me_response, parse_codex_usage_headers, parse_codex_wham_usage_response,
|
||||
};
|
||||
use self::plan::{build_codex_quota_request_spec, execute_codex_quota_plan};
|
||||
use super::shared::{
|
||||
@@ -111,7 +110,7 @@ pub(crate) async fn refresh_codex_provider_quota_locally(
|
||||
"key_id": key.id,
|
||||
"key_name": key.name,
|
||||
"status": "error",
|
||||
"message": format!("wham/usage 请求执行失败: {detail}"),
|
||||
"message": format!("backend-api/me 请求执行失败: {detail}"),
|
||||
"status_code": 502,
|
||||
}));
|
||||
continue;
|
||||
@@ -138,7 +137,9 @@ pub(crate) async fn refresh_codex_provider_quota_locally(
|
||||
.as_ref()
|
||||
.and_then(|body| body.json_body.as_ref())
|
||||
{
|
||||
if let Some(parsed) = parse_codex_wham_usage_response(body_json, now_unix_secs) {
|
||||
if let Some(parsed) = parse_codex_backend_me_response(body_json, now_unix_secs)
|
||||
.or_else(|| parse_codex_wham_usage_response(body_json, now_unix_secs))
|
||||
{
|
||||
metadata_update = Some(json!({
|
||||
"codex": merge_codex_quota_metadata(header_metadata.as_ref(), &parsed)
|
||||
}));
|
||||
@@ -151,21 +152,21 @@ pub(crate) async fn refresh_codex_provider_quota_locally(
|
||||
status = "success".to_string();
|
||||
} else {
|
||||
status = "no_metadata".to_string();
|
||||
message = Some("响应中未包含限额信息".to_string());
|
||||
message = Some("backend-api/me 响应中未包含账号信息".to_string());
|
||||
}
|
||||
} else {
|
||||
message = Some("无法解析 wham/usage API 响应".to_string());
|
||||
message = Some("无法解析 backend-api/me API 响应".to_string());
|
||||
}
|
||||
} else {
|
||||
let err_msg = extract_execution_error_message(&result);
|
||||
message = Some(match err_msg.as_deref() {
|
||||
Some(detail) if !detail.is_empty() => {
|
||||
format!(
|
||||
"wham/usage API 返回状态码 {}: {}",
|
||||
"backend-api/me API 返回状态码 {}: {}",
|
||||
result.status_code, detail
|
||||
)
|
||||
}
|
||||
_ => format!("wham/usage API 返回状态码 {}", result.status_code),
|
||||
_ => format!("backend-api/me API 返回状态码 {}", result.status_code),
|
||||
});
|
||||
|
||||
match result.status_code {
|
||||
@@ -222,26 +223,14 @@ pub(crate) async fn refresh_codex_provider_quota_locally(
|
||||
oauth_invalid_reason = reason;
|
||||
status = "workspace_deactivated".to_string();
|
||||
} else {
|
||||
let plan_type = transport
|
||||
.key
|
||||
.decrypted_auth_config
|
||||
.as_deref()
|
||||
.and_then(|raw| serde_json::from_str::<serde_json::Value>(raw).ok())
|
||||
.and_then(|value| {
|
||||
value
|
||||
.get("plan_type")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(ToOwned::to_owned)
|
||||
});
|
||||
metadata_update = Some(json!({
|
||||
"codex": build_codex_quota_exhausted_fallback_metadata(
|
||||
plan_type.as_deref(),
|
||||
now_unix_secs,
|
||||
)
|
||||
}));
|
||||
(oauth_invalid_at_unix_secs, oauth_invalid_reason) =
|
||||
quota_refresh_success_invalid_state(&key);
|
||||
status = "quota_exhausted".to_string();
|
||||
let (at, reason) = codex_build_invalid_state(
|
||||
&key,
|
||||
codex_structured_invalid_reason(402, err_msg.as_deref()),
|
||||
now_unix_secs,
|
||||
);
|
||||
oauth_invalid_at_unix_secs = at;
|
||||
oauth_invalid_reason = reason;
|
||||
status = "payment_required".to_string();
|
||||
}
|
||||
}
|
||||
403 => {
|
||||
|
||||
@@ -22,6 +22,13 @@ pub(super) fn parse_codex_wham_usage_response(
|
||||
admin_provider_quota_pure::parse_codex_wham_usage_response(value, updated_at_unix_secs)
|
||||
}
|
||||
|
||||
pub(super) fn parse_codex_backend_me_response(
|
||||
value: &serde_json::Value,
|
||||
updated_at_unix_secs: u64,
|
||||
) -> Option<serde_json::Value> {
|
||||
admin_provider_quota_pure::parse_codex_backend_me_response(value, updated_at_unix_secs)
|
||||
}
|
||||
|
||||
pub(super) fn parse_codex_usage_headers(
|
||||
headers: &BTreeMap<String, String>,
|
||||
updated_at_unix_secs: u64,
|
||||
|
||||
@@ -226,7 +226,7 @@ pub(crate) struct AdminProviderUpdateRequest {
|
||||
|
||||
pub(crate) type AdminProviderUpdatePatch = AdminTypedObjectPatch<AdminProviderUpdateRequest>;
|
||||
|
||||
pub(crate) const CODEX_WHAM_USAGE_URL: &str = "https://chatgpt.com/backend-api/wham/usage";
|
||||
pub(crate) const CODEX_WHAM_USAGE_URL: &str = "https://chatgpt.com/backend-api/me";
|
||||
pub(crate) const KIRO_USAGE_LIMITS_PATH: &str = "/getUsageLimits";
|
||||
pub(crate) const KIRO_USAGE_SDK_VERSION: &str = "1.0.0";
|
||||
pub(crate) const ANTIGRAVITY_FETCH_AVAILABLE_MODELS_PATH: &str = "/v1internal:fetchAvailableModels";
|
||||
|
||||
@@ -50,6 +50,28 @@ pub(crate) fn normalize_admin_base_url(base_url: &str) -> Result<String, String>
|
||||
Ok(normalized.to_string())
|
||||
}
|
||||
|
||||
pub(crate) fn sanitize_public_model_config_for_user(
|
||||
config: Option<serde_json::Value>,
|
||||
) -> Option<serde_json::Value> {
|
||||
let Some(mut config) = config else {
|
||||
return None;
|
||||
};
|
||||
if let Some(object) = config.as_object_mut() {
|
||||
for key in [
|
||||
"model_mappings",
|
||||
"model_mapping",
|
||||
"global_model_mappings",
|
||||
"provider_model_mappings",
|
||||
"provider_model_aliases",
|
||||
"mapping_preview",
|
||||
"model_mapping_preview",
|
||||
] {
|
||||
object.remove(key);
|
||||
}
|
||||
}
|
||||
Some(config)
|
||||
}
|
||||
|
||||
pub(crate) fn admin_requested_force_stream(value: &serde_json::Value) -> bool {
|
||||
match value {
|
||||
serde_json::Value::Bool(value) => *value,
|
||||
@@ -165,21 +187,12 @@ pub(crate) async fn build_public_providers_payload(
|
||||
.into_iter()
|
||||
.map(|provider| {
|
||||
let provider_id = provider.id.clone();
|
||||
let description = provider
|
||||
.config
|
||||
.as_ref()
|
||||
.and_then(|value| value.get("description"))
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(ToOwned::to_owned);
|
||||
let model_count = models_by_provider
|
||||
.get(&provider_id)
|
||||
.map(BTreeSet::len)
|
||||
.unwrap_or(0);
|
||||
json!({
|
||||
"id": provider_id.clone(),
|
||||
"name": provider.name,
|
||||
"description": description,
|
||||
"website": provider.website,
|
||||
"is_active": provider.is_active,
|
||||
"provider_priority": provider.provider_priority,
|
||||
"models_count": model_count,
|
||||
@@ -196,8 +209,6 @@ pub(crate) async fn build_public_providers_payload(
|
||||
fn serialize_public_catalog_model(model: StoredPublicCatalogModel) -> serde_json::Value {
|
||||
json!({
|
||||
"id": model.id,
|
||||
"provider_id": model.provider_id,
|
||||
"provider_name": model.provider_name,
|
||||
"name": model.name,
|
||||
"display_name": model.display_name,
|
||||
"description": model.description,
|
||||
|
||||
@@ -11,7 +11,7 @@ pub(crate) use self::catalog_helpers::{
|
||||
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_ms, request_candidate_status_label,
|
||||
ApiFormatHealthMonitorOptions,
|
||||
sanitize_public_model_config_for_user, ApiFormatHealthMonitorOptions,
|
||||
};
|
||||
pub(crate) use self::system_modules_helpers::{
|
||||
build_admin_keys_grouped_by_format_payload, build_public_auth_modules_status_payload,
|
||||
|
||||
@@ -2,8 +2,8 @@ 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, ldap_module_config_is_valid,
|
||||
serialize_public_capability, supported_capability_names, ApiFormatHealthMonitorOptions,
|
||||
PUBLIC_CAPABILITY_DEFINITIONS,
|
||||
sanitize_public_model_config_for_user, serialize_public_capability, supported_capability_names,
|
||||
ApiFormatHealthMonitorOptions, PUBLIC_CAPABILITY_DEFINITIONS,
|
||||
};
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::handlers::shared::{
|
||||
@@ -232,10 +232,17 @@ pub(crate) async fn maybe_build_local_public_support_response(
|
||||
.flatten()
|
||||
.and_then(|value| value.as_str().map(ToOwned::to_owned))
|
||||
.unwrap_or_else(|| "AI Gateway".to_string());
|
||||
let show_github_link = state
|
||||
.read_system_config_json_value("show_github_link")
|
||||
.await
|
||||
.ok()
|
||||
.flatten();
|
||||
let show_github_link = system_config_bool(show_github_link.as_ref(), true);
|
||||
return Some(
|
||||
Json(json!({
|
||||
"site_name": site_name,
|
||||
"site_subtitle": site_subtitle,
|
||||
"show_github_link": show_github_link,
|
||||
}))
|
||||
.into_response(),
|
||||
);
|
||||
@@ -374,7 +381,7 @@ pub(crate) async fn maybe_build_local_public_support_response(
|
||||
"default_price_per_request": model.default_price_per_request,
|
||||
"default_tiered_pricing": model.default_tiered_pricing,
|
||||
"supported_capabilities": model.supported_capabilities,
|
||||
"config": model.config,
|
||||
"config": sanitize_public_model_config_for_user(model.config),
|
||||
"usage_count": model.usage_count,
|
||||
})
|
||||
})
|
||||
@@ -576,15 +583,11 @@ pub(crate) async fn maybe_build_local_public_support_response(
|
||||
.await
|
||||
.ok()
|
||||
.unwrap_or_default();
|
||||
let current_provider = providers
|
||||
.first()
|
||||
.map(|provider| provider.name.clone())
|
||||
.unwrap_or_else(|| "None".to_string());
|
||||
return Some(
|
||||
Json(json!({
|
||||
"message": "AI Proxy with Modular Architecture v4.0.0",
|
||||
"status": "running",
|
||||
"current_provider": current_provider,
|
||||
"current_provider": serde_json::Value::Null,
|
||||
"available_providers": providers.len(),
|
||||
"config": {},
|
||||
"endpoints": {
|
||||
@@ -647,10 +650,8 @@ pub(crate) async fn maybe_build_local_public_support_response(
|
||||
.into_iter()
|
||||
.map(|provider| {
|
||||
let provider_id = provider.id.clone();
|
||||
let provider_name = provider.name.clone();
|
||||
let mut payload = json!({
|
||||
"id": provider_id.clone(),
|
||||
"name": provider_name,
|
||||
"is_active": provider.is_active,
|
||||
"provider_priority": provider.provider_priority,
|
||||
});
|
||||
@@ -661,7 +662,6 @@ pub(crate) async fn maybe_build_local_public_support_response(
|
||||
.filter(|endpoint| endpoint.provider_id == provider_id)
|
||||
.map(|endpoint| json!({
|
||||
"id": endpoint.id,
|
||||
"base_url": endpoint.base_url,
|
||||
"api_format": endpoint.api_format,
|
||||
"is_active": endpoint.is_active,
|
||||
}))
|
||||
@@ -709,19 +709,19 @@ pub(crate) async fn maybe_build_local_public_support_response(
|
||||
};
|
||||
let provider = match provider {
|
||||
Some(provider) => provider,
|
||||
None => state
|
||||
.list_provider_catalog_providers(false)
|
||||
.await
|
||||
.ok()
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.find(|provider| provider.name == provider_identifier)?,
|
||||
None => {
|
||||
return Some(
|
||||
(
|
||||
http::StatusCode::NOT_FOUND,
|
||||
Json(json!({ "detail": "Provider not found" })),
|
||||
)
|
||||
.into_response(),
|
||||
);
|
||||
}
|
||||
};
|
||||
let provider_id = provider.id.clone();
|
||||
let provider_name = provider.name.clone();
|
||||
let mut payload = json!({
|
||||
"id": provider_id.clone(),
|
||||
"name": provider_name,
|
||||
"is_active": provider.is_active,
|
||||
"provider_priority": provider.provider_priority,
|
||||
});
|
||||
@@ -739,7 +739,6 @@ pub(crate) async fn maybe_build_local_public_support_response(
|
||||
.map(|endpoint| {
|
||||
json!({
|
||||
"id": endpoint.id,
|
||||
"base_url": endpoint.base_url,
|
||||
"api_format": endpoint.api_format,
|
||||
"is_active": endpoint.is_active,
|
||||
})
|
||||
|
||||
@@ -18,6 +18,10 @@ pub(super) use serde_json::json;
|
||||
mod auth_helpers;
|
||||
pub(crate) use auth_helpers::*;
|
||||
|
||||
#[path = "auth_turnstile.rs"]
|
||||
mod auth_turnstile;
|
||||
use auth_turnstile::*;
|
||||
|
||||
#[path = "auth_email.rs"]
|
||||
mod auth_email;
|
||||
use auth_email::*;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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,
|
||||
auth_turnstile_public_settings, 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(
|
||||
@@ -40,12 +40,17 @@ pub(crate) async fn build_auth_registration_settings_payload(
|
||||
Some(value) if matches!(value.as_str(), "weak" | "medium" | "strong") => value,
|
||||
_ => "weak".to_string(),
|
||||
};
|
||||
let turnstile_settings = auth_turnstile_public_settings(state)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.detail))?;
|
||||
|
||||
Ok(json!({
|
||||
"enable_registration": enable_registration,
|
||||
"require_email_verification": require_email_verification,
|
||||
"email_configured": email_configured,
|
||||
"password_policy_level": password_policy_level,
|
||||
"turnstile_enabled": turnstile_settings.enabled,
|
||||
"turnstile_site_key": turnstile_settings.site_key,
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@ use super::{
|
||||
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,
|
||||
system_config_string, system_config_string_list, verify_auth_turnstile_token, AppState, Body,
|
||||
GatewayError, Regex, Response,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
|
||||
@@ -16,11 +17,13 @@ struct AuthRegisterRequest {
|
||||
email: Option<String>,
|
||||
username: String,
|
||||
password: String,
|
||||
turnstile_token: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct AuthEmailRequest {
|
||||
email: String,
|
||||
turnstile_token: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -195,20 +198,6 @@ pub(super) async fn handle_auth_send_verification_code(
|
||||
return build_auth_error_response(http::StatusCode::BAD_REQUEST, "邮箱格式无效", false);
|
||||
};
|
||||
|
||||
if state
|
||||
.find_user_auth_by_identifier(&email)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.is_some()
|
||||
{
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"该邮箱已被注册,请直接登录或使用其他邮箱",
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
match validate_auth_email_suffix(state, &email).await {
|
||||
Ok(Ok(())) => {}
|
||||
Ok(Err(detail)) => {
|
||||
@@ -223,6 +212,26 @@ pub(super) async fn handle_auth_send_verification_code(
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(err) =
|
||||
verify_auth_turnstile_token(state, payload.turnstile_token.as_deref(), None).await
|
||||
{
|
||||
return build_auth_error_response(err.status, err.detail, false);
|
||||
}
|
||||
|
||||
if state
|
||||
.find_user_auth_by_identifier(&email)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.is_some()
|
||||
{
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"该邮箱已被注册,请直接登录或使用其他邮箱",
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
let smtp_config = match read_auth_smtp_config(state).await {
|
||||
Ok(Some(value)) => value,
|
||||
Ok(None) => {
|
||||
@@ -421,6 +430,10 @@ pub(super) async fn handle_auth_register(
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if let Err(err) =
|
||||
verify_auth_turnstile_token(state, payload.turnstile_token.as_deref(), None).await
|
||||
{
|
||||
return build_auth_error_response(err.status, err.detail, false);
|
||||
}
|
||||
if let Some(email) = email.as_deref() {
|
||||
match validate_auth_email_suffix(state, email).await {
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
use super::{
|
||||
decrypt_catalog_secret_with_fallbacks, http, system_config_bool, system_config_string,
|
||||
system_config_string_list, AppState,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use std::time::Duration;
|
||||
|
||||
const TURNSTILE_SITEVERIFY_URL: &str = "https://challenges.cloudflare.com/turnstile/v0/siteverify";
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(super) struct AuthTurnstilePublicSettings {
|
||||
pub(super) enabled: bool,
|
||||
pub(super) site_key: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) struct AuthTurnstileError {
|
||||
pub(super) status: http::StatusCode,
|
||||
pub(super) detail: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct AuthTurnstileConfig {
|
||||
enabled: bool,
|
||||
site_key: Option<String>,
|
||||
secret_key: Option<String>,
|
||||
siteverify_url: String,
|
||||
allowed_hostnames: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct TurnstileSiteverifyResponse {
|
||||
success: bool,
|
||||
hostname: Option<String>,
|
||||
#[serde(default, rename = "error-codes")]
|
||||
error_codes: Vec<String>,
|
||||
}
|
||||
|
||||
fn turnstile_error(status: http::StatusCode, detail: impl Into<String>) -> AuthTurnstileError {
|
||||
AuthTurnstileError {
|
||||
status,
|
||||
detail: detail.into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn turnstile_config_error(detail: impl Into<String>) -> AuthTurnstileError {
|
||||
turnstile_error(http::StatusCode::INTERNAL_SERVER_ERROR, detail)
|
||||
}
|
||||
|
||||
async fn read_auth_turnstile_config(
|
||||
state: &AppState,
|
||||
) -> Result<AuthTurnstileConfig, AuthTurnstileError> {
|
||||
let enabled = state
|
||||
.read_system_config_json_value("turnstile_enabled")
|
||||
.await
|
||||
.map_err(|err| {
|
||||
turnstile_config_error(format!("auth turnstile settings lookup failed: {err:?}"))
|
||||
})?;
|
||||
let site_key = state
|
||||
.read_system_config_json_value("turnstile_site_key")
|
||||
.await
|
||||
.map_err(|err| {
|
||||
turnstile_config_error(format!("auth turnstile settings lookup failed: {err:?}"))
|
||||
})?;
|
||||
let secret_key = state
|
||||
.read_system_config_json_value("turnstile_secret_key")
|
||||
.await
|
||||
.map_err(|err| {
|
||||
turnstile_config_error(format!("auth turnstile settings lookup failed: {err:?}"))
|
||||
})?;
|
||||
let siteverify_url = state
|
||||
.read_system_config_json_value("turnstile_siteverify_url")
|
||||
.await
|
||||
.map_err(|err| {
|
||||
turnstile_config_error(format!("auth turnstile settings lookup failed: {err:?}"))
|
||||
})?;
|
||||
let allowed_hostnames = state
|
||||
.read_system_config_json_value("turnstile_allowed_hostnames")
|
||||
.await
|
||||
.map_err(|err| {
|
||||
turnstile_config_error(format!("auth turnstile settings lookup failed: {err:?}"))
|
||||
})?;
|
||||
|
||||
let secret_key = system_config_string(secret_key.as_ref()).map(|value| {
|
||||
decrypt_catalog_secret_with_fallbacks(state.encryption_key(), &value).unwrap_or(value)
|
||||
});
|
||||
|
||||
Ok(AuthTurnstileConfig {
|
||||
enabled: system_config_bool(enabled.as_ref(), false),
|
||||
site_key: system_config_string(site_key.as_ref()),
|
||||
secret_key,
|
||||
siteverify_url: system_config_string(siteverify_url.as_ref())
|
||||
.unwrap_or_else(|| TURNSTILE_SITEVERIFY_URL.to_string()),
|
||||
allowed_hostnames: system_config_string_list(allowed_hostnames.as_ref()),
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) async fn auth_turnstile_public_settings(
|
||||
state: &AppState,
|
||||
) -> Result<AuthTurnstilePublicSettings, AuthTurnstileError> {
|
||||
let config = read_auth_turnstile_config(state).await?;
|
||||
let enabled = config.enabled && config.site_key.is_some();
|
||||
Ok(AuthTurnstilePublicSettings {
|
||||
enabled,
|
||||
site_key: enabled.then_some(config.site_key).flatten(),
|
||||
})
|
||||
}
|
||||
|
||||
fn turnstile_service_error(error_codes: &[String]) -> bool {
|
||||
error_codes.iter().any(|code| {
|
||||
matches!(
|
||||
code.trim(),
|
||||
"missing-input-secret" | "invalid-input-secret" | "internal-error"
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn turnstile_hostname_allowed(hostname: Option<&str>, allowed_hostnames: &[String]) -> bool {
|
||||
if allowed_hostnames.is_empty() {
|
||||
return true;
|
||||
}
|
||||
let Some(hostname) = hostname.map(str::trim).filter(|value| !value.is_empty()) else {
|
||||
return false;
|
||||
};
|
||||
let hostname = hostname.to_ascii_lowercase();
|
||||
allowed_hostnames.iter().any(|allowed| allowed == &hostname)
|
||||
}
|
||||
|
||||
pub(super) async fn verify_auth_turnstile_token(
|
||||
state: &AppState,
|
||||
token: Option<&str>,
|
||||
remote_ip: Option<&str>,
|
||||
) -> Result<(), AuthTurnstileError> {
|
||||
let config = read_auth_turnstile_config(state).await?;
|
||||
if !config.enabled || config.site_key.is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
let token = token
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.ok_or_else(|| turnstile_error(http::StatusCode::BAD_REQUEST, "请先完成人机验证"))?;
|
||||
let secret_key = config.secret_key.as_deref().ok_or_else(|| {
|
||||
turnstile_error(http::StatusCode::SERVICE_UNAVAILABLE, "人机验证服务未配置")
|
||||
})?;
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(Duration::from_secs(8))
|
||||
.build()
|
||||
.map_err(|err| {
|
||||
turnstile_error(
|
||||
http::StatusCode::SERVICE_UNAVAILABLE,
|
||||
format!("人机验证服务暂不可用: {err}"),
|
||||
)
|
||||
})?;
|
||||
let mut form = vec![
|
||||
("secret", secret_key.to_string()),
|
||||
("response", token.to_string()),
|
||||
];
|
||||
if let Some(remote_ip) = remote_ip.map(str::trim).filter(|value| !value.is_empty()) {
|
||||
form.push(("remoteip", remote_ip.to_string()));
|
||||
}
|
||||
|
||||
let response = client
|
||||
.post(config.siteverify_url)
|
||||
.form(&form)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|_| {
|
||||
turnstile_error(
|
||||
http::StatusCode::SERVICE_UNAVAILABLE,
|
||||
"人机验证服务暂不可用",
|
||||
)
|
||||
})?;
|
||||
if !response.status().is_success() {
|
||||
return Err(turnstile_error(
|
||||
http::StatusCode::SERVICE_UNAVAILABLE,
|
||||
"人机验证服务暂不可用",
|
||||
));
|
||||
}
|
||||
|
||||
let payload = response
|
||||
.json::<TurnstileSiteverifyResponse>()
|
||||
.await
|
||||
.map_err(|_| {
|
||||
turnstile_error(
|
||||
http::StatusCode::SERVICE_UNAVAILABLE,
|
||||
"人机验证服务暂不可用",
|
||||
)
|
||||
})?;
|
||||
if payload.success {
|
||||
if turnstile_hostname_allowed(payload.hostname.as_deref(), &config.allowed_hostnames) {
|
||||
return Ok(());
|
||||
}
|
||||
return Err(turnstile_error(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"人机验证失败,请重试",
|
||||
));
|
||||
}
|
||||
if turnstile_service_error(&payload.error_codes) {
|
||||
return Err(turnstile_error(
|
||||
http::StatusCode::SERVICE_UNAVAILABLE,
|
||||
"人机验证服务暂不可用",
|
||||
));
|
||||
}
|
||||
Err(turnstile_error(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"人机验证失败,请重试",
|
||||
))
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
use super::super::support_wallet::build_wallet_balance_payload_for_user;
|
||||
use super::{
|
||||
build_auth_error_response, query_param_value, resolve_authenticated_local_user, AppState,
|
||||
GatewayError, GatewayPublicRequestContext,
|
||||
@@ -180,6 +181,35 @@ fn dashboard_format_usd(value: f64) -> String {
|
||||
format!("${:.2}", dashboard_round_f64(value, 2))
|
||||
}
|
||||
|
||||
fn dashboard_json_f64(value: Option<&serde_json::Value>) -> f64 {
|
||||
value.and_then(serde_json::Value::as_f64).unwrap_or(0.0)
|
||||
}
|
||||
|
||||
fn dashboard_wallet_card_value_and_subvalue(
|
||||
wallet_payload: &serde_json::Value,
|
||||
) -> (String, String) {
|
||||
let unlimited = wallet_payload
|
||||
.get("unlimited")
|
||||
.and_then(serde_json::Value::as_bool)
|
||||
.unwrap_or(false);
|
||||
if unlimited {
|
||||
return ("无限额度".to_string(), "无限额度".to_string());
|
||||
}
|
||||
|
||||
let package_balance = dashboard_json_f64(wallet_payload.get("package_balance")).max(0.0);
|
||||
let wallet_balance = dashboard_json_f64(wallet_payload.get("wallet_balance")).max(0.0);
|
||||
let total_available =
|
||||
dashboard_json_f64(wallet_payload.get("total_available_balance")).max(0.0);
|
||||
(
|
||||
dashboard_format_usd(total_available),
|
||||
format!(
|
||||
"套餐额度 {} · 钱包余额 {}",
|
||||
dashboard_format_usd(package_balance),
|
||||
dashboard_format_usd(wallet_balance)
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn dashboard_format_percentage(value: f64) -> String {
|
||||
format!("{:.1}%", dashboard_round_f64(value, 1))
|
||||
}
|
||||
@@ -1050,26 +1080,10 @@ pub(super) async fn handle_dashboard_stats_get(
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let wallet_value = wallet
|
||||
.as_ref()
|
||||
.map(|wallet| {
|
||||
if wallet.limit_mode.eq_ignore_ascii_case("unlimited") {
|
||||
"无限制".to_string()
|
||||
} else {
|
||||
dashboard_format_usd(wallet.balance)
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| dashboard_format_usd(0.0));
|
||||
let wallet_sub_value = wallet
|
||||
.as_ref()
|
||||
.map(|wallet| {
|
||||
if wallet.limit_mode.eq_ignore_ascii_case("unlimited") {
|
||||
"无限额度".to_string()
|
||||
} else {
|
||||
format!("赠款 {}", dashboard_format_usd(wallet.gift_balance))
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| "暂无钱包".to_string());
|
||||
let wallet_payload =
|
||||
build_wallet_balance_payload_for_user(state, &auth.user.id, wallet.as_ref()).await;
|
||||
let (wallet_value, wallet_sub_value) =
|
||||
dashboard_wallet_card_value_and_subvalue(&wallet_payload);
|
||||
let payload = json!({
|
||||
"stats": [
|
||||
{
|
||||
@@ -1305,6 +1319,19 @@ pub(super) async fn handle_dashboard_provider_status_get(
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
headers: &http::HeaderMap,
|
||||
) -> Response<Body> {
|
||||
let auth = match resolve_authenticated_local_user(state, request_context, headers).await {
|
||||
Ok(value) => value,
|
||||
Err(response) => return response,
|
||||
};
|
||||
|
||||
if !dashboard_role_is_admin(&auth.user.role) {
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::FORBIDDEN,
|
||||
"仅管理员可查看供应商状态",
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
if !state.has_usage_data_reader() {
|
||||
return dashboard_backend_unavailable_response("Usage data backend unavailable");
|
||||
}
|
||||
@@ -1312,16 +1339,7 @@ pub(super) async fn handle_dashboard_provider_status_get(
|
||||
return dashboard_backend_unavailable_response("Provider catalog backend unavailable");
|
||||
}
|
||||
|
||||
let auth = match resolve_authenticated_local_user(state, request_context, headers).await {
|
||||
Ok(value) => value,
|
||||
Err(response) => return response,
|
||||
};
|
||||
|
||||
let cache_identity = if dashboard_role_is_admin(&auth.user.role) {
|
||||
"admin"
|
||||
} else {
|
||||
auth.user.id.as_str()
|
||||
};
|
||||
let cache_identity = "admin";
|
||||
let cache_key = format!("provider:{cache_identity}");
|
||||
let cache_ttl = std::time::Duration::from_secs(20);
|
||||
|
||||
@@ -1398,11 +1416,7 @@ pub(super) async fn handle_dashboard_provider_status_get(
|
||||
.cmp(right["name"].as_str().unwrap_or_default())
|
||||
})
|
||||
});
|
||||
let limit = if dashboard_role_is_admin(&auth.user.role) {
|
||||
10
|
||||
} else {
|
||||
5
|
||||
};
|
||||
let limit = 10;
|
||||
if entries.len() > limit {
|
||||
entries.truncate(limit);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ use axum::{
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
const PUBLIC_MODELS_OWNER: &str = "aether";
|
||||
|
||||
pub(crate) fn build_models_auth_error_response(api_format: &str) -> Response<Body> {
|
||||
match api_format {
|
||||
"claude:messages" => (
|
||||
@@ -109,7 +111,7 @@ pub(super) fn build_openai_models_list_response(
|
||||
"id": row.global_model_name,
|
||||
"object": "model",
|
||||
"created": 0,
|
||||
"owned_by": row.provider_name,
|
||||
"owned_by": PUBLIC_MODELS_OWNER,
|
||||
})
|
||||
}).collect::<Vec<_>>(),
|
||||
}))
|
||||
@@ -123,7 +125,7 @@ pub(super) fn build_openai_model_detail_response(
|
||||
"id": row.global_model_name,
|
||||
"object": "model",
|
||||
"created": 0,
|
||||
"owned_by": row.provider_name,
|
||||
"owned_by": PUBLIC_MODELS_OWNER,
|
||||
}))
|
||||
.into_response()
|
||||
}
|
||||
|
||||
@@ -338,7 +338,7 @@ pub(super) async fn maybe_build_local_test_connection_route_response(
|
||||
Some(
|
||||
Json(json!({
|
||||
"status": "success",
|
||||
"provider": provider.name,
|
||||
"provider_id": provider.id,
|
||||
"endpoint_id": endpoint.id,
|
||||
"api_format": format_value,
|
||||
"timestamp": timestamp,
|
||||
|
||||
@@ -8,12 +8,13 @@ pub(super) fn select_test_connection_provider(
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty());
|
||||
if let Some(provider_query) = provider_query {
|
||||
if let Some(provider) = providers.iter().find(|provider| {
|
||||
provider.id.eq_ignore_ascii_case(provider_query)
|
||||
|| provider.name.eq_ignore_ascii_case(provider_query)
|
||||
}) {
|
||||
if let Some(provider) = providers
|
||||
.iter()
|
||||
.find(|provider| provider.id.eq_ignore_ascii_case(provider_query))
|
||||
{
|
||||
return Some(provider.clone());
|
||||
}
|
||||
return None;
|
||||
}
|
||||
providers.into_iter().next()
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ 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,
|
||||
handle_users_me_api_key_install_session_create, query_param_optional_bool, query_param_value,
|
||||
resolve_authenticated_local_user, unix_secs_to_rfc3339,
|
||||
resolve_authenticated_local_user, sanitize_public_model_config_for_user, unix_secs_to_rfc3339,
|
||||
users_me_api_key_install_sessions_path_matches, validate_auth_register_password, AppState,
|
||||
AuthenticatedLocalUserContext, GatewayPublicRequestContext, PUBLIC_CAPABILITY_DEFINITIONS,
|
||||
};
|
||||
|
||||
@@ -14,15 +14,23 @@ 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,
|
||||
resolve_authenticated_local_user, sanitize_public_model_config_for_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: StoredPublicGlobalModel) -> serde_json::Value {
|
||||
fn build_users_me_available_model_payload(
|
||||
model: StoredPublicGlobalModel,
|
||||
hide_mapping_config: bool,
|
||||
) -> serde_json::Value {
|
||||
let config = if hide_mapping_config {
|
||||
sanitize_public_model_config_for_user(model.config)
|
||||
} else {
|
||||
model.config
|
||||
};
|
||||
json!({
|
||||
"id": model.id,
|
||||
"name": model.name,
|
||||
@@ -31,7 +39,7 @@ fn build_users_me_available_model_payload(model: StoredPublicGlobalModel) -> ser
|
||||
"default_price_per_request": model.default_price_per_request,
|
||||
"default_tiered_pricing": model.default_tiered_pricing,
|
||||
"supported_capabilities": model.supported_capabilities,
|
||||
"config": model.config,
|
||||
"config": config,
|
||||
"usage_count": model.usage_count,
|
||||
})
|
||||
}
|
||||
@@ -49,35 +57,27 @@ fn parse_users_me_available_models_query(query: Option<&str>) -> (usize, usize,
|
||||
}
|
||||
|
||||
fn users_me_allowed_provider_names(
|
||||
user: &aether_data::repository::users::StoredUserAuthRecord,
|
||||
allowed_providers: Option<&[String]>,
|
||||
) -> Option<BTreeSet<String>> {
|
||||
if user.role.eq_ignore_ascii_case("admin") {
|
||||
return None;
|
||||
}
|
||||
|
||||
user.allowed_providers
|
||||
.as_ref()
|
||||
.map(|providers| {
|
||||
providers
|
||||
.iter()
|
||||
.map(|value| value.trim().to_ascii_lowercase())
|
||||
.filter(|value| !value.is_empty())
|
||||
.collect::<BTreeSet<_>>()
|
||||
})
|
||||
.filter(|providers| !providers.is_empty())
|
||||
allowed_providers.map(|providers| {
|
||||
providers
|
||||
.iter()
|
||||
.map(|value| value.trim().to_ascii_lowercase())
|
||||
.filter(|value| !value.is_empty())
|
||||
.collect::<BTreeSet<_>>()
|
||||
})
|
||||
}
|
||||
|
||||
async fn resolve_users_me_allowed_global_model_ids(
|
||||
state: &AppState,
|
||||
user: &aether_data::repository::users::StoredUserAuthRecord,
|
||||
allowed_providers: Option<&[String]>,
|
||||
) -> Result<Option<BTreeSet<String>>, Response<Body>> {
|
||||
let Some(allowed_providers) = user
|
||||
.allowed_providers
|
||||
.as_ref()
|
||||
.filter(|providers| !providers.is_empty())
|
||||
else {
|
||||
let Some(allowed_providers) = allowed_providers else {
|
||||
return Ok(None);
|
||||
};
|
||||
if allowed_providers.is_empty() {
|
||||
return Ok(Some(BTreeSet::new()));
|
||||
}
|
||||
|
||||
if !state.has_provider_catalog_data_reader() {
|
||||
return Err(build_auth_error_response(
|
||||
@@ -155,10 +155,35 @@ pub(super) async fn handle_users_me_available_models(
|
||||
let (skip, limit, search) =
|
||||
parse_users_me_available_models_query(request_context.request_query_string.as_deref());
|
||||
|
||||
let effective_policies = if auth.user.role.eq_ignore_ascii_case("admin") {
|
||||
None
|
||||
} else {
|
||||
match state
|
||||
.data
|
||||
.resolve_user_effective_list_policies(&auth.user)
|
||||
.await
|
||||
{
|
||||
Ok(value) => Some(value),
|
||||
Err(err) => {
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("user policy lookup failed: {err:?}"),
|
||||
false,
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
let provider_model_ids = if auth.user.role.eq_ignore_ascii_case("admin") {
|
||||
None
|
||||
} else {
|
||||
match resolve_users_me_allowed_global_model_ids(state, &auth.user).await {
|
||||
match resolve_users_me_allowed_global_model_ids(
|
||||
state,
|
||||
effective_policies
|
||||
.as_ref()
|
||||
.and_then(|policies| policies.allowed_providers.as_deref()),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(value) => value,
|
||||
Err(response) => return response,
|
||||
}
|
||||
@@ -166,9 +191,9 @@ pub(super) async fn handle_users_me_available_models(
|
||||
let allowed_models: Option<BTreeSet<String>> = if auth.user.role.eq_ignore_ascii_case("admin") {
|
||||
None
|
||||
} else {
|
||||
auth.user
|
||||
.allowed_models
|
||||
effective_policies
|
||||
.as_ref()
|
||||
.and_then(|policies| policies.allowed_models.as_ref())
|
||||
.map(|models: &Vec<String>| {
|
||||
models
|
||||
.iter()
|
||||
@@ -176,9 +201,18 @@ pub(super) async fn handle_users_me_available_models(
|
||||
.filter(|value: &String| !value.is_empty())
|
||||
.collect::<BTreeSet<_>>()
|
||||
})
|
||||
.filter(|models: &BTreeSet<String>| !models.is_empty())
|
||||
};
|
||||
|
||||
let hide_mapping_config = !auth.user.role.eq_ignore_ascii_case("admin");
|
||||
|
||||
let allowed_models: Option<BTreeSet<String>> = allowed_models.map(|models| {
|
||||
models
|
||||
.into_iter()
|
||||
.map(|value| value.trim().to_ascii_lowercase())
|
||||
.filter(|value| !value.is_empty())
|
||||
.collect::<BTreeSet<_>>()
|
||||
});
|
||||
|
||||
let page = if provider_model_ids.is_none() && allowed_models.is_none() {
|
||||
match state
|
||||
.list_public_global_models(&PublicGlobalModelQuery {
|
||||
@@ -247,7 +281,7 @@ pub(super) async fn handle_users_me_available_models(
|
||||
"models": page
|
||||
.items
|
||||
.into_iter()
|
||||
.map(build_users_me_available_model_payload)
|
||||
.map(|model| build_users_me_available_model_payload(model, hide_mapping_config))
|
||||
.collect::<Vec<_>>(),
|
||||
"total": page.total,
|
||||
}))
|
||||
@@ -271,7 +305,26 @@ pub(super) async fn handle_users_me_providers_get(
|
||||
Ok(value) => value,
|
||||
Err(response) => return response,
|
||||
};
|
||||
let allowed_provider_names = users_me_allowed_provider_names(&auth.user);
|
||||
let expose_provider_details = auth.user.role.eq_ignore_ascii_case("admin");
|
||||
let allowed_provider_names = if expose_provider_details {
|
||||
None
|
||||
} else {
|
||||
let effective_policies = match state
|
||||
.data
|
||||
.resolve_user_effective_list_policies(&auth.user)
|
||||
.await
|
||||
{
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("user policy lookup failed: {err:?}"),
|
||||
false,
|
||||
)
|
||||
}
|
||||
};
|
||||
users_me_allowed_provider_names(effective_policies.allowed_providers.as_deref())
|
||||
};
|
||||
|
||||
let mut providers = match state.list_provider_catalog_providers(true).await {
|
||||
Ok(value) => value,
|
||||
@@ -315,15 +368,18 @@ pub(super) async fn handle_users_me_providers_get(
|
||||
};
|
||||
let mut endpoints_by_provider = BTreeMap::<String, Vec<serde_json::Value>>::new();
|
||||
for endpoint in endpoints {
|
||||
let mut endpoint_payload = json!({
|
||||
"id": endpoint.id,
|
||||
"api_format": endpoint.api_format,
|
||||
"is_active": endpoint.is_active,
|
||||
});
|
||||
if expose_provider_details {
|
||||
endpoint_payload["base_url"] = json!(endpoint.base_url);
|
||||
}
|
||||
endpoints_by_provider
|
||||
.entry(endpoint.provider_id)
|
||||
.or_default()
|
||||
.push(json!({
|
||||
"id": endpoint.id,
|
||||
"api_format": endpoint.api_format,
|
||||
"base_url": endpoint.base_url,
|
||||
"is_active": endpoint.is_active,
|
||||
}));
|
||||
.push(endpoint_payload);
|
||||
}
|
||||
|
||||
let mut models_by_provider = BTreeMap::<String, Vec<serde_json::Value>>::new();
|
||||
@@ -375,20 +431,23 @@ pub(super) async fn handle_users_me_providers_get(
|
||||
.into_iter()
|
||||
.map(|provider| {
|
||||
let provider_id = provider.id.clone();
|
||||
let description = provider
|
||||
.config
|
||||
.as_ref()
|
||||
.and_then(|value| value.get("description"))
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(ToOwned::to_owned);
|
||||
json!({
|
||||
let mut payload = json!({
|
||||
"id": provider_id.clone(),
|
||||
"name": provider.name,
|
||||
"description": description,
|
||||
"provider_priority": provider.provider_priority,
|
||||
"endpoints": endpoints_by_provider.remove(&provider_id).unwrap_or_default(),
|
||||
"models": models_by_provider.remove(&provider_id).unwrap_or_default(),
|
||||
})
|
||||
});
|
||||
if expose_provider_details {
|
||||
let description = provider
|
||||
.config
|
||||
.as_ref()
|
||||
.and_then(|value| value.get("description"))
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(ToOwned::to_owned);
|
||||
payload["name"] = json!(provider.name);
|
||||
payload["description"] = json!(description);
|
||||
}
|
||||
payload
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
|
||||
@@ -75,12 +75,12 @@ fn validate_user_model_capability_settings(
|
||||
|
||||
fn build_users_me_preferences_payload(
|
||||
preferences: &GatewayUserPreferenceView,
|
||||
expose_provider_details: bool,
|
||||
) -> serde_json::Value {
|
||||
json!({
|
||||
let mut payload = json!({
|
||||
"avatar_url": preferences.avatar_url,
|
||||
"bio": preferences.bio,
|
||||
"default_provider_id": preferences.default_provider_id,
|
||||
"default_provider": preferences.default_provider_name,
|
||||
"theme": preferences.theme,
|
||||
"language": preferences.language,
|
||||
"timezone": preferences.timezone,
|
||||
@@ -89,7 +89,11 @@ fn build_users_me_preferences_payload(
|
||||
"usage_alerts": preferences.usage_alerts,
|
||||
"announcements": preferences.announcement_notifications,
|
||||
},
|
||||
})
|
||||
});
|
||||
if expose_provider_details {
|
||||
payload["default_provider"] = json!(preferences.default_provider_name);
|
||||
}
|
||||
payload
|
||||
}
|
||||
|
||||
fn parse_users_me_optional_string_field(
|
||||
@@ -188,7 +192,11 @@ pub(super) async fn handle_users_me_preferences_get(
|
||||
}
|
||||
};
|
||||
|
||||
Json(build_users_me_preferences_payload(&preferences)).into_response()
|
||||
Json(build_users_me_preferences_payload(
|
||||
&preferences,
|
||||
auth.user.role.eq_ignore_ascii_case("admin"),
|
||||
))
|
||||
.into_response()
|
||||
}
|
||||
|
||||
pub(super) async fn handle_users_me_preferences_put(
|
||||
|
||||
@@ -722,24 +722,26 @@ pub(super) async fn handle_users_me_usage_get(
|
||||
);
|
||||
}
|
||||
};
|
||||
summary_by_provider = match state
|
||||
.summarize_usage_breakdown(&UsageBreakdownSummaryQuery {
|
||||
created_from_unix_secs,
|
||||
created_until_unix_secs,
|
||||
user_id: Some(auth.user.id.clone()),
|
||||
group_by: UsageBreakdownGroupBy::Provider,
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("user usage provider breakdown lookup failed: {err:?}"),
|
||||
false,
|
||||
);
|
||||
}
|
||||
};
|
||||
if include_actual_cost {
|
||||
summary_by_provider = match state
|
||||
.summarize_usage_breakdown(&UsageBreakdownSummaryQuery {
|
||||
created_from_unix_secs,
|
||||
created_until_unix_secs,
|
||||
user_id: Some(auth.user.id.clone()),
|
||||
group_by: UsageBreakdownGroupBy::Provider,
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("user usage provider breakdown lookup failed: {err:?}"),
|
||||
false,
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
summary_by_api_format = match state
|
||||
.summarize_usage_breakdown(&UsageBreakdownSummaryQuery {
|
||||
created_from_unix_secs,
|
||||
@@ -936,7 +938,6 @@ pub(super) async fn handle_users_me_usage_get(
|
||||
"avg_response_time": avg_response_time,
|
||||
"billing": build_auth_wallet_summary_payload(wallet.as_ref()),
|
||||
"summary_by_model": build_users_me_usage_summary_by_model(&summary_by_model, include_actual_cost),
|
||||
"summary_by_provider": build_users_me_usage_summary_by_provider(&summary_by_provider),
|
||||
"summary_by_api_format": build_users_me_usage_summary_by_api_format(&summary_by_api_format),
|
||||
"pagination": {
|
||||
"total": total_record_count,
|
||||
@@ -948,6 +949,9 @@ pub(super) async fn handle_users_me_usage_get(
|
||||
});
|
||||
if include_actual_cost {
|
||||
payload["total_actual_cost"] = json!(total_actual_cost);
|
||||
payload["summary_by_provider"] = json!(build_users_me_usage_summary_by_provider(
|
||||
&summary_by_provider
|
||||
));
|
||||
}
|
||||
Json(payload).into_response()
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ mod redeem;
|
||||
#[path = "wallet/refunds.rs"]
|
||||
mod refunds;
|
||||
use self::flow::handle_wallet_flow;
|
||||
pub(in crate::handlers::public::support) use self::reads::build_wallet_balance_payload_for_user;
|
||||
use self::reads::{
|
||||
build_wallet_daily_usage_payload, build_wallet_payload, build_wallet_zero_today_entry,
|
||||
handle_wallet_balance, handle_wallet_today_cost, handle_wallet_transactions,
|
||||
|
||||
@@ -49,7 +49,7 @@ fn build_wallet_balance_payload(
|
||||
payload
|
||||
}
|
||||
|
||||
async fn build_wallet_balance_payload_for_user(
|
||||
pub(in crate::handlers::public::support) async fn build_wallet_balance_payload_for_user(
|
||||
state: &AppState,
|
||||
user_id: &str,
|
||||
wallet: Option<&aether_data::repository::wallet::StoredWalletSnapshot>,
|
||||
|
||||
Reference in New Issue
Block a user