mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +08:00
refactor: 拆分 gateway 单体为独立 crate,新增 systemd 部署方案
将 gateway 内部的 model-fetch、provider-transport、scheduler-core、 usage-runtime、video-tasks-core 模块提取为独立 crate;重构 gateway 内部模块结构(state/router/cache/data/query 等);移除大量遗留模块 文件;新增 systemd 二进制部署骨架及相关文档;更新前端 usage 相关 API 和组件。
This commit is contained in:
9
apps/aether-gateway/src/state/admin_types.rs
Normal file
9
apps/aether-gateway/src/state/admin_types.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
pub(crate) use aether_data::repository::billing::{
|
||||
AdminBillingCollectorRecord, AdminBillingCollectorWriteInput, AdminBillingPresetApplyResult,
|
||||
AdminBillingRuleRecord, AdminBillingRuleWriteInput,
|
||||
};
|
||||
pub(crate) use aether_data::repository::system::AdminSecurityBlacklistEntry;
|
||||
pub(crate) use aether_data::repository::wallet::{
|
||||
AdminPaymentCallbackRecord, AdminWalletPaymentOrderRecord, AdminWalletRefundRecord,
|
||||
AdminWalletTransactionRecord,
|
||||
};
|
||||
114
apps/aether-gateway/src/state/app.rs
Normal file
114
apps/aether-gateway/src/state/app.rs
Normal file
@@ -0,0 +1,114 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex as StdMutex;
|
||||
|
||||
use aether_runtime::{ConcurrencyGate, DistributedConcurrencyGate};
|
||||
|
||||
use super::super::async_task::{VideoTaskPollerConfig, VideoTaskService};
|
||||
use super::super::fallback_metrics;
|
||||
use super::super::cache::{
|
||||
AuthApiKeyLastUsedCache, AuthContextCache, DirectPlanBypassCache, SchedulerAffinityCache,
|
||||
};
|
||||
use super::super::data::GatewayDataState;
|
||||
use super::super::rate_limit::FrontdoorUserRpmLimiter;
|
||||
use super::super::{provider_transport, usage};
|
||||
use super::{
|
||||
AdminBillingCollectorRecord, AdminBillingRuleRecord, AdminPaymentCallbackRecord,
|
||||
AdminWalletPaymentOrderRecord, AdminWalletRefundRecord, AdminWalletTransactionRecord,
|
||||
CachedProviderTransportSnapshot, FrontdoorCorsConfig, LocalExecutionRuntimeMissDiagnostic,
|
||||
LocalProviderDeleteTaskState, ProviderTransportSnapshotCacheKey,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AppState {
|
||||
#[cfg(test)]
|
||||
pub(crate) execution_runtime_override_base_url: Option<String>,
|
||||
pub(crate) data: Arc<GatewayDataState>,
|
||||
pub(crate) usage_runtime: Arc<usage::UsageRuntime>,
|
||||
pub(crate) video_tasks: Arc<VideoTaskService>,
|
||||
pub(crate) video_task_poller: Option<VideoTaskPollerConfig>,
|
||||
pub(crate) request_gate: Option<Arc<ConcurrencyGate>>,
|
||||
pub(crate) distributed_request_gate: Option<Arc<DistributedConcurrencyGate>>,
|
||||
pub(crate) client: reqwest::Client,
|
||||
pub(crate) auth_context_cache: Arc<AuthContextCache>,
|
||||
pub(crate) auth_api_key_last_used_cache: Arc<AuthApiKeyLastUsedCache>,
|
||||
pub(crate) oauth_refresh: Arc<provider_transport::LocalOAuthRefreshCoordinator>,
|
||||
pub(crate) direct_plan_bypass_cache: Arc<DirectPlanBypassCache>,
|
||||
pub(crate) scheduler_affinity_cache: Arc<SchedulerAffinityCache>,
|
||||
pub(crate) fallback_metrics: Arc<fallback_metrics::GatewayFallbackMetrics>,
|
||||
pub(crate) frontdoor_cors: Option<Arc<FrontdoorCorsConfig>>,
|
||||
pub(crate) frontdoor_user_rpm: Arc<FrontdoorUserRpmLimiter>,
|
||||
pub(crate) tunnel: crate::tunnel::EmbeddedTunnelState,
|
||||
pub(crate) provider_transport_snapshot_cache:
|
||||
Arc<StdMutex<HashMap<ProviderTransportSnapshotCacheKey, CachedProviderTransportSnapshot>>>,
|
||||
pub(crate) provider_key_rpm_resets: Arc<StdMutex<HashMap<String, u64>>>,
|
||||
pub(crate) local_execution_runtime_miss_diagnostics:
|
||||
Arc<StdMutex<HashMap<String, LocalExecutionRuntimeMissDiagnostic>>>,
|
||||
pub(crate) admin_monitoring_error_stats_reset_at: Arc<StdMutex<Option<u64>>>,
|
||||
pub(crate) provider_delete_tasks:
|
||||
Arc<StdMutex<HashMap<String, LocalProviderDeleteTaskState>>>,
|
||||
#[cfg(test)]
|
||||
pub(crate) provider_oauth_state_store:
|
||||
Option<Arc<StdMutex<HashMap<String, String>>>>,
|
||||
#[cfg(test)]
|
||||
pub(crate) provider_oauth_device_session_store:
|
||||
Option<Arc<StdMutex<HashMap<String, String>>>>,
|
||||
#[cfg(test)]
|
||||
pub(crate) provider_oauth_batch_task_store:
|
||||
Option<Arc<StdMutex<HashMap<String, String>>>>,
|
||||
#[cfg(test)]
|
||||
pub(crate) auth_session_store: Option<
|
||||
Arc<
|
||||
StdMutex<HashMap<String, crate::data::state::StoredUserSessionRecord>>,
|
||||
>,
|
||||
>,
|
||||
#[cfg(test)]
|
||||
pub(crate) auth_email_verification_store:
|
||||
Option<Arc<StdMutex<HashMap<String, String>>>>,
|
||||
#[cfg(test)]
|
||||
pub(crate) auth_email_delivery_store: Option<Arc<StdMutex<Vec<serde_json::Value>>>>,
|
||||
#[cfg(test)]
|
||||
pub(crate) auth_user_store: Option<
|
||||
Arc<StdMutex<HashMap<String, aether_data::repository::users::StoredUserAuthRecord>>>,
|
||||
>,
|
||||
#[cfg(test)]
|
||||
pub(crate) auth_user_model_capability_store:
|
||||
Option<Arc<StdMutex<HashMap<String, serde_json::Value>>>>,
|
||||
#[cfg(test)]
|
||||
pub(crate) auth_wallet_store: Option<
|
||||
Arc<StdMutex<HashMap<String, aether_data::repository::wallet::StoredWalletSnapshot>>>,
|
||||
>,
|
||||
#[cfg(test)]
|
||||
pub(crate) admin_wallet_payment_order_store:
|
||||
Option<Arc<StdMutex<HashMap<String, AdminWalletPaymentOrderRecord>>>>,
|
||||
#[cfg(test)]
|
||||
pub(crate) admin_payment_callback_store:
|
||||
Option<Arc<StdMutex<HashMap<String, AdminPaymentCallbackRecord>>>>,
|
||||
#[cfg(test)]
|
||||
pub(crate) admin_wallet_transaction_store:
|
||||
Option<Arc<StdMutex<HashMap<String, AdminWalletTransactionRecord>>>>,
|
||||
#[cfg(test)]
|
||||
pub(crate) admin_wallet_refund_store:
|
||||
Option<Arc<StdMutex<HashMap<String, AdminWalletRefundRecord>>>>,
|
||||
#[cfg(test)]
|
||||
pub(crate) admin_billing_rule_store:
|
||||
Option<Arc<StdMutex<HashMap<String, AdminBillingRuleRecord>>>>,
|
||||
#[cfg(test)]
|
||||
pub(crate) admin_billing_collector_store:
|
||||
Option<Arc<StdMutex<HashMap<String, AdminBillingCollectorRecord>>>>,
|
||||
#[cfg(test)]
|
||||
pub(crate) admin_security_blacklist_store:
|
||||
Option<Arc<StdMutex<HashMap<String, String>>>>,
|
||||
#[cfg(test)]
|
||||
pub(crate) admin_security_whitelist_store:
|
||||
Option<Arc<StdMutex<std::collections::BTreeSet<String>>>>,
|
||||
#[cfg(test)]
|
||||
pub(crate) admin_monitoring_cache_affinity_store:
|
||||
Option<Arc<StdMutex<HashMap<String, String>>>>,
|
||||
#[cfg(test)]
|
||||
pub(crate) admin_monitoring_redis_key_store:
|
||||
Option<Arc<StdMutex<HashMap<String, String>>>>,
|
||||
#[cfg(test)]
|
||||
pub(crate) provider_oauth_token_url_overrides:
|
||||
Arc<StdMutex<HashMap<String, String>>>,
|
||||
}
|
||||
14
apps/aether-gateway/src/state/cache.rs
Normal file
14
apps/aether-gateway/src/state/cache.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use super::super::provider_transport;
|
||||
|
||||
pub(crate) const AUTH_API_KEY_LAST_USED_TTL: Duration = Duration::from_secs(60);
|
||||
pub(crate) const AUTH_API_KEY_LAST_USED_MAX_ENTRIES: usize = 10_000;
|
||||
pub(crate) const PROVIDER_TRANSPORT_SNAPSHOT_CACHE_TTL: Duration = Duration::from_secs(1);
|
||||
pub(crate) const PROVIDER_TRANSPORT_SNAPSHOT_CACHE_MAX_ENTRIES: usize = 1_024;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct CachedProviderTransportSnapshot {
|
||||
pub(crate) loaded_at: std::time::Instant,
|
||||
pub(crate) snapshot: provider_transport::GatewayProviderTransportSnapshot,
|
||||
}
|
||||
725
apps/aether-gateway/src/state/catalog.rs
Normal file
725
apps/aether-gateway/src/state/catalog.rs
Normal file
@@ -0,0 +1,725 @@
|
||||
use super::{AppState, GatewayError, LocalMutationOutcome, LocalProviderDeleteTaskState};
|
||||
|
||||
impl AppState {
|
||||
pub fn has_provider_catalog_data_reader(&self) -> bool {
|
||||
self.data.has_provider_catalog_reader()
|
||||
}
|
||||
|
||||
pub(crate) fn has_provider_catalog_data_writer(&self) -> bool {
|
||||
self.data.has_provider_catalog_writer()
|
||||
}
|
||||
|
||||
pub(crate) fn has_global_model_data_reader(&self) -> bool {
|
||||
self.data.has_global_model_reader()
|
||||
}
|
||||
|
||||
pub(crate) fn has_global_model_data_writer(&self) -> bool {
|
||||
self.data.has_global_model_writer()
|
||||
}
|
||||
|
||||
pub(crate) fn has_minimal_candidate_selection_reader(&self) -> bool {
|
||||
self.data.has_minimal_candidate_selection_reader()
|
||||
}
|
||||
|
||||
pub(crate) fn has_management_token_reader(&self) -> bool {
|
||||
self.data.has_management_token_reader()
|
||||
}
|
||||
|
||||
pub(crate) fn has_management_token_writer(&self) -> bool {
|
||||
self.data.has_management_token_writer()
|
||||
}
|
||||
|
||||
pub(crate) async fn list_provider_catalog_providers(
|
||||
&self,
|
||||
active_only: bool,
|
||||
) -> Result<
|
||||
Vec<aether_data::repository::provider_catalog::StoredProviderCatalogProvider>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.data
|
||||
.list_provider_catalog_providers(active_only)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn list_provider_catalog_endpoints_by_provider_ids(
|
||||
&self,
|
||||
provider_ids: &[String],
|
||||
) -> Result<
|
||||
Vec<aether_data::repository::provider_catalog::StoredProviderCatalogEndpoint>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.data
|
||||
.list_provider_catalog_endpoints_by_provider_ids(provider_ids)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn list_public_global_models(
|
||||
&self,
|
||||
query: &aether_data::repository::global_models::PublicGlobalModelQuery,
|
||||
) -> Result<aether_data::repository::global_models::StoredPublicGlobalModelPage, GatewayError>
|
||||
{
|
||||
self.data
|
||||
.list_public_global_models(query)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn list_management_tokens(
|
||||
&self,
|
||||
query: &aether_data::repository::management_tokens::ManagementTokenListQuery,
|
||||
) -> Result<
|
||||
aether_data::repository::management_tokens::StoredManagementTokenListPage,
|
||||
GatewayError,
|
||||
> {
|
||||
self.data
|
||||
.list_management_tokens(query)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn get_management_token_with_user(
|
||||
&self,
|
||||
token_id: &str,
|
||||
) -> Result<
|
||||
Option<aether_data::repository::management_tokens::StoredManagementTokenWithUser>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.data
|
||||
.get_management_token_with_user(token_id)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn create_management_token(
|
||||
&self,
|
||||
record: &aether_data::repository::management_tokens::CreateManagementTokenRecord,
|
||||
) -> Result<
|
||||
LocalMutationOutcome<aether_data::repository::management_tokens::StoredManagementToken>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.data
|
||||
.create_management_token(record)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn update_management_token(
|
||||
&self,
|
||||
record: &aether_data::repository::management_tokens::UpdateManagementTokenRecord,
|
||||
) -> Result<
|
||||
LocalMutationOutcome<aether_data::repository::management_tokens::StoredManagementToken>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.data
|
||||
.update_management_token(record)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_management_token(
|
||||
&self,
|
||||
token_id: &str,
|
||||
) -> Result<bool, GatewayError> {
|
||||
self.data
|
||||
.delete_management_token(token_id)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn set_management_token_active(
|
||||
&self,
|
||||
token_id: &str,
|
||||
is_active: bool,
|
||||
) -> Result<
|
||||
Option<aether_data::repository::management_tokens::StoredManagementToken>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.data
|
||||
.set_management_token_active(token_id, is_active)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn regenerate_management_token_secret(
|
||||
&self,
|
||||
mutation: &aether_data::repository::management_tokens::RegenerateManagementTokenSecret,
|
||||
) -> Result<
|
||||
LocalMutationOutcome<aether_data::repository::management_tokens::StoredManagementToken>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.data
|
||||
.regenerate_management_token_secret(mutation)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn get_public_global_model_by_name(
|
||||
&self,
|
||||
model_name: &str,
|
||||
) -> Result<Option<aether_data::repository::global_models::StoredPublicGlobalModel>, GatewayError>
|
||||
{
|
||||
self.data
|
||||
.get_public_global_model_by_name(model_name)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn list_public_catalog_models(
|
||||
&self,
|
||||
query: &aether_data::repository::global_models::PublicCatalogModelListQuery,
|
||||
) -> Result<Vec<aether_data::repository::global_models::StoredPublicCatalogModel>, GatewayError>
|
||||
{
|
||||
self.data
|
||||
.list_public_catalog_models(query)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn search_public_catalog_models(
|
||||
&self,
|
||||
query: &aether_data::repository::global_models::PublicCatalogModelSearchQuery,
|
||||
) -> Result<Vec<aether_data::repository::global_models::StoredPublicCatalogModel>, GatewayError>
|
||||
{
|
||||
self.data
|
||||
.search_public_catalog_models(query)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn list_admin_provider_models(
|
||||
&self,
|
||||
query: &aether_data::repository::global_models::AdminProviderModelListQuery,
|
||||
) -> Result<Vec<aether_data::repository::global_models::StoredAdminProviderModel>, GatewayError>
|
||||
{
|
||||
self.data
|
||||
.list_admin_provider_models(query)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn list_admin_global_models(
|
||||
&self,
|
||||
query: &aether_data::repository::global_models::AdminGlobalModelListQuery,
|
||||
) -> Result<aether_data::repository::global_models::StoredAdminGlobalModelPage, GatewayError>
|
||||
{
|
||||
self.data
|
||||
.list_admin_global_models(query)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn get_admin_provider_model(
|
||||
&self,
|
||||
provider_id: &str,
|
||||
model_id: &str,
|
||||
) -> Result<
|
||||
Option<aether_data::repository::global_models::StoredAdminProviderModel>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.data
|
||||
.get_admin_provider_model(provider_id, model_id)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn list_admin_provider_available_source_models(
|
||||
&self,
|
||||
provider_id: &str,
|
||||
) -> Result<Vec<aether_data::repository::global_models::StoredAdminProviderModel>, GatewayError>
|
||||
{
|
||||
self.data
|
||||
.list_admin_provider_available_source_models(provider_id)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn get_admin_global_model_by_id(
|
||||
&self,
|
||||
global_model_id: &str,
|
||||
) -> Result<Option<aether_data::repository::global_models::StoredAdminGlobalModel>, GatewayError>
|
||||
{
|
||||
self.data
|
||||
.get_admin_global_model_by_id(global_model_id)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn get_admin_global_model_by_name(
|
||||
&self,
|
||||
model_name: &str,
|
||||
) -> Result<Option<aether_data::repository::global_models::StoredAdminGlobalModel>, GatewayError>
|
||||
{
|
||||
self.data
|
||||
.get_admin_global_model_by_name(model_name)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn list_admin_provider_models_by_global_model_id(
|
||||
&self,
|
||||
global_model_id: &str,
|
||||
) -> Result<Vec<aether_data::repository::global_models::StoredAdminProviderModel>, GatewayError>
|
||||
{
|
||||
self.data
|
||||
.list_admin_provider_models_by_global_model_id(global_model_id)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn create_admin_provider_model(
|
||||
&self,
|
||||
record: &aether_data::repository::global_models::UpsertAdminProviderModelRecord,
|
||||
) -> Result<
|
||||
Option<aether_data::repository::global_models::StoredAdminProviderModel>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.data
|
||||
.create_admin_provider_model(record)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn update_admin_provider_model(
|
||||
&self,
|
||||
record: &aether_data::repository::global_models::UpsertAdminProviderModelRecord,
|
||||
) -> Result<
|
||||
Option<aether_data::repository::global_models::StoredAdminProviderModel>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.data
|
||||
.update_admin_provider_model(record)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_admin_provider_model(
|
||||
&self,
|
||||
provider_id: &str,
|
||||
model_id: &str,
|
||||
) -> Result<bool, GatewayError> {
|
||||
self.data
|
||||
.delete_admin_provider_model(provider_id, model_id)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn create_admin_global_model(
|
||||
&self,
|
||||
record: &aether_data::repository::global_models::CreateAdminGlobalModelRecord,
|
||||
) -> Result<Option<aether_data::repository::global_models::StoredAdminGlobalModel>, GatewayError>
|
||||
{
|
||||
self.data
|
||||
.create_admin_global_model(record)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn update_admin_global_model(
|
||||
&self,
|
||||
record: &aether_data::repository::global_models::UpdateAdminGlobalModelRecord,
|
||||
) -> Result<Option<aether_data::repository::global_models::StoredAdminGlobalModel>, GatewayError>
|
||||
{
|
||||
self.data
|
||||
.update_admin_global_model(record)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_admin_global_model(
|
||||
&self,
|
||||
global_model_id: &str,
|
||||
) -> Result<bool, GatewayError> {
|
||||
self.data
|
||||
.delete_admin_global_model(global_model_id)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn list_provider_model_stats(
|
||||
&self,
|
||||
provider_ids: &[String],
|
||||
) -> Result<Vec<aether_data::repository::global_models::StoredProviderModelStats>, GatewayError>
|
||||
{
|
||||
self.data
|
||||
.list_provider_model_stats(provider_ids)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn list_active_global_model_ids_by_provider_ids(
|
||||
&self,
|
||||
provider_ids: &[String],
|
||||
) -> Result<
|
||||
Vec<aether_data::repository::global_models::StoredProviderActiveGlobalModel>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.data
|
||||
.list_active_global_model_ids_by_provider_ids(provider_ids)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn list_finalized_request_candidates_by_endpoint_ids_since(
|
||||
&self,
|
||||
endpoint_ids: &[String],
|
||||
since_unix_secs: u64,
|
||||
limit: usize,
|
||||
) -> Result<Vec<aether_data::repository::candidates::StoredRequestCandidate>, GatewayError>
|
||||
{
|
||||
self.data
|
||||
.list_finalized_request_candidates_by_endpoint_ids_since(
|
||||
endpoint_ids,
|
||||
since_unix_secs,
|
||||
limit,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn count_finalized_request_candidate_statuses_by_endpoint_ids_since(
|
||||
&self,
|
||||
endpoint_ids: &[String],
|
||||
since_unix_secs: u64,
|
||||
) -> Result<Vec<aether_data::repository::candidates::PublicHealthStatusCount>, GatewayError>
|
||||
{
|
||||
self.data
|
||||
.count_finalized_request_candidate_statuses_by_endpoint_ids_since(
|
||||
endpoint_ids,
|
||||
since_unix_secs,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn aggregate_finalized_request_candidate_timeline_by_endpoint_ids_since(
|
||||
&self,
|
||||
endpoint_ids: &[String],
|
||||
since_unix_secs: u64,
|
||||
until_unix_secs: u64,
|
||||
segments: u32,
|
||||
) -> Result<Vec<aether_data::repository::candidates::PublicHealthTimelineBucket>, GatewayError>
|
||||
{
|
||||
self.data
|
||||
.aggregate_finalized_request_candidate_timeline_by_endpoint_ids_since(
|
||||
endpoint_ids,
|
||||
since_unix_secs,
|
||||
until_unix_secs,
|
||||
segments,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn list_provider_catalog_keys_by_provider_ids(
|
||||
&self,
|
||||
provider_ids: &[String],
|
||||
) -> Result<
|
||||
Vec<aether_data::repository::provider_catalog::StoredProviderCatalogKey>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.data
|
||||
.list_provider_catalog_keys_by_provider_ids(provider_ids)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn list_provider_catalog_keys_by_ids(
|
||||
&self,
|
||||
key_ids: &[String],
|
||||
) -> Result<
|
||||
Vec<aether_data::repository::provider_catalog::StoredProviderCatalogKey>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.data
|
||||
.list_provider_catalog_keys_by_ids(key_ids)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn list_provider_catalog_key_page(
|
||||
&self,
|
||||
query: &aether_data::repository::provider_catalog::ProviderCatalogKeyListQuery,
|
||||
) -> Result<aether_data::repository::provider_catalog::StoredProviderCatalogKeyPage, GatewayError>
|
||||
{
|
||||
self.data
|
||||
.list_provider_catalog_key_page(query)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn list_provider_catalog_key_stats_by_provider_ids(
|
||||
&self,
|
||||
provider_ids: &[String],
|
||||
) -> Result<
|
||||
Vec<aether_data::repository::provider_catalog::StoredProviderCatalogKeyStats>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.data
|
||||
.list_provider_catalog_key_stats_by_provider_ids(provider_ids)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn create_provider_catalog_key(
|
||||
&self,
|
||||
key: &aether_data::repository::provider_catalog::StoredProviderCatalogKey,
|
||||
) -> Result<
|
||||
Option<aether_data::repository::provider_catalog::StoredProviderCatalogKey>,
|
||||
GatewayError,
|
||||
> {
|
||||
let created = self
|
||||
.data
|
||||
.create_provider_catalog_key(key)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?;
|
||||
if created.is_some() {
|
||||
self.clear_provider_transport_snapshot_cache();
|
||||
}
|
||||
Ok(created)
|
||||
}
|
||||
|
||||
pub(crate) async fn create_provider_catalog_provider(
|
||||
&self,
|
||||
provider: &aether_data::repository::provider_catalog::StoredProviderCatalogProvider,
|
||||
shift_existing_priorities_from: Option<i32>,
|
||||
) -> Result<
|
||||
Option<aether_data::repository::provider_catalog::StoredProviderCatalogProvider>,
|
||||
GatewayError,
|
||||
> {
|
||||
let created = self
|
||||
.data
|
||||
.create_provider_catalog_provider(provider, shift_existing_priorities_from)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?;
|
||||
if created.is_some() {
|
||||
self.clear_provider_transport_snapshot_cache();
|
||||
}
|
||||
Ok(created)
|
||||
}
|
||||
|
||||
pub(crate) async fn update_provider_catalog_provider(
|
||||
&self,
|
||||
provider: &aether_data::repository::provider_catalog::StoredProviderCatalogProvider,
|
||||
) -> Result<
|
||||
Option<aether_data::repository::provider_catalog::StoredProviderCatalogProvider>,
|
||||
GatewayError,
|
||||
> {
|
||||
let updated = self
|
||||
.data
|
||||
.update_provider_catalog_provider(provider)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?;
|
||||
if updated.is_some() {
|
||||
self.clear_provider_transport_snapshot_cache();
|
||||
}
|
||||
Ok(updated)
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_provider_catalog_provider(
|
||||
&self,
|
||||
provider_id: &str,
|
||||
) -> Result<bool, GatewayError> {
|
||||
let deleted = self
|
||||
.data
|
||||
.delete_provider_catalog_provider(provider_id)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?;
|
||||
if deleted {
|
||||
self.clear_provider_transport_snapshot_cache();
|
||||
}
|
||||
Ok(deleted)
|
||||
}
|
||||
|
||||
pub(crate) async fn cleanup_deleted_provider_catalog_refs(
|
||||
&self,
|
||||
provider_id: &str,
|
||||
endpoint_ids: &[String],
|
||||
key_ids: &[String],
|
||||
) -> Result<(), GatewayError> {
|
||||
self.data
|
||||
.cleanup_deleted_provider_catalog_refs(provider_id, endpoint_ids, key_ids)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?;
|
||||
if !endpoint_ids.is_empty() || !key_ids.is_empty() {
|
||||
self.clear_provider_transport_snapshot_cache();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn create_provider_catalog_endpoint(
|
||||
&self,
|
||||
endpoint: &aether_data::repository::provider_catalog::StoredProviderCatalogEndpoint,
|
||||
) -> Result<
|
||||
Option<aether_data::repository::provider_catalog::StoredProviderCatalogEndpoint>,
|
||||
GatewayError,
|
||||
> {
|
||||
let created = self
|
||||
.data
|
||||
.create_provider_catalog_endpoint(endpoint)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?;
|
||||
if created.is_some() {
|
||||
self.clear_provider_transport_snapshot_cache();
|
||||
}
|
||||
Ok(created)
|
||||
}
|
||||
|
||||
pub(crate) async fn update_provider_catalog_endpoint(
|
||||
&self,
|
||||
endpoint: &aether_data::repository::provider_catalog::StoredProviderCatalogEndpoint,
|
||||
) -> Result<
|
||||
Option<aether_data::repository::provider_catalog::StoredProviderCatalogEndpoint>,
|
||||
GatewayError,
|
||||
> {
|
||||
let updated = self
|
||||
.data
|
||||
.update_provider_catalog_endpoint(endpoint)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?;
|
||||
if updated.is_some() {
|
||||
self.clear_provider_transport_snapshot_cache();
|
||||
}
|
||||
Ok(updated)
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_provider_catalog_endpoint(
|
||||
&self,
|
||||
endpoint_id: &str,
|
||||
) -> Result<bool, GatewayError> {
|
||||
let deleted = self
|
||||
.data
|
||||
.delete_provider_catalog_endpoint(endpoint_id)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?;
|
||||
if deleted {
|
||||
self.clear_provider_transport_snapshot_cache();
|
||||
}
|
||||
Ok(deleted)
|
||||
}
|
||||
|
||||
pub(crate) async fn update_provider_catalog_key(
|
||||
&self,
|
||||
key: &aether_data::repository::provider_catalog::StoredProviderCatalogKey,
|
||||
) -> Result<
|
||||
Option<aether_data::repository::provider_catalog::StoredProviderCatalogKey>,
|
||||
GatewayError,
|
||||
> {
|
||||
let updated = self
|
||||
.data
|
||||
.update_provider_catalog_key(key)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?;
|
||||
if updated.is_some() {
|
||||
self.clear_provider_transport_snapshot_cache();
|
||||
}
|
||||
Ok(updated)
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_provider_catalog_key(
|
||||
&self,
|
||||
key_id: &str,
|
||||
) -> Result<bool, GatewayError> {
|
||||
let deleted = self
|
||||
.data
|
||||
.delete_provider_catalog_key(key_id)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?;
|
||||
if deleted {
|
||||
self.clear_provider_transport_snapshot_cache();
|
||||
}
|
||||
Ok(deleted)
|
||||
}
|
||||
|
||||
pub(crate) async fn clear_provider_catalog_key_oauth_invalid_marker(
|
||||
&self,
|
||||
key_id: &str,
|
||||
) -> Result<bool, GatewayError> {
|
||||
self.data
|
||||
.clear_provider_catalog_key_oauth_invalid_marker(key_id)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) fn put_provider_delete_task(&self, task: LocalProviderDeleteTaskState) {
|
||||
let mut tasks = self
|
||||
.provider_delete_tasks
|
||||
.lock()
|
||||
.expect("provider delete tasks cache should lock");
|
||||
tasks.insert(task.task_id.clone(), task);
|
||||
}
|
||||
|
||||
pub(crate) fn get_provider_delete_task(
|
||||
&self,
|
||||
task_id: &str,
|
||||
) -> Option<LocalProviderDeleteTaskState> {
|
||||
let tasks = self
|
||||
.provider_delete_tasks
|
||||
.lock()
|
||||
.expect("provider delete tasks cache should lock");
|
||||
tasks.get(task_id).cloned()
|
||||
}
|
||||
|
||||
pub(crate) async fn read_provider_catalog_providers_by_ids(
|
||||
&self,
|
||||
provider_ids: &[String],
|
||||
) -> Result<
|
||||
Vec<aether_data::repository::provider_catalog::StoredProviderCatalogProvider>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.data
|
||||
.list_provider_catalog_providers_by_ids(provider_ids)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn read_provider_catalog_endpoints_by_ids(
|
||||
&self,
|
||||
endpoint_ids: &[String],
|
||||
) -> Result<
|
||||
Vec<aether_data::repository::provider_catalog::StoredProviderCatalogEndpoint>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.data
|
||||
.list_provider_catalog_endpoints_by_ids(endpoint_ids)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn read_provider_catalog_keys_by_ids(
|
||||
&self,
|
||||
key_ids: &[String],
|
||||
) -> Result<
|
||||
Vec<aether_data::repository::provider_catalog::StoredProviderCatalogKey>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.data
|
||||
.list_provider_catalog_keys_by_ids(key_ids)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn update_provider_catalog_key_health_state(
|
||||
&self,
|
||||
key_id: &str,
|
||||
is_active: bool,
|
||||
health_by_format: Option<&serde_json::Value>,
|
||||
circuit_breaker_by_format: Option<&serde_json::Value>,
|
||||
) -> Result<bool, GatewayError> {
|
||||
let updated = self
|
||||
.data
|
||||
.update_provider_catalog_key_health_state(
|
||||
key_id,
|
||||
is_active,
|
||||
health_by_format,
|
||||
circuit_breaker_by_format,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?;
|
||||
if updated {
|
||||
self.clear_provider_transport_snapshot_cache();
|
||||
}
|
||||
Ok(updated)
|
||||
}
|
||||
}
|
||||
637
apps/aether-gateway/src/state/core.rs
Normal file
637
apps/aether-gateway/src/state/core.rs
Normal file
@@ -0,0 +1,637 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex as StdMutex;
|
||||
use std::time::Duration;
|
||||
|
||||
use aether_data::repository::proxy_nodes::{
|
||||
ProxyNodeHeartbeatMutation, ProxyNodeTunnelStatusMutation, StoredProxyNode,
|
||||
StoredProxyNodeEvent,
|
||||
};
|
||||
use aether_http::{build_http_client, HttpClientConfig};
|
||||
use aether_runtime::{
|
||||
service_up_sample, AdmissionPermit, ConcurrencyGate, ConcurrencySnapshot,
|
||||
DistributedConcurrencyError, DistributedConcurrencyGate, DistributedConcurrencySnapshot,
|
||||
MetricKind, MetricLabel, MetricSample,
|
||||
};
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
use super::{AppState, FrontdoorCorsConfig, LocalExecutionRuntimeMissDiagnostic};
|
||||
|
||||
use super::super::async_task::{
|
||||
spawn_video_task_poller, VideoTaskPollerConfig, VideoTaskService, VideoTaskTruthSourceMode,
|
||||
};
|
||||
use super::super::fallback_metrics;
|
||||
use super::super::fallback_metrics::{GatewayFallbackMetricKind, GatewayFallbackReason};
|
||||
use super::super::cache::{
|
||||
AuthApiKeyLastUsedCache, AuthContextCache, DirectPlanBypassCache, SchedulerAffinityCache,
|
||||
SchedulerAffinityTarget,
|
||||
};
|
||||
use super::super::data::{GatewayDataConfig, GatewayDataState};
|
||||
use super::super::model_fetch::spawn_model_fetch_worker;
|
||||
use super::super::rate_limit::{FrontdoorUserRpmConfig, FrontdoorUserRpmLimiter};
|
||||
use super::super::router::RequestAdmissionError;
|
||||
use super::super::{control::GatewayControlDecision, error::GatewayError};
|
||||
use super::super::{provider_transport, scheduler, usage};
|
||||
|
||||
use crate::maintenance::spawn_audit_cleanup_worker;
|
||||
use crate::maintenance::spawn_db_maintenance_worker;
|
||||
use crate::maintenance::spawn_gemini_file_mapping_cleanup_worker;
|
||||
use crate::maintenance::spawn_pending_cleanup_worker;
|
||||
use crate::maintenance::spawn_pool_monitor_worker;
|
||||
use crate::maintenance::spawn_provider_checkin_worker;
|
||||
use crate::maintenance::spawn_request_candidate_cleanup_worker;
|
||||
use crate::maintenance::spawn_stats_aggregation_worker;
|
||||
use crate::maintenance::spawn_stats_hourly_aggregation_worker;
|
||||
use crate::maintenance::spawn_usage_cleanup_worker;
|
||||
use crate::maintenance::spawn_wallet_daily_usage_aggregation_worker;
|
||||
|
||||
impl AppState {
|
||||
pub(crate) fn replace_data_state(&mut self, data: Arc<GatewayDataState>) {
|
||||
self.clear_provider_transport_snapshot_cache();
|
||||
self.tunnel = crate::tunnel::EmbeddedTunnelState::with_data(Arc::clone(&data));
|
||||
self.data = data;
|
||||
}
|
||||
|
||||
pub fn force_close_all_tunnel_proxies(&self) -> usize {
|
||||
self.tunnel.request_close_all_proxies()
|
||||
}
|
||||
|
||||
pub fn new() -> Result<Self, reqwest::Error> {
|
||||
Self::build(None)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn with_execution_runtime_override_base_url(
|
||||
mut self,
|
||||
execution_runtime_override_base_url: impl Into<String>,
|
||||
) -> Self {
|
||||
self.execution_runtime_override_base_url = Some(
|
||||
execution_runtime_override_base_url
|
||||
.into()
|
||||
.trim_end_matches('/')
|
||||
.to_string(),
|
||||
)
|
||||
.filter(|value| !value.is_empty());
|
||||
self
|
||||
}
|
||||
|
||||
fn build(execution_runtime_override_base_url: Option<String>) -> Result<Self, reqwest::Error> {
|
||||
let data = Arc::new(GatewayDataState::disabled());
|
||||
let client = build_http_client(&HttpClientConfig {
|
||||
connect_timeout_ms: Some(10_000),
|
||||
request_timeout_ms: Some(300_000),
|
||||
http2_adaptive_window: true,
|
||||
..HttpClientConfig::default()
|
||||
})?;
|
||||
Ok(Self {
|
||||
#[cfg(test)]
|
||||
execution_runtime_override_base_url: execution_runtime_override_base_url
|
||||
.map(|value| value.trim_end_matches('/').to_string())
|
||||
.filter(|value| !value.is_empty()),
|
||||
data: Arc::clone(&data),
|
||||
usage_runtime: Arc::new(usage::UsageRuntime::disabled()),
|
||||
video_tasks: Arc::new(VideoTaskService::new(
|
||||
VideoTaskTruthSourceMode::PythonSyncReport,
|
||||
)),
|
||||
video_task_poller: None,
|
||||
request_gate: None,
|
||||
distributed_request_gate: None,
|
||||
client,
|
||||
auth_context_cache: Arc::new(AuthContextCache::default()),
|
||||
auth_api_key_last_used_cache: Arc::new(AuthApiKeyLastUsedCache::default()),
|
||||
oauth_refresh: Arc::new(provider_transport::LocalOAuthRefreshCoordinator::new()),
|
||||
direct_plan_bypass_cache: Arc::new(DirectPlanBypassCache::default()),
|
||||
scheduler_affinity_cache: Arc::new(SchedulerAffinityCache::default()),
|
||||
fallback_metrics: Arc::new(fallback_metrics::GatewayFallbackMetrics::default()),
|
||||
frontdoor_cors: None,
|
||||
frontdoor_user_rpm: Arc::new(FrontdoorUserRpmLimiter::new(
|
||||
FrontdoorUserRpmConfig::default(),
|
||||
)),
|
||||
tunnel: crate::tunnel::EmbeddedTunnelState::with_data(data),
|
||||
provider_transport_snapshot_cache: Arc::new(StdMutex::new(HashMap::new())),
|
||||
provider_key_rpm_resets: Arc::new(StdMutex::new(HashMap::new())),
|
||||
local_execution_runtime_miss_diagnostics: Arc::new(StdMutex::new(HashMap::new())),
|
||||
admin_monitoring_error_stats_reset_at: Arc::new(StdMutex::new(None)),
|
||||
provider_delete_tasks: Arc::new(StdMutex::new(HashMap::new())),
|
||||
#[cfg(test)]
|
||||
provider_oauth_state_store: None,
|
||||
#[cfg(test)]
|
||||
provider_oauth_device_session_store: Some(Arc::new(StdMutex::new(HashMap::new()))),
|
||||
#[cfg(test)]
|
||||
provider_oauth_batch_task_store: Some(Arc::new(StdMutex::new(HashMap::new()))),
|
||||
#[cfg(test)]
|
||||
auth_session_store: Some(Arc::new(StdMutex::new(HashMap::new()))),
|
||||
#[cfg(test)]
|
||||
auth_email_verification_store: Some(Arc::new(StdMutex::new(HashMap::new()))),
|
||||
#[cfg(test)]
|
||||
auth_email_delivery_store: Some(Arc::new(StdMutex::new(Vec::new()))),
|
||||
#[cfg(test)]
|
||||
auth_user_store: Some(Arc::new(StdMutex::new(HashMap::new()))),
|
||||
#[cfg(test)]
|
||||
auth_user_model_capability_store: Some(Arc::new(StdMutex::new(HashMap::new()))),
|
||||
#[cfg(test)]
|
||||
auth_wallet_store: Some(Arc::new(StdMutex::new(HashMap::new()))),
|
||||
#[cfg(test)]
|
||||
admin_wallet_payment_order_store: Some(Arc::new(StdMutex::new(HashMap::new()))),
|
||||
#[cfg(test)]
|
||||
admin_payment_callback_store: Some(Arc::new(StdMutex::new(HashMap::new()))),
|
||||
#[cfg(test)]
|
||||
admin_wallet_transaction_store: Some(Arc::new(StdMutex::new(HashMap::new()))),
|
||||
#[cfg(test)]
|
||||
admin_wallet_refund_store: Some(Arc::new(StdMutex::new(HashMap::new()))),
|
||||
#[cfg(test)]
|
||||
admin_billing_rule_store: Some(Arc::new(StdMutex::new(HashMap::new()))),
|
||||
#[cfg(test)]
|
||||
admin_billing_collector_store: Some(Arc::new(StdMutex::new(HashMap::new()))),
|
||||
#[cfg(test)]
|
||||
admin_security_blacklist_store: Some(Arc::new(StdMutex::new(HashMap::new()))),
|
||||
#[cfg(test)]
|
||||
admin_security_whitelist_store: Some(Arc::new(StdMutex::new(
|
||||
std::collections::BTreeSet::new(),
|
||||
))),
|
||||
#[cfg(test)]
|
||||
admin_monitoring_cache_affinity_store: Some(Arc::new(StdMutex::new(HashMap::new()))),
|
||||
#[cfg(test)]
|
||||
admin_monitoring_redis_key_store: Some(Arc::new(StdMutex::new(HashMap::new()))),
|
||||
#[cfg(test)]
|
||||
provider_oauth_token_url_overrides: Arc::new(StdMutex::new(HashMap::new())),
|
||||
})
|
||||
}
|
||||
|
||||
pub const fn execution_runtime_configured(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn execution_runtime_override_base_url(&self) -> Option<&str> {
|
||||
self.execution_runtime_override_base_url.as_deref()
|
||||
}
|
||||
|
||||
pub fn with_data_config(
|
||||
mut self,
|
||||
config: GatewayDataConfig,
|
||||
) -> Result<Self, aether_data::DataLayerError> {
|
||||
self.replace_data_state(Arc::new(GatewayDataState::from_config(config)?));
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn with_tunnel_identity(
|
||||
mut self,
|
||||
instance_id: impl Into<String>,
|
||||
relay_base_url: Option<impl Into<String>>,
|
||||
) -> Self {
|
||||
self.tunnel = crate::tunnel::EmbeddedTunnelState::with_data_and_identity(
|
||||
Arc::clone(&self.data),
|
||||
instance_id,
|
||||
relay_base_url,
|
||||
90,
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_video_task_truth_source_mode(mut self, mode: VideoTaskTruthSourceMode) -> Self {
|
||||
self.video_tasks = Arc::new(self.video_tasks.with_truth_source_mode(mode));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_usage_runtime_config(
|
||||
mut self,
|
||||
config: usage::UsageRuntimeConfig,
|
||||
) -> Result<Self, aether_data::DataLayerError> {
|
||||
self.usage_runtime = Arc::new(usage::UsageRuntime::new(config)?);
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub async fn run_postgres_migrations(&self) -> Result<bool, sqlx::migrate::MigrateError> {
|
||||
let Some(pool) = self.postgres_pool() else {
|
||||
return Ok(false);
|
||||
};
|
||||
aether_data::migrate::run_migrations(&pool).await?;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub fn with_video_task_poller_config(mut self, interval: Duration, batch_size: usize) -> Self {
|
||||
self.video_task_poller = Some(VideoTaskPollerConfig {
|
||||
interval,
|
||||
batch_size: batch_size.max(1),
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_request_concurrency_limit(mut self, limit: usize) -> Self {
|
||||
self.request_gate = Some(Arc::new(ConcurrencyGate::new(
|
||||
"gateway_requests",
|
||||
limit.max(1),
|
||||
)));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_distributed_request_concurrency_gate(
|
||||
mut self,
|
||||
gate: DistributedConcurrencyGate,
|
||||
) -> Self {
|
||||
self.distributed_request_gate = Some(Arc::new(gate));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_frontdoor_cors_config(mut self, config: FrontdoorCorsConfig) -> Self {
|
||||
self.frontdoor_cors = Some(Arc::new(config));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_frontdoor_user_rpm_config(mut self, config: FrontdoorUserRpmConfig) -> Self {
|
||||
self.frontdoor_user_rpm = Arc::new(FrontdoorUserRpmLimiter::new(config));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn has_data_backends(&self) -> bool {
|
||||
self.data.has_backends()
|
||||
}
|
||||
|
||||
pub(crate) fn has_auth_api_key_reader(&self) -> bool {
|
||||
self.data.has_auth_api_key_reader()
|
||||
}
|
||||
|
||||
pub(crate) fn frontdoor_cors(&self) -> Option<Arc<FrontdoorCorsConfig>> {
|
||||
self.frontdoor_cors.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn frontdoor_user_rpm(&self) -> Arc<FrontdoorUserRpmLimiter> {
|
||||
Arc::clone(&self.frontdoor_user_rpm)
|
||||
}
|
||||
|
||||
pub(crate) fn mark_provider_key_rpm_reset(&self, key_id: &str, now_unix_secs: u64) {
|
||||
let mut resets = self
|
||||
.provider_key_rpm_resets
|
||||
.lock()
|
||||
.expect("provider key rpm reset cache should lock");
|
||||
let min_kept = now_unix_secs.saturating_sub(scheduler::PROVIDER_KEY_RPM_WINDOW_SECS);
|
||||
resets.retain(|_, reset_at| *reset_at >= min_kept);
|
||||
resets.insert(key_id.to_string(), now_unix_secs);
|
||||
}
|
||||
|
||||
pub(crate) fn provider_key_rpm_reset_at(
|
||||
&self,
|
||||
key_id: &str,
|
||||
now_unix_secs: u64,
|
||||
) -> Option<u64> {
|
||||
let mut resets = self
|
||||
.provider_key_rpm_resets
|
||||
.lock()
|
||||
.expect("provider key rpm reset cache should lock");
|
||||
let min_kept = now_unix_secs.saturating_sub(scheduler::PROVIDER_KEY_RPM_WINDOW_SECS);
|
||||
resets.retain(|_, reset_at| *reset_at >= min_kept);
|
||||
resets.get(key_id).copied()
|
||||
}
|
||||
|
||||
pub(crate) fn admin_monitoring_error_stats_reset_at(&self) -> Option<u64> {
|
||||
*self
|
||||
.admin_monitoring_error_stats_reset_at
|
||||
.lock()
|
||||
.expect("admin monitoring error stats reset cache should lock")
|
||||
}
|
||||
|
||||
pub(crate) fn mark_admin_monitoring_error_stats_reset(&self, now_unix_secs: u64) {
|
||||
let mut reset_at = self
|
||||
.admin_monitoring_error_stats_reset_at
|
||||
.lock()
|
||||
.expect("admin monitoring error stats reset cache should lock");
|
||||
*reset_at = Some(now_unix_secs);
|
||||
}
|
||||
|
||||
pub(crate) async fn read_system_config_json_value(
|
||||
&self,
|
||||
key: &str,
|
||||
) -> Result<Option<serde_json::Value>, GatewayError> {
|
||||
self.data
|
||||
.find_system_config_value(key)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn upsert_system_config_json_value(
|
||||
&self,
|
||||
key: &str,
|
||||
value: &serde_json::Value,
|
||||
description: Option<&str>,
|
||||
) -> Result<serde_json::Value, GatewayError> {
|
||||
self.data
|
||||
.upsert_system_config_value(key, value, description)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn list_system_config_entries(
|
||||
&self,
|
||||
) -> Result<Vec<crate::data::state::StoredSystemConfigEntry>, GatewayError>
|
||||
{
|
||||
self.data
|
||||
.list_system_config_entries()
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn upsert_system_config_entry(
|
||||
&self,
|
||||
key: &str,
|
||||
value: &serde_json::Value,
|
||||
description: Option<&str>,
|
||||
) -> Result<crate::data::state::StoredSystemConfigEntry, GatewayError> {
|
||||
self.data
|
||||
.upsert_system_config_entry(key, value, description)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_system_config_value(&self, key: &str) -> Result<bool, GatewayError> {
|
||||
self.data
|
||||
.delete_system_config_value(key)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn read_admin_system_stats(
|
||||
&self,
|
||||
) -> Result<aether_data::repository::system::AdminSystemStats, GatewayError> {
|
||||
self.data
|
||||
.read_admin_system_stats()
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn find_proxy_node(
|
||||
&self,
|
||||
node_id: &str,
|
||||
) -> Result<Option<StoredProxyNode>, GatewayError> {
|
||||
self.data
|
||||
.find_proxy_node(node_id)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn list_proxy_nodes(&self) -> Result<Vec<StoredProxyNode>, GatewayError> {
|
||||
self.data
|
||||
.list_proxy_nodes()
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn list_proxy_node_events(
|
||||
&self,
|
||||
node_id: &str,
|
||||
limit: usize,
|
||||
) -> Result<Vec<StoredProxyNodeEvent>, GatewayError> {
|
||||
self.data
|
||||
.list_proxy_node_events(node_id, limit)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn apply_proxy_node_heartbeat(
|
||||
&self,
|
||||
mutation: &ProxyNodeHeartbeatMutation,
|
||||
) -> Result<Option<StoredProxyNode>, GatewayError> {
|
||||
self.data
|
||||
.apply_proxy_node_heartbeat(mutation)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn update_proxy_node_tunnel_status(
|
||||
&self,
|
||||
mutation: &ProxyNodeTunnelStatusMutation,
|
||||
) -> Result<Option<StoredProxyNode>, GatewayError> {
|
||||
self.data
|
||||
.update_proxy_node_tunnel_status(mutation)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) fn request_concurrency_snapshot(&self) -> Option<ConcurrencySnapshot> {
|
||||
self.request_gate.as_ref().map(|gate| gate.snapshot())
|
||||
}
|
||||
|
||||
pub(crate) async fn distributed_request_concurrency_snapshot(
|
||||
&self,
|
||||
) -> Result<Option<DistributedConcurrencySnapshot>, DistributedConcurrencyError> {
|
||||
match self.distributed_request_gate.as_ref() {
|
||||
Some(gate) => gate.snapshot().await.map(Some),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn metric_samples(&self) -> Vec<MetricSample> {
|
||||
let mut samples = vec![service_up_sample("aether-gateway")];
|
||||
if let Some(snapshot) = self.request_concurrency_snapshot() {
|
||||
samples.extend(snapshot.to_metric_samples("gateway_requests"));
|
||||
}
|
||||
if let Some(gate) = self.distributed_request_gate.as_ref() {
|
||||
match gate.snapshot().await {
|
||||
Ok(snapshot) => {
|
||||
samples.extend(snapshot.to_metric_samples("gateway_requests_distributed"));
|
||||
}
|
||||
Err(_) => samples.push(
|
||||
MetricSample::new(
|
||||
"concurrency_unavailable",
|
||||
"Whether the distributed concurrency gate is currently unavailable.",
|
||||
MetricKind::Gauge,
|
||||
1,
|
||||
)
|
||||
.with_labels(vec![MetricLabel::new(
|
||||
"gate",
|
||||
"gateway_requests_distributed",
|
||||
)]),
|
||||
),
|
||||
}
|
||||
}
|
||||
samples.extend(self.tunnel.metric_samples());
|
||||
samples.extend(self.fallback_metrics.metric_samples());
|
||||
samples
|
||||
}
|
||||
|
||||
pub(crate) fn record_fallback_metric(
|
||||
&self,
|
||||
kind: GatewayFallbackMetricKind,
|
||||
decision: Option<&GatewayControlDecision>,
|
||||
plan_kind: Option<&str>,
|
||||
execution_path: Option<&str>,
|
||||
reason: GatewayFallbackReason,
|
||||
) {
|
||||
self.fallback_metrics
|
||||
.record(kind, decision, plan_kind, execution_path, reason);
|
||||
}
|
||||
|
||||
pub(crate) fn clear_local_execution_runtime_miss_diagnostic(&self, trace_id: &str) {
|
||||
self.local_execution_runtime_miss_diagnostics
|
||||
.lock()
|
||||
.expect("local execution runtime miss diagnostics should lock")
|
||||
.remove(trace_id);
|
||||
}
|
||||
|
||||
pub(crate) fn set_local_execution_runtime_miss_diagnostic(
|
||||
&self,
|
||||
trace_id: &str,
|
||||
diagnostic: LocalExecutionRuntimeMissDiagnostic,
|
||||
) {
|
||||
self.local_execution_runtime_miss_diagnostics
|
||||
.lock()
|
||||
.expect("local execution runtime miss diagnostics should lock")
|
||||
.insert(trace_id.to_string(), diagnostic);
|
||||
}
|
||||
|
||||
pub(crate) fn mutate_local_execution_runtime_miss_diagnostic<F>(
|
||||
&self,
|
||||
trace_id: &str,
|
||||
mutate: F,
|
||||
) where
|
||||
F: FnOnce(&mut LocalExecutionRuntimeMissDiagnostic),
|
||||
{
|
||||
let mut diagnostics = self
|
||||
.local_execution_runtime_miss_diagnostics
|
||||
.lock()
|
||||
.expect("local execution runtime miss diagnostics should lock");
|
||||
if let Some(diagnostic) = diagnostics.get_mut(trace_id) {
|
||||
mutate(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn take_local_execution_runtime_miss_diagnostic(
|
||||
&self,
|
||||
trace_id: &str,
|
||||
) -> Option<LocalExecutionRuntimeMissDiagnostic> {
|
||||
self.local_execution_runtime_miss_diagnostics
|
||||
.lock()
|
||||
.expect("local execution runtime miss diagnostics should lock")
|
||||
.remove(trace_id)
|
||||
}
|
||||
|
||||
pub(crate) async fn try_acquire_request_permit(
|
||||
&self,
|
||||
) -> Result<Option<AdmissionPermit>, RequestAdmissionError> {
|
||||
let local = self
|
||||
.request_gate
|
||||
.as_ref()
|
||||
.map(|gate| gate.try_acquire())
|
||||
.transpose()
|
||||
.map_err(RequestAdmissionError::Local)?;
|
||||
let distributed = match self.distributed_request_gate.as_ref() {
|
||||
Some(gate) => Some(
|
||||
gate.try_acquire()
|
||||
.await
|
||||
.map_err(RequestAdmissionError::Distributed)?,
|
||||
),
|
||||
None => None,
|
||||
};
|
||||
Ok(AdmissionPermit::from_parts(local, distributed))
|
||||
}
|
||||
|
||||
pub fn has_auth_api_key_data_reader(&self) -> bool {
|
||||
self.data.has_auth_api_key_reader()
|
||||
}
|
||||
|
||||
pub fn has_gemini_file_mapping_data_reader(&self) -> bool {
|
||||
self.data.has_gemini_file_mapping_reader()
|
||||
}
|
||||
|
||||
pub fn has_gemini_file_mapping_data_writer(&self) -> bool {
|
||||
self.data.has_gemini_file_mapping_writer()
|
||||
}
|
||||
|
||||
pub fn has_redis_data_backend(&self) -> bool {
|
||||
self.data.has_redis_backend()
|
||||
}
|
||||
|
||||
pub(crate) fn redis_kv_runner(&self) -> Option<aether_data::redis::RedisKvRunner> {
|
||||
self.data.kv_runner()
|
||||
}
|
||||
|
||||
pub(crate) fn postgres_pool(&self) -> Option<aether_data::postgres::PostgresPool> {
|
||||
self.data.postgres_pool()
|
||||
}
|
||||
|
||||
pub(crate) fn remove_scheduler_affinity_cache_entry(&self, cache_key: &str) -> bool {
|
||||
self.scheduler_affinity_cache.remove(cache_key).is_some()
|
||||
}
|
||||
|
||||
pub(crate) fn read_scheduler_affinity_target(
|
||||
&self,
|
||||
cache_key: &str,
|
||||
ttl: Duration,
|
||||
) -> Option<SchedulerAffinityTarget> {
|
||||
self.scheduler_affinity_cache.get_fresh(cache_key, ttl)
|
||||
}
|
||||
|
||||
pub(crate) fn remember_scheduler_affinity_target(
|
||||
&self,
|
||||
cache_key: &str,
|
||||
target: SchedulerAffinityTarget,
|
||||
ttl: Duration,
|
||||
max_entries: usize,
|
||||
) {
|
||||
self.scheduler_affinity_cache
|
||||
.insert(cache_key.to_string(), target, ttl, max_entries);
|
||||
}
|
||||
|
||||
pub fn with_video_task_store_path(
|
||||
mut self,
|
||||
path: impl Into<std::path::PathBuf>,
|
||||
) -> std::io::Result<Self> {
|
||||
self.video_tasks = Arc::new(VideoTaskService::with_file_store(
|
||||
self.video_tasks.truth_source_mode(),
|
||||
path,
|
||||
)?);
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn spawn_background_tasks(&self) -> Vec<JoinHandle<()>> {
|
||||
let mut tasks = Vec::new();
|
||||
if let Some(handle) = self.usage_runtime.spawn_worker(self.data.clone()) {
|
||||
tasks.push(handle);
|
||||
}
|
||||
if let Some(handle) =
|
||||
crate::wallet_runtime::spawn_provider_quota_reset_worker(self.data.clone())
|
||||
{
|
||||
tasks.push(handle);
|
||||
}
|
||||
if let Some(handle) = spawn_audit_cleanup_worker(self.data.clone()) {
|
||||
tasks.push(handle);
|
||||
}
|
||||
if let Some(handle) = spawn_db_maintenance_worker(self.data.clone()) {
|
||||
tasks.push(handle);
|
||||
}
|
||||
if let Some(handle) = spawn_wallet_daily_usage_aggregation_worker(self.data.clone()) {
|
||||
tasks.push(handle);
|
||||
}
|
||||
if let Some(handle) = spawn_stats_aggregation_worker(self.data.clone()) {
|
||||
tasks.push(handle);
|
||||
}
|
||||
if let Some(handle) = spawn_usage_cleanup_worker(self.data.clone()) {
|
||||
tasks.push(handle);
|
||||
}
|
||||
if let Some(handle) = spawn_pool_monitor_worker(self.data.clone()) {
|
||||
tasks.push(handle);
|
||||
}
|
||||
if let Some(handle) = spawn_stats_hourly_aggregation_worker(self.data.clone()) {
|
||||
tasks.push(handle);
|
||||
}
|
||||
if let Some(handle) = spawn_pending_cleanup_worker(self.data.clone()) {
|
||||
tasks.push(handle);
|
||||
}
|
||||
if let Some(handle) = spawn_provider_checkin_worker(self.clone()) {
|
||||
tasks.push(handle);
|
||||
}
|
||||
if let Some(handle) = spawn_request_candidate_cleanup_worker(self.data.clone()) {
|
||||
tasks.push(handle);
|
||||
}
|
||||
if let Some(handle) = spawn_gemini_file_mapping_cleanup_worker(self.data.clone()) {
|
||||
tasks.push(handle);
|
||||
}
|
||||
if let Some(handle) = spawn_model_fetch_worker(self.clone()) {
|
||||
tasks.push(handle);
|
||||
}
|
||||
if let Some(handle) = spawn_video_task_poller(self.clone()) {
|
||||
tasks.push(handle);
|
||||
}
|
||||
tasks
|
||||
}
|
||||
}
|
||||
70
apps/aether-gateway/src/state/cors.rs
Normal file
70
apps/aether-gateway/src/state/cors.rs
Normal file
@@ -0,0 +1,70 @@
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct FrontdoorCorsConfig {
|
||||
allowed_origins: Vec<String>,
|
||||
allow_credentials: bool,
|
||||
}
|
||||
|
||||
impl FrontdoorCorsConfig {
|
||||
pub fn new(allowed_origins: Vec<String>, allow_credentials: bool) -> Option<Self> {
|
||||
let allowed_origins = allowed_origins
|
||||
.into_iter()
|
||||
.map(|value| value.trim().to_string())
|
||||
.filter(|value| !value.is_empty())
|
||||
.collect::<Vec<_>>();
|
||||
if allowed_origins.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let allow_any_origin = allowed_origins.iter().any(|value| value == "*");
|
||||
Some(Self {
|
||||
allowed_origins,
|
||||
allow_credentials: allow_credentials && !allow_any_origin,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn from_environment(
|
||||
environment: &str,
|
||||
cors_origins: Option<&str>,
|
||||
allow_credentials: bool,
|
||||
) -> Option<Self> {
|
||||
let configured = cors_origins
|
||||
.unwrap_or_default()
|
||||
.split(',')
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
.collect::<Vec<_>>();
|
||||
if !configured.is_empty() {
|
||||
return Self::new(configured, allow_credentials);
|
||||
}
|
||||
if environment.eq_ignore_ascii_case("development") {
|
||||
return Self::new(
|
||||
vec![
|
||||
"http://localhost:3000".to_string(),
|
||||
"http://localhost:5173".to_string(),
|
||||
"http://127.0.0.1:3000".to_string(),
|
||||
"http://127.0.0.1:5173".to_string(),
|
||||
],
|
||||
allow_credentials,
|
||||
);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) fn allows_origin(&self, origin: &str) -> bool {
|
||||
self.allowed_origins
|
||||
.iter()
|
||||
.any(|value| value == "*" || value == origin)
|
||||
}
|
||||
|
||||
pub(crate) fn allow_any_origin(&self) -> bool {
|
||||
self.allowed_origins.iter().any(|value| value == "*")
|
||||
}
|
||||
|
||||
pub(crate) fn allow_credentials(&self) -> bool {
|
||||
self.allow_credentials
|
||||
}
|
||||
|
||||
pub(crate) fn allowed_origins(&self) -> &[String] {
|
||||
&self.allowed_origins
|
||||
}
|
||||
}
|
||||
350
apps/aether-gateway/src/state/integrations.rs
Normal file
350
apps/aether-gateway/src/state/integrations.rs
Normal file
@@ -0,0 +1,350 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use aether_contracts::{ExecutionPlan, ExecutionResult, ProxySnapshot};
|
||||
use aether_data::repository::candidate_selection::StoredMinimalCandidateSelectionRow;
|
||||
use aether_data::repository::candidates::{StoredRequestCandidate, UpsertRequestCandidateRecord};
|
||||
use aether_data::repository::global_models::{
|
||||
AdminGlobalModelListQuery, AdminProviderModelListQuery, StoredAdminGlobalModelPage,
|
||||
StoredAdminProviderModel, UpsertAdminProviderModelRecord,
|
||||
};
|
||||
use aether_data::repository::provider_catalog::{
|
||||
StoredProviderCatalogEndpoint, StoredProviderCatalogKey, StoredProviderCatalogProvider,
|
||||
};
|
||||
use aether_data::repository::quota::StoredProviderQuotaSnapshot;
|
||||
use aether_data::DataLayerError;
|
||||
use aether_model_fetch::{
|
||||
aggregate_models_for_cache, model_fetch_interval_minutes, ModelFetchAssociationStore,
|
||||
ModelFetchTransportRuntime,
|
||||
};
|
||||
use aether_scheduler_core::SchedulerAffinityTarget;
|
||||
use async_trait::async_trait;
|
||||
use serde_json::Value;
|
||||
use tracing::debug;
|
||||
|
||||
use super::{AppState, GatewayError};
|
||||
use crate::model_fetch::ModelFetchRuntimeState;
|
||||
use crate::provider_transport::{
|
||||
resolve_transport_proxy_snapshot_with_tunnel_affinity, GatewayProviderTransportSnapshot,
|
||||
LocalResolvedOAuthRequestAuth,
|
||||
};
|
||||
use crate::scheduler::{
|
||||
GatewayMinimalCandidateSelectionCandidate, SchedulerCandidateSelectionRowSource,
|
||||
SchedulerRequestCandidateRuntimeState, SchedulerRuntimeState,
|
||||
};
|
||||
use crate::{execution_runtime, provider_transport};
|
||||
|
||||
#[async_trait]
|
||||
impl provider_transport::TransportTunnelAffinityLookup for AppState {
|
||||
async fn lookup_tunnel_attachment_owner(
|
||||
&self,
|
||||
node_id: &str,
|
||||
) -> Result<Option<provider_transport::TransportTunnelAttachmentOwner>, String> {
|
||||
self.tunnel
|
||||
.lookup_attachment_owner(self.data.as_ref(), node_id)
|
||||
.await
|
||||
.map(|owner| {
|
||||
owner.map(|owner| provider_transport::TransportTunnelAttachmentOwner {
|
||||
gateway_instance_id: owner.gateway_instance_id,
|
||||
relay_base_url: owner.relay_base_url,
|
||||
observed_at_unix_secs: owner.observed_at_unix_secs,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl provider_transport::VideoTaskTransportSnapshotLookup for AppState {
|
||||
async fn read_video_task_provider_transport_snapshot(
|
||||
&self,
|
||||
provider_id: &str,
|
||||
endpoint_id: &str,
|
||||
key_id: &str,
|
||||
) -> Result<Option<GatewayProviderTransportSnapshot>, String> {
|
||||
self.read_provider_transport_snapshot(provider_id, endpoint_id, key_id)
|
||||
.await
|
||||
.map_err(|err| match err {
|
||||
GatewayError::UpstreamUnavailable { message, .. }
|
||||
| GatewayError::ControlUnavailable { message, .. }
|
||||
| GatewayError::Internal(message) => message,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ModelFetchTransportRuntime for AppState {
|
||||
async fn resolve_local_oauth_request_auth(
|
||||
&self,
|
||||
transport: &GatewayProviderTransportSnapshot,
|
||||
) -> Result<Option<LocalResolvedOAuthRequestAuth>, String> {
|
||||
AppState::resolve_local_oauth_request_auth(self, transport)
|
||||
.await
|
||||
.map_err(|err| match err {
|
||||
GatewayError::UpstreamUnavailable { message, .. }
|
||||
| GatewayError::ControlUnavailable { message, .. }
|
||||
| GatewayError::Internal(message) => message,
|
||||
})
|
||||
}
|
||||
|
||||
async fn resolve_model_fetch_proxy(
|
||||
&self,
|
||||
transport: &GatewayProviderTransportSnapshot,
|
||||
) -> Option<ProxySnapshot> {
|
||||
resolve_transport_proxy_snapshot_with_tunnel_affinity(self, transport).await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ModelFetchRuntimeState for AppState {
|
||||
fn has_provider_catalog_data_reader(&self) -> bool {
|
||||
AppState::has_provider_catalog_data_reader(self)
|
||||
}
|
||||
|
||||
fn has_provider_catalog_data_writer(&self) -> bool {
|
||||
AppState::has_provider_catalog_data_writer(self)
|
||||
}
|
||||
|
||||
async fn list_provider_catalog_providers(
|
||||
&self,
|
||||
active_only: bool,
|
||||
) -> Result<Vec<StoredProviderCatalogProvider>, GatewayError> {
|
||||
AppState::list_provider_catalog_providers(self, active_only).await
|
||||
}
|
||||
|
||||
async fn list_provider_catalog_endpoints_by_provider_ids(
|
||||
&self,
|
||||
provider_ids: &[String],
|
||||
) -> Result<Vec<StoredProviderCatalogEndpoint>, GatewayError> {
|
||||
AppState::list_provider_catalog_endpoints_by_provider_ids(self, provider_ids).await
|
||||
}
|
||||
|
||||
async fn read_provider_transport_snapshot(
|
||||
&self,
|
||||
provider_id: &str,
|
||||
endpoint_id: &str,
|
||||
key_id: &str,
|
||||
) -> Result<Option<GatewayProviderTransportSnapshot>, GatewayError> {
|
||||
AppState::read_provider_transport_snapshot(self, provider_id, endpoint_id, key_id).await
|
||||
}
|
||||
|
||||
async fn execute_execution_runtime_sync_plan(
|
||||
&self,
|
||||
plan: &ExecutionPlan,
|
||||
) -> Result<ExecutionResult, GatewayError> {
|
||||
execution_runtime::execute_execution_runtime_sync_plan(self, None, plan).await
|
||||
}
|
||||
|
||||
async fn update_provider_catalog_key(
|
||||
&self,
|
||||
key: &StoredProviderCatalogKey,
|
||||
) -> Result<(), GatewayError> {
|
||||
AppState::update_provider_catalog_key(self, key).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn write_upstream_models_cache(
|
||||
&self,
|
||||
provider_id: &str,
|
||||
key_id: &str,
|
||||
cached_models: &[Value],
|
||||
) {
|
||||
let Some(runner) = AppState::redis_kv_runner(self) else {
|
||||
return;
|
||||
};
|
||||
let Ok(serialized) = serde_json::to_string(&aggregate_models_for_cache(cached_models))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let cache_key = format!("upstream_models:{provider_id}:{key_id}");
|
||||
if let Err(err) = runner
|
||||
.setex(
|
||||
&cache_key,
|
||||
&serialized,
|
||||
Some(model_fetch_interval_minutes().saturating_mul(60)),
|
||||
)
|
||||
.await
|
||||
{
|
||||
debug!(
|
||||
provider_id = %provider_id,
|
||||
key_id = %key_id,
|
||||
error = %err,
|
||||
"gateway model fetch cache write failed"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ModelFetchAssociationStore for AppState {
|
||||
type Error = String;
|
||||
|
||||
fn has_global_model_reader(&self) -> bool {
|
||||
self.data.has_global_model_reader()
|
||||
}
|
||||
|
||||
fn has_global_model_writer(&self) -> bool {
|
||||
self.data.has_global_model_writer()
|
||||
}
|
||||
|
||||
fn model_fetch_internal_error(&self, message: String) -> Self::Error {
|
||||
message
|
||||
}
|
||||
|
||||
async fn list_admin_provider_models(
|
||||
&self,
|
||||
query: &AdminProviderModelListQuery,
|
||||
) -> Result<Vec<StoredAdminProviderModel>, Self::Error> {
|
||||
AppState::list_admin_provider_models(self, query)
|
||||
.await
|
||||
.map_err(|err| format!("{err:?}"))
|
||||
}
|
||||
|
||||
async fn list_admin_global_models(
|
||||
&self,
|
||||
query: &AdminGlobalModelListQuery,
|
||||
) -> Result<StoredAdminGlobalModelPage, Self::Error> {
|
||||
AppState::list_admin_global_models(self, query)
|
||||
.await
|
||||
.map_err(|err| format!("{err:?}"))
|
||||
}
|
||||
|
||||
async fn create_admin_provider_model(
|
||||
&self,
|
||||
record: &UpsertAdminProviderModelRecord,
|
||||
) -> Result<Option<StoredAdminProviderModel>, Self::Error> {
|
||||
AppState::create_admin_provider_model(self, record)
|
||||
.await
|
||||
.map_err(|err| format!("{err:?}"))
|
||||
}
|
||||
|
||||
async fn list_provider_catalog_keys_by_provider_ids(
|
||||
&self,
|
||||
provider_ids: &[String],
|
||||
) -> Result<Vec<StoredProviderCatalogKey>, Self::Error> {
|
||||
AppState::list_provider_catalog_keys_by_provider_ids(self, provider_ids)
|
||||
.await
|
||||
.map_err(|err| format!("{err:?}"))
|
||||
}
|
||||
|
||||
async fn delete_admin_provider_model(
|
||||
&self,
|
||||
provider_id: &str,
|
||||
model_id: &str,
|
||||
) -> Result<bool, Self::Error> {
|
||||
AppState::delete_admin_provider_model(self, provider_id, model_id)
|
||||
.await
|
||||
.map_err(|err| format!("{err:?}"))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl SchedulerRequestCandidateRuntimeState for AppState {
|
||||
fn has_request_candidate_data_writer(&self) -> bool {
|
||||
AppState::has_request_candidate_data_writer(self)
|
||||
}
|
||||
|
||||
async fn read_request_candidates_by_request_id(
|
||||
&self,
|
||||
request_id: &str,
|
||||
) -> Result<Vec<StoredRequestCandidate>, GatewayError> {
|
||||
AppState::read_request_candidates_by_request_id(self, request_id).await
|
||||
}
|
||||
|
||||
async fn upsert_request_candidate(
|
||||
&self,
|
||||
candidate: UpsertRequestCandidateRecord,
|
||||
) -> Result<Option<StoredRequestCandidate>, GatewayError> {
|
||||
AppState::upsert_request_candidate(self, candidate).await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl SchedulerCandidateSelectionRowSource for AppState {
|
||||
async fn read_minimal_candidate_selection_rows_for_api_format_and_global_model(
|
||||
&self,
|
||||
api_format: &str,
|
||||
global_model_name: &str,
|
||||
) -> Result<Vec<StoredMinimalCandidateSelectionRow>, DataLayerError> {
|
||||
self.data
|
||||
.list_minimal_candidate_selection_rows(api_format, global_model_name)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn read_minimal_candidate_selection_rows_for_api_format(
|
||||
&self,
|
||||
api_format: &str,
|
||||
) -> Result<Vec<StoredMinimalCandidateSelectionRow>, DataLayerError> {
|
||||
self.data
|
||||
.list_minimal_candidate_selection_rows_for_api_format(api_format)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl SchedulerRuntimeState for AppState {
|
||||
async fn read_provider_quota_snapshot(
|
||||
&self,
|
||||
provider_id: &str,
|
||||
) -> Result<Option<StoredProviderQuotaSnapshot>, GatewayError> {
|
||||
AppState::read_provider_quota_snapshot(self, provider_id).await
|
||||
}
|
||||
|
||||
async fn read_provider_catalog_providers_by_ids(
|
||||
&self,
|
||||
provider_ids: &[String],
|
||||
) -> Result<Vec<StoredProviderCatalogProvider>, GatewayError> {
|
||||
AppState::read_provider_catalog_providers_by_ids(self, provider_ids).await
|
||||
}
|
||||
|
||||
async fn read_provider_catalog_keys_by_ids(
|
||||
&self,
|
||||
key_ids: &[String],
|
||||
) -> Result<Vec<StoredProviderCatalogKey>, GatewayError> {
|
||||
AppState::read_provider_catalog_keys_by_ids(self, key_ids).await
|
||||
}
|
||||
|
||||
async fn read_recent_request_candidates(
|
||||
&self,
|
||||
limit: usize,
|
||||
) -> Result<Vec<StoredRequestCandidate>, GatewayError> {
|
||||
AppState::read_recent_request_candidates(self, limit).await
|
||||
}
|
||||
|
||||
async fn read_minimal_candidate_selection(
|
||||
&self,
|
||||
api_format: &str,
|
||||
global_model_name: &str,
|
||||
require_streaming: bool,
|
||||
auth_snapshot: Option<&crate::data::auth::GatewayAuthApiKeySnapshot>,
|
||||
) -> Result<Vec<GatewayMinimalCandidateSelectionCandidate>, GatewayError> {
|
||||
AppState::read_minimal_candidate_selection(
|
||||
self,
|
||||
api_format,
|
||||
global_model_name,
|
||||
require_streaming,
|
||||
auth_snapshot,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
fn provider_key_rpm_reset_at(&self, key_id: &str, now_unix_secs: u64) -> Option<u64> {
|
||||
AppState::provider_key_rpm_reset_at(self, key_id, now_unix_secs)
|
||||
}
|
||||
|
||||
fn read_cached_scheduler_affinity_target(
|
||||
&self,
|
||||
cache_key: &str,
|
||||
ttl: Duration,
|
||||
) -> Option<SchedulerAffinityTarget> {
|
||||
AppState::read_scheduler_affinity_target(self, cache_key, ttl)
|
||||
}
|
||||
|
||||
fn remember_scheduler_affinity_target(
|
||||
&self,
|
||||
cache_key: &str,
|
||||
target: SchedulerAffinityTarget,
|
||||
ttl: Duration,
|
||||
max_entries: usize,
|
||||
) {
|
||||
AppState::remember_scheduler_affinity_target(self, cache_key, target, ttl, max_entries);
|
||||
}
|
||||
}
|
||||
36
apps/aether-gateway/src/state/mod.rs
Normal file
36
apps/aether-gateway/src/state/mod.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
use super::error::GatewayError;
|
||||
use super::data::GatewayDataState;
|
||||
|
||||
mod admin_types;
|
||||
mod app;
|
||||
mod cache;
|
||||
mod catalog;
|
||||
mod core;
|
||||
mod cors;
|
||||
mod integrations;
|
||||
mod oauth;
|
||||
mod runtime;
|
||||
#[cfg(test)]
|
||||
mod testing;
|
||||
mod types;
|
||||
mod video;
|
||||
|
||||
pub(crate) use self::admin_types::{
|
||||
AdminBillingCollectorRecord, AdminBillingCollectorWriteInput, AdminBillingPresetApplyResult,
|
||||
AdminBillingRuleRecord, AdminBillingRuleWriteInput, AdminPaymentCallbackRecord,
|
||||
AdminSecurityBlacklistEntry, AdminWalletPaymentOrderRecord, AdminWalletRefundRecord,
|
||||
AdminWalletTransactionRecord,
|
||||
};
|
||||
pub use self::app::AppState;
|
||||
pub(crate) use self::cache::{
|
||||
CachedProviderTransportSnapshot, AUTH_API_KEY_LAST_USED_MAX_ENTRIES,
|
||||
AUTH_API_KEY_LAST_USED_TTL, PROVIDER_TRANSPORT_SNAPSHOT_CACHE_MAX_ENTRIES,
|
||||
PROVIDER_TRANSPORT_SNAPSHOT_CACHE_TTL,
|
||||
};
|
||||
pub use self::cors::FrontdoorCorsConfig;
|
||||
pub(crate) use self::types::{
|
||||
AdminWalletMutationOutcome, LocalExecutionRuntimeMissDiagnostic, LocalMutationOutcome,
|
||||
LocalProviderDeleteTaskState,
|
||||
};
|
||||
use super::provider_transport::provider_transport_snapshot_looks_refreshed;
|
||||
pub(crate) use super::provider_transport::ProviderTransportSnapshotCacheKey;
|
||||
570
apps/aether-gateway/src/state/oauth.rs
Normal file
570
apps/aether-gateway/src/state/oauth.rs
Normal file
@@ -0,0 +1,570 @@
|
||||
use super::{
|
||||
provider_transport_snapshot_looks_refreshed, AppState, CachedProviderTransportSnapshot,
|
||||
GatewayError, ProviderTransportSnapshotCacheKey, PROVIDER_TRANSPORT_SNAPSHOT_CACHE_MAX_ENTRIES,
|
||||
PROVIDER_TRANSPORT_SNAPSHOT_CACHE_TTL,
|
||||
};
|
||||
|
||||
use super::super::provider_transport;
|
||||
use std::time::Duration;
|
||||
|
||||
use aether_crypto::encrypt_python_fernet_plaintext;
|
||||
|
||||
impl AppState {
|
||||
pub(crate) fn clear_provider_transport_snapshot_cache(&self) {
|
||||
self.provider_transport_snapshot_cache
|
||||
.lock()
|
||||
.expect("provider transport snapshot cache should lock")
|
||||
.clear();
|
||||
}
|
||||
|
||||
fn get_cached_provider_transport_snapshot(
|
||||
&self,
|
||||
cache_key: &ProviderTransportSnapshotCacheKey,
|
||||
) -> Option<provider_transport::GatewayProviderTransportSnapshot> {
|
||||
let mut cache = self
|
||||
.provider_transport_snapshot_cache
|
||||
.lock()
|
||||
.expect("provider transport snapshot cache should lock");
|
||||
let cached = cache.get(cache_key).cloned()?;
|
||||
if cached.loaded_at.elapsed() <= PROVIDER_TRANSPORT_SNAPSHOT_CACHE_TTL {
|
||||
return Some(cached.snapshot);
|
||||
}
|
||||
cache.remove(cache_key);
|
||||
None
|
||||
}
|
||||
|
||||
fn put_cached_provider_transport_snapshot(
|
||||
&self,
|
||||
cache_key: ProviderTransportSnapshotCacheKey,
|
||||
snapshot: provider_transport::GatewayProviderTransportSnapshot,
|
||||
) {
|
||||
let mut cache = self
|
||||
.provider_transport_snapshot_cache
|
||||
.lock()
|
||||
.expect("provider transport snapshot cache should lock");
|
||||
if cache.len() >= PROVIDER_TRANSPORT_SNAPSHOT_CACHE_MAX_ENTRIES {
|
||||
cache.retain(|_, entry| {
|
||||
entry.loaded_at.elapsed() <= PROVIDER_TRANSPORT_SNAPSHOT_CACHE_TTL
|
||||
});
|
||||
if cache.len() >= PROVIDER_TRANSPORT_SNAPSHOT_CACHE_MAX_ENTRIES {
|
||||
cache.clear();
|
||||
}
|
||||
}
|
||||
cache.insert(
|
||||
cache_key,
|
||||
CachedProviderTransportSnapshot {
|
||||
loaded_at: std::time::Instant::now(),
|
||||
snapshot,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async fn read_provider_transport_snapshot_uncached(
|
||||
&self,
|
||||
provider_id: &str,
|
||||
endpoint_id: &str,
|
||||
key_id: &str,
|
||||
) -> Result<
|
||||
Option<crate::provider_transport::GatewayProviderTransportSnapshot>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.data
|
||||
.read_provider_transport_snapshot(provider_id, endpoint_id, key_id)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn list_enabled_oauth_module_providers(
|
||||
&self,
|
||||
) -> Result<
|
||||
Vec<aether_data::repository::auth_modules::StoredOAuthProviderModuleConfig>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.data
|
||||
.list_enabled_oauth_module_providers()
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn get_ldap_module_config(
|
||||
&self,
|
||||
) -> Result<Option<aether_data::repository::auth_modules::StoredLdapModuleConfig>, GatewayError>
|
||||
{
|
||||
self.data
|
||||
.get_ldap_module_config()
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn upsert_ldap_module_config(
|
||||
&self,
|
||||
config: &aether_data::repository::auth_modules::StoredLdapModuleConfig,
|
||||
) -> Result<Option<aether_data::repository::auth_modules::StoredLdapModuleConfig>, GatewayError>
|
||||
{
|
||||
self.data
|
||||
.upsert_ldap_module_config(config)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn count_active_local_admin_users_with_valid_password(
|
||||
&self,
|
||||
) -> Result<u64, GatewayError> {
|
||||
self.data
|
||||
.count_active_local_admin_users_with_valid_password()
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn list_oauth_provider_configs(
|
||||
&self,
|
||||
) -> Result<
|
||||
Vec<aether_data::repository::oauth_providers::StoredOAuthProviderConfig>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.data
|
||||
.list_oauth_provider_configs()
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn get_oauth_provider_config(
|
||||
&self,
|
||||
provider_type: &str,
|
||||
) -> Result<
|
||||
Option<aether_data::repository::oauth_providers::StoredOAuthProviderConfig>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.data
|
||||
.get_oauth_provider_config(provider_type)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn count_locked_users_if_oauth_provider_disabled(
|
||||
&self,
|
||||
provider_type: &str,
|
||||
ldap_exclusive: bool,
|
||||
) -> Result<usize, GatewayError> {
|
||||
self.data
|
||||
.count_locked_users_if_oauth_provider_disabled(provider_type, ldap_exclusive)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn upsert_oauth_provider_config(
|
||||
&self,
|
||||
record: &aether_data::repository::oauth_providers::UpsertOAuthProviderConfigRecord,
|
||||
) -> Result<
|
||||
Option<aether_data::repository::oauth_providers::StoredOAuthProviderConfig>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.data
|
||||
.upsert_oauth_provider_config(record)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_oauth_provider_config(
|
||||
&self,
|
||||
provider_type: &str,
|
||||
) -> Result<bool, GatewayError> {
|
||||
self.data
|
||||
.delete_oauth_provider_config(provider_type)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) fn encryption_key(&self) -> Option<&str> {
|
||||
self.data.encryption_key()
|
||||
}
|
||||
|
||||
pub(crate) fn has_auth_module_writer(&self) -> bool {
|
||||
self.data.has_auth_module_writer()
|
||||
}
|
||||
|
||||
pub(crate) fn provider_oauth_token_url(
|
||||
&self,
|
||||
_provider_type: &str,
|
||||
default_token_url: &str,
|
||||
) -> String {
|
||||
#[cfg(test)]
|
||||
{
|
||||
if let Some(value) = self
|
||||
.provider_oauth_token_url_overrides
|
||||
.lock()
|
||||
.expect("provider oauth token url overrides should lock")
|
||||
.get(_provider_type.trim())
|
||||
.cloned()
|
||||
{
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
default_token_url.to_string()
|
||||
}
|
||||
|
||||
pub(crate) fn save_provider_oauth_state_for_tests(&self, _key: &str, _value: &str) -> bool {
|
||||
#[cfg(test)]
|
||||
{
|
||||
if let Some(store) = self.provider_oauth_state_store.as_ref() {
|
||||
store
|
||||
.lock()
|
||||
.expect("provider oauth state store should lock")
|
||||
.insert(_key.to_string(), _value.to_string());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub(crate) fn take_provider_oauth_state_for_tests(&self, _key: &str) -> Option<String> {
|
||||
#[cfg(test)]
|
||||
{
|
||||
return self.provider_oauth_state_store.as_ref().and_then(|store| {
|
||||
store
|
||||
.lock()
|
||||
.expect("provider oauth state store should lock")
|
||||
.remove(_key)
|
||||
});
|
||||
}
|
||||
|
||||
#[allow(unreachable_code)]
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) fn save_provider_oauth_device_session_for_tests(
|
||||
&self,
|
||||
_key: &str,
|
||||
_value: &str,
|
||||
) -> bool {
|
||||
#[cfg(test)]
|
||||
{
|
||||
if let Some(store) = self.provider_oauth_device_session_store.as_ref() {
|
||||
store
|
||||
.lock()
|
||||
.expect("provider oauth device session store should lock")
|
||||
.insert(_key.to_string(), _value.to_string());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub(crate) fn load_provider_oauth_device_session_for_tests(
|
||||
&self,
|
||||
_key: &str,
|
||||
) -> Option<String> {
|
||||
#[cfg(test)]
|
||||
{
|
||||
return self
|
||||
.provider_oauth_device_session_store
|
||||
.as_ref()
|
||||
.and_then(|store| {
|
||||
store
|
||||
.lock()
|
||||
.expect("provider oauth device session store should lock")
|
||||
.get(_key)
|
||||
.cloned()
|
||||
});
|
||||
}
|
||||
|
||||
#[allow(unreachable_code)]
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) fn save_provider_oauth_batch_task_for_tests(
|
||||
&self,
|
||||
_key: &str,
|
||||
_value: &str,
|
||||
) -> bool {
|
||||
#[cfg(test)]
|
||||
{
|
||||
if let Some(store) = self.provider_oauth_batch_task_store.as_ref() {
|
||||
store
|
||||
.lock()
|
||||
.expect("provider oauth batch task store should lock")
|
||||
.insert(_key.to_string(), _value.to_string());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub(crate) fn load_provider_oauth_batch_task_for_tests(&self, _key: &str) -> Option<String> {
|
||||
#[cfg(test)]
|
||||
{
|
||||
return self
|
||||
.provider_oauth_batch_task_store
|
||||
.as_ref()
|
||||
.and_then(|store| {
|
||||
store
|
||||
.lock()
|
||||
.expect("provider oauth batch task store should lock")
|
||||
.get(_key)
|
||||
.cloned()
|
||||
});
|
||||
}
|
||||
|
||||
#[allow(unreachable_code)]
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) async fn read_provider_transport_snapshot(
|
||||
&self,
|
||||
provider_id: &str,
|
||||
endpoint_id: &str,
|
||||
key_id: &str,
|
||||
) -> Result<
|
||||
Option<crate::provider_transport::GatewayProviderTransportSnapshot>,
|
||||
GatewayError,
|
||||
> {
|
||||
let Some(cache_key) =
|
||||
ProviderTransportSnapshotCacheKey::new(provider_id, endpoint_id, key_id)
|
||||
else {
|
||||
return self
|
||||
.read_provider_transport_snapshot_uncached(provider_id, endpoint_id, key_id)
|
||||
.await;
|
||||
};
|
||||
if let Some(snapshot) = self.get_cached_provider_transport_snapshot(&cache_key) {
|
||||
return Ok(Some(snapshot));
|
||||
}
|
||||
|
||||
let snapshot = self
|
||||
.read_provider_transport_snapshot_uncached(provider_id, endpoint_id, key_id)
|
||||
.await?;
|
||||
if let Some(snapshot) = snapshot.as_ref() {
|
||||
self.put_cached_provider_transport_snapshot(cache_key, snapshot.clone());
|
||||
}
|
||||
Ok(snapshot)
|
||||
}
|
||||
|
||||
pub(crate) async fn update_provider_catalog_key_oauth_credentials(
|
||||
&self,
|
||||
key_id: &str,
|
||||
encrypted_api_key: &str,
|
||||
encrypted_auth_config: Option<&str>,
|
||||
expires_at_unix_secs: Option<u64>,
|
||||
) -> Result<bool, GatewayError> {
|
||||
let updated = self
|
||||
.data
|
||||
.update_provider_catalog_key_oauth_credentials(
|
||||
key_id,
|
||||
encrypted_api_key,
|
||||
encrypted_auth_config,
|
||||
expires_at_unix_secs,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?;
|
||||
if updated {
|
||||
self.clear_provider_transport_snapshot_cache();
|
||||
}
|
||||
Ok(updated)
|
||||
}
|
||||
|
||||
pub(crate) async fn resolve_local_oauth_request_auth(
|
||||
&self,
|
||||
transport: &provider_transport::GatewayProviderTransportSnapshot,
|
||||
) -> Result<Option<provider_transport::LocalResolvedOAuthRequestAuth>, GatewayError> {
|
||||
let distributed_lock = self.data.oauth_refresh_lock_runner();
|
||||
let lock_owner = format!("aether-gateway-{}", std::process::id());
|
||||
let mut current_transport = transport.clone();
|
||||
|
||||
for _ in 0..2 {
|
||||
let resolution = self
|
||||
.oauth_refresh
|
||||
.resolve_with_result(
|
||||
&self.client,
|
||||
¤t_transport,
|
||||
distributed_lock.as_ref(),
|
||||
Some(lock_owner.as_str()),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?;
|
||||
|
||||
if resolution
|
||||
.as_ref()
|
||||
.is_some_and(|resolution| resolution.refresh_in_flight)
|
||||
{
|
||||
let Some(reloaded_transport) = self
|
||||
.wait_for_remote_oauth_refresh(¤t_transport)
|
||||
.await?
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
current_transport = reloaded_transport;
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(refreshed_entry) = resolution
|
||||
.as_ref()
|
||||
.and_then(|resolution| resolution.refreshed_entry.as_ref())
|
||||
{
|
||||
if let Err(err) = self
|
||||
.persist_local_oauth_refresh_entry(¤t_transport, refreshed_entry)
|
||||
.await
|
||||
{
|
||||
tracing::warn!(
|
||||
key_id = %current_transport.key.id,
|
||||
provider_type = %current_transport.provider.provider_type,
|
||||
error = ?err,
|
||||
"gateway local oauth refresh persistence failed"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(resolution.and_then(|resolution| resolution.auth));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub(crate) async fn force_local_oauth_refresh_entry(
|
||||
&self,
|
||||
transport: &provider_transport::GatewayProviderTransportSnapshot,
|
||||
) -> Result<
|
||||
Option<provider_transport::CachedOAuthEntry>,
|
||||
provider_transport::LocalOAuthRefreshError,
|
||||
> {
|
||||
let distributed_lock = self.data.oauth_refresh_lock_runner();
|
||||
let lock_owner = format!("aether-gateway-admin-{}", std::process::id());
|
||||
let mut current_transport = transport.clone();
|
||||
current_transport.key.decrypted_api_key = "__placeholder__".to_string();
|
||||
|
||||
for _ in 0..2 {
|
||||
let resolution = self
|
||||
.oauth_refresh
|
||||
.resolve_with_result(
|
||||
&self.client,
|
||||
¤t_transport,
|
||||
distributed_lock.as_ref(),
|
||||
Some(lock_owner.as_str()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
if resolution
|
||||
.as_ref()
|
||||
.is_some_and(|resolution| resolution.refresh_in_flight)
|
||||
{
|
||||
let Some(reloaded_transport) = self
|
||||
.wait_for_remote_oauth_refresh(¤t_transport)
|
||||
.await
|
||||
.map_err(
|
||||
|err| provider_transport::LocalOAuthRefreshError::InvalidResponse {
|
||||
provider_type: "gateway",
|
||||
message: format!("{err:?}"),
|
||||
},
|
||||
)?
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
current_transport = reloaded_transport;
|
||||
current_transport.key.decrypted_api_key = "__placeholder__".to_string();
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(refreshed_entry) = resolution
|
||||
.as_ref()
|
||||
.and_then(|resolution| resolution.refreshed_entry.as_ref())
|
||||
{
|
||||
if let Err(err) = self
|
||||
.persist_local_oauth_refresh_entry(¤t_transport, refreshed_entry)
|
||||
.await
|
||||
{
|
||||
tracing::warn!(
|
||||
key_id = %current_transport.key.id,
|
||||
provider_type = %current_transport.provider.provider_type,
|
||||
error = ?err,
|
||||
"gateway manual oauth refresh persistence failed"
|
||||
);
|
||||
}
|
||||
return Ok(Some(refreshed_entry.clone()));
|
||||
}
|
||||
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
async fn persist_local_oauth_refresh_entry(
|
||||
&self,
|
||||
transport: &provider_transport::GatewayProviderTransportSnapshot,
|
||||
entry: &provider_transport::CachedOAuthEntry,
|
||||
) -> Result<(), GatewayError> {
|
||||
let key_id = transport.key.id.trim();
|
||||
if key_id.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let Some(encryption_key) = self.data.encryption_key() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let access_token = entry
|
||||
.auth_header_value
|
||||
.trim()
|
||||
.strip_prefix("Bearer ")
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.ok_or_else(|| {
|
||||
GatewayError::Internal(
|
||||
"local oauth refresh produced non-bearer auth header".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let encrypted_api_key = encrypt_python_fernet_plaintext(encryption_key, access_token)
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?;
|
||||
let encrypted_auth_config = entry
|
||||
.metadata
|
||||
.as_ref()
|
||||
.map(|value| serde_json::to_string(value))
|
||||
.transpose()
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?
|
||||
.map(|value| encrypt_python_fernet_plaintext(encryption_key, value.as_str()))
|
||||
.transpose()
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?;
|
||||
|
||||
self.update_provider_catalog_key_oauth_credentials(
|
||||
key_id,
|
||||
encrypted_api_key.as_str(),
|
||||
encrypted_auth_config.as_deref(),
|
||||
entry.expires_at_unix_secs,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn wait_for_remote_oauth_refresh(
|
||||
&self,
|
||||
transport: &provider_transport::GatewayProviderTransportSnapshot,
|
||||
) -> Result<Option<provider_transport::GatewayProviderTransportSnapshot>, GatewayError> {
|
||||
if !self.data.has_provider_catalog_reader() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
for _ in 0..20 {
|
||||
let Some(reloaded_transport) = self
|
||||
.read_provider_transport_snapshot_uncached(
|
||||
&transport.provider.id,
|
||||
&transport.endpoint.id,
|
||||
&transport.key.id,
|
||||
)
|
||||
.await?
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
if provider_transport_snapshot_looks_refreshed(transport, &reloaded_transport) {
|
||||
return Ok(Some(reloaded_transport));
|
||||
}
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
79
apps/aether-gateway/src/state/runtime/announcements.rs
Normal file
79
apps/aether-gateway/src/state/runtime/announcements.rs
Normal file
@@ -0,0 +1,79 @@
|
||||
use crate::{AppState, GatewayError};
|
||||
|
||||
impl AppState {
|
||||
pub(crate) async fn list_announcements(
|
||||
&self,
|
||||
query: &aether_data::repository::announcements::AnnouncementListQuery,
|
||||
) -> Result<aether_data::repository::announcements::StoredAnnouncementPage, GatewayError> {
|
||||
self.data
|
||||
.list_announcements(query)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn find_announcement_by_id(
|
||||
&self,
|
||||
announcement_id: &str,
|
||||
) -> Result<Option<aether_data::repository::announcements::StoredAnnouncement>, GatewayError>
|
||||
{
|
||||
self.data
|
||||
.find_announcement_by_id(announcement_id)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn count_unread_active_announcements(
|
||||
&self,
|
||||
user_id: &str,
|
||||
now_unix_secs: u64,
|
||||
) -> Result<u64, GatewayError> {
|
||||
self.data
|
||||
.count_unread_active_announcements(user_id, now_unix_secs)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn create_announcement(
|
||||
&self,
|
||||
record: aether_data::repository::announcements::CreateAnnouncementRecord,
|
||||
) -> Result<Option<aether_data::repository::announcements::StoredAnnouncement>, GatewayError>
|
||||
{
|
||||
self.data
|
||||
.create_announcement(record)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn update_announcement(
|
||||
&self,
|
||||
record: aether_data::repository::announcements::UpdateAnnouncementRecord,
|
||||
) -> Result<Option<aether_data::repository::announcements::StoredAnnouncement>, GatewayError>
|
||||
{
|
||||
self.data
|
||||
.update_announcement(record)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_announcement(
|
||||
&self,
|
||||
announcement_id: &str,
|
||||
) -> Result<bool, GatewayError> {
|
||||
self.data
|
||||
.delete_announcement(announcement_id)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn mark_announcement_as_read(
|
||||
&self,
|
||||
user_id: &str,
|
||||
announcement_id: &str,
|
||||
read_at_unix_secs: u64,
|
||||
) -> Result<bool, GatewayError> {
|
||||
self.data
|
||||
.mark_announcement_as_read(user_id, announcement_id, read_at_unix_secs)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
}
|
||||
283
apps/aether-gateway/src/state/runtime/api_key_exports.rs
Normal file
283
apps/aether-gateway/src/state/runtime/api_key_exports.rs
Normal file
@@ -0,0 +1,283 @@
|
||||
use crate::{AppState, GatewayError};
|
||||
|
||||
impl AppState {
|
||||
pub(crate) async fn list_auth_api_key_export_records_by_user_ids(
|
||||
&self,
|
||||
user_ids: &[String],
|
||||
) -> Result<Vec<aether_data::repository::auth::StoredAuthApiKeyExportRecord>, GatewayError>
|
||||
{
|
||||
self.data
|
||||
.list_auth_api_key_export_records_by_user_ids(user_ids)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn list_auth_api_key_export_records_by_ids(
|
||||
&self,
|
||||
api_key_ids: &[String],
|
||||
) -> Result<Vec<aether_data::repository::auth::StoredAuthApiKeyExportRecord>, GatewayError>
|
||||
{
|
||||
self.data
|
||||
.list_auth_api_key_export_records_by_ids(api_key_ids)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn list_auth_api_key_export_standalone_records_page(
|
||||
&self,
|
||||
query: &aether_data::repository::auth::StandaloneApiKeyExportListQuery,
|
||||
) -> Result<Vec<aether_data::repository::auth::StoredAuthApiKeyExportRecord>, GatewayError>
|
||||
{
|
||||
self.data
|
||||
.list_auth_api_key_export_standalone_records_page(query)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn count_auth_api_key_export_standalone_records(
|
||||
&self,
|
||||
is_active: Option<bool>,
|
||||
) -> Result<u64, GatewayError> {
|
||||
self.data
|
||||
.count_auth_api_key_export_standalone_records(is_active)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn summarize_auth_api_key_export_records_by_user_ids(
|
||||
&self,
|
||||
user_ids: &[String],
|
||||
now_unix_secs: u64,
|
||||
) -> Result<aether_data::repository::auth::AuthApiKeyExportSummary, GatewayError> {
|
||||
self.data
|
||||
.summarize_auth_api_key_export_records_by_user_ids(user_ids, now_unix_secs)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn summarize_auth_api_key_export_non_standalone_records(
|
||||
&self,
|
||||
now_unix_secs: u64,
|
||||
) -> Result<aether_data::repository::auth::AuthApiKeyExportSummary, GatewayError> {
|
||||
self.data
|
||||
.summarize_auth_api_key_export_non_standalone_records(now_unix_secs)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn list_auth_api_key_export_standalone_records(
|
||||
&self,
|
||||
) -> Result<Vec<aether_data::repository::auth::StoredAuthApiKeyExportRecord>, GatewayError>
|
||||
{
|
||||
self.data
|
||||
.list_auth_api_key_export_standalone_records()
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn summarize_auth_api_key_export_standalone_records(
|
||||
&self,
|
||||
now_unix_secs: u64,
|
||||
) -> Result<aether_data::repository::auth::AuthApiKeyExportSummary, GatewayError> {
|
||||
self.data
|
||||
.summarize_auth_api_key_export_standalone_records(now_unix_secs)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn find_auth_api_key_export_standalone_record_by_id(
|
||||
&self,
|
||||
api_key_id: &str,
|
||||
) -> Result<Option<aether_data::repository::auth::StoredAuthApiKeyExportRecord>, GatewayError>
|
||||
{
|
||||
self.data
|
||||
.find_auth_api_key_export_standalone_record_by_id(api_key_id)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn list_non_admin_export_users(
|
||||
&self,
|
||||
) -> Result<Vec<aether_data::repository::users::StoredUserExportRow>, GatewayError> {
|
||||
self.data
|
||||
.list_non_admin_export_users()
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn list_export_users(
|
||||
&self,
|
||||
) -> Result<Vec<aether_data::repository::users::StoredUserExportRow>, GatewayError> {
|
||||
self.data
|
||||
.list_export_users()
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn summarize_export_users(
|
||||
&self,
|
||||
) -> Result<aether_data::repository::users::UserExportSummary, GatewayError> {
|
||||
self.data
|
||||
.summarize_export_users()
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn list_export_users_page(
|
||||
&self,
|
||||
query: &aether_data::repository::users::UserExportListQuery,
|
||||
) -> Result<Vec<aether_data::repository::users::StoredUserExportRow>, GatewayError> {
|
||||
self.data
|
||||
.list_export_users_page(query)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn find_export_user_by_id(
|
||||
&self,
|
||||
user_id: &str,
|
||||
) -> Result<Option<aether_data::repository::users::StoredUserExportRow>, GatewayError> {
|
||||
self.data
|
||||
.find_export_user_by_id(user_id)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn list_user_auth_by_ids(
|
||||
&self,
|
||||
user_ids: &[String],
|
||||
) -> Result<Vec<aether_data::repository::users::StoredUserAuthRecord>, GatewayError> {
|
||||
self.data
|
||||
.list_user_auth_by_ids(user_ids)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn create_user_api_key(
|
||||
&self,
|
||||
record: aether_data::repository::auth::CreateUserApiKeyRecord,
|
||||
) -> Result<Option<aether_data::repository::auth::StoredAuthApiKeyExportRecord>, GatewayError>
|
||||
{
|
||||
self.data
|
||||
.create_user_api_key(record)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn create_standalone_api_key(
|
||||
&self,
|
||||
record: aether_data::repository::auth::CreateStandaloneApiKeyRecord,
|
||||
) -> Result<Option<aether_data::repository::auth::StoredAuthApiKeyExportRecord>, GatewayError>
|
||||
{
|
||||
self.data
|
||||
.create_standalone_api_key(record)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn update_user_api_key_basic(
|
||||
&self,
|
||||
record: aether_data::repository::auth::UpdateUserApiKeyBasicRecord,
|
||||
) -> Result<Option<aether_data::repository::auth::StoredAuthApiKeyExportRecord>, GatewayError>
|
||||
{
|
||||
self.data
|
||||
.update_user_api_key_basic(record)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn update_standalone_api_key_basic(
|
||||
&self,
|
||||
record: aether_data::repository::auth::UpdateStandaloneApiKeyBasicRecord,
|
||||
) -> Result<Option<aether_data::repository::auth::StoredAuthApiKeyExportRecord>, GatewayError>
|
||||
{
|
||||
self.data
|
||||
.update_standalone_api_key_basic(record)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn set_user_api_key_active(
|
||||
&self,
|
||||
user_id: &str,
|
||||
api_key_id: &str,
|
||||
is_active: bool,
|
||||
) -> Result<Option<aether_data::repository::auth::StoredAuthApiKeyExportRecord>, GatewayError>
|
||||
{
|
||||
self.data
|
||||
.set_user_api_key_active(user_id, api_key_id, is_active)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn set_standalone_api_key_active(
|
||||
&self,
|
||||
api_key_id: &str,
|
||||
is_active: bool,
|
||||
) -> Result<Option<aether_data::repository::auth::StoredAuthApiKeyExportRecord>, GatewayError>
|
||||
{
|
||||
self.data
|
||||
.set_standalone_api_key_active(api_key_id, is_active)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn set_user_api_key_locked(
|
||||
&self,
|
||||
user_id: &str,
|
||||
api_key_id: &str,
|
||||
is_locked: bool,
|
||||
) -> Result<bool, GatewayError> {
|
||||
self.data
|
||||
.set_user_api_key_locked(user_id, api_key_id, is_locked)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn set_user_api_key_allowed_providers(
|
||||
&self,
|
||||
user_id: &str,
|
||||
api_key_id: &str,
|
||||
allowed_providers: Option<Vec<String>>,
|
||||
) -> Result<Option<aether_data::repository::auth::StoredAuthApiKeyExportRecord>, GatewayError>
|
||||
{
|
||||
self.data
|
||||
.set_user_api_key_allowed_providers(user_id, api_key_id, allowed_providers)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn set_user_api_key_force_capabilities(
|
||||
&self,
|
||||
user_id: &str,
|
||||
api_key_id: &str,
|
||||
force_capabilities: Option<serde_json::Value>,
|
||||
) -> Result<Option<aether_data::repository::auth::StoredAuthApiKeyExportRecord>, GatewayError>
|
||||
{
|
||||
self.data
|
||||
.set_user_api_key_force_capabilities(user_id, api_key_id, force_capabilities)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_user_api_key(
|
||||
&self,
|
||||
user_id: &str,
|
||||
api_key_id: &str,
|
||||
) -> Result<bool, GatewayError> {
|
||||
self.data
|
||||
.delete_user_api_key(user_id, api_key_id)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_standalone_api_key(
|
||||
&self,
|
||||
api_key_id: &str,
|
||||
) -> Result<bool, GatewayError> {
|
||||
self.data
|
||||
.delete_standalone_api_key(api_key_id)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
}
|
||||
141
apps/aether-gateway/src/state/runtime/audit.rs
Normal file
141
apps/aether-gateway/src/state/runtime/audit.rs
Normal file
@@ -0,0 +1,141 @@
|
||||
use crate::{scheduler, AppState, GatewayError};
|
||||
use aether_data::repository::audit::RequestAuditBundle;
|
||||
use aether_data::repository::usage::StoredRequestUsageAudit;
|
||||
|
||||
use super::super::{AUTH_API_KEY_LAST_USED_MAX_ENTRIES, AUTH_API_KEY_LAST_USED_TTL};
|
||||
|
||||
impl AppState {
|
||||
pub(crate) async fn read_request_candidate_trace(
|
||||
&self,
|
||||
request_id: &str,
|
||||
attempted_only: bool,
|
||||
) -> Result<Option<crate::data::candidates::RequestCandidateTrace>, GatewayError>
|
||||
{
|
||||
self.data
|
||||
.read_request_candidate_trace(request_id, attempted_only)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn read_decision_trace(
|
||||
&self,
|
||||
request_id: &str,
|
||||
attempted_only: bool,
|
||||
) -> Result<Option<crate::data::decision_trace::DecisionTrace>, GatewayError>
|
||||
{
|
||||
self.data
|
||||
.read_decision_trace(request_id, attempted_only)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn read_request_usage_audit(
|
||||
&self,
|
||||
request_id: &str,
|
||||
) -> Result<Option<StoredRequestUsageAudit>, GatewayError> {
|
||||
self.data
|
||||
.read_request_usage_audit(request_id)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn find_request_usage_by_id(
|
||||
&self,
|
||||
usage_id: &str,
|
||||
) -> Result<Option<aether_data::repository::usage::StoredRequestUsageAudit>, GatewayError> {
|
||||
self.data
|
||||
.find_request_usage_by_id(usage_id)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn read_request_audit_bundle(
|
||||
&self,
|
||||
request_id: &str,
|
||||
attempted_only: bool,
|
||||
now_unix_secs: u64,
|
||||
) -> Result<Option<RequestAuditBundle>, GatewayError> {
|
||||
self.data
|
||||
.read_request_audit_bundle(request_id, attempted_only, now_unix_secs)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn read_auth_api_key_snapshot(
|
||||
&self,
|
||||
user_id: &str,
|
||||
api_key_id: &str,
|
||||
now_unix_secs: u64,
|
||||
) -> Result<Option<crate::data::auth::GatewayAuthApiKeySnapshot>, GatewayError>
|
||||
{
|
||||
self.data
|
||||
.read_auth_api_key_snapshot(user_id, api_key_id, now_unix_secs)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn read_auth_api_key_snapshot_by_key_hash(
|
||||
&self,
|
||||
key_hash: &str,
|
||||
now_unix_secs: u64,
|
||||
) -> Result<Option<crate::data::auth::GatewayAuthApiKeySnapshot>, GatewayError>
|
||||
{
|
||||
self.data
|
||||
.read_auth_api_key_snapshot_by_key_hash(key_hash, now_unix_secs)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn read_auth_api_key_snapshots_by_ids(
|
||||
&self,
|
||||
api_key_ids: &[String],
|
||||
) -> Result<Vec<aether_data::repository::auth::StoredAuthApiKeySnapshot>, GatewayError> {
|
||||
self.data
|
||||
.list_auth_api_key_snapshots_by_ids(api_key_ids)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) fn has_auth_api_key_writer(&self) -> bool {
|
||||
self.data.has_auth_api_key_writer()
|
||||
}
|
||||
|
||||
pub(crate) async fn touch_auth_api_key_last_used_best_effort(&self, api_key_id: &str) {
|
||||
let api_key_id = api_key_id.trim();
|
||||
if api_key_id.is_empty() || !self.has_auth_api_key_writer() {
|
||||
return;
|
||||
}
|
||||
if !self.auth_api_key_last_used_cache.should_touch(
|
||||
api_key_id,
|
||||
AUTH_API_KEY_LAST_USED_TTL,
|
||||
AUTH_API_KEY_LAST_USED_MAX_ENTRIES,
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if let Err(err) = self.data.touch_auth_api_key_last_used(api_key_id).await {
|
||||
tracing::warn!(
|
||||
api_key_id = %api_key_id,
|
||||
error = ?err,
|
||||
"gateway auth api key last_used_at touch failed"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn read_minimal_candidate_selection(
|
||||
&self,
|
||||
api_format: &str,
|
||||
global_model_name: &str,
|
||||
require_streaming: bool,
|
||||
auth_snapshot: Option<&crate::data::auth::GatewayAuthApiKeySnapshot>,
|
||||
) -> Result<Vec<scheduler::GatewayMinimalCandidateSelectionCandidate>, GatewayError> {
|
||||
scheduler::read_minimal_candidate_selection(
|
||||
self.data.as_ref(),
|
||||
api_format,
|
||||
global_model_name,
|
||||
require_streaming,
|
||||
auth_snapshot,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
}
|
||||
3
apps/aether-gateway/src/state/runtime/auth/mod.rs
Normal file
3
apps/aether-gateway/src/state/runtime/auth/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
mod sessions;
|
||||
mod user_lifecycle;
|
||||
mod user_provisioning;
|
||||
254
apps/aether-gateway/src/state/runtime/auth/sessions.rs
Normal file
254
apps/aether-gateway/src/state/runtime/auth/sessions.rs
Normal file
@@ -0,0 +1,254 @@
|
||||
use crate::{AppState, GatewayError};
|
||||
|
||||
impl AppState {
|
||||
pub(crate) async fn find_user_session(
|
||||
&self,
|
||||
user_id: &str,
|
||||
session_id: &str,
|
||||
) -> Result<Option<crate::data::state::StoredUserSessionRecord>, GatewayError>
|
||||
{
|
||||
#[cfg(test)]
|
||||
if let Some(store) = self.auth_session_store.as_ref() {
|
||||
let key = format!("{user_id}:{session_id}");
|
||||
return Ok(store
|
||||
.lock()
|
||||
.expect("auth session store should lock")
|
||||
.get(&key)
|
||||
.cloned());
|
||||
}
|
||||
|
||||
self.data
|
||||
.find_user_session(user_id, session_id)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn list_user_sessions(
|
||||
&self,
|
||||
user_id: &str,
|
||||
) -> Result<Vec<crate::data::state::StoredUserSessionRecord>, GatewayError>
|
||||
{
|
||||
#[cfg(test)]
|
||||
if let Some(store) = self.auth_session_store.as_ref() {
|
||||
let prefix = format!("{user_id}:");
|
||||
let now = chrono::Utc::now();
|
||||
let mut sessions = store
|
||||
.lock()
|
||||
.expect("auth session store should lock")
|
||||
.iter()
|
||||
.filter(|(key, _)| key.starts_with(&prefix))
|
||||
.map(|(_, session)| session.clone())
|
||||
.filter(|session| !session.is_revoked() && !session.is_expired(now))
|
||||
.collect::<Vec<_>>();
|
||||
sessions.sort_by(|left, right| {
|
||||
right
|
||||
.last_seen_at
|
||||
.cmp(&left.last_seen_at)
|
||||
.then_with(|| right.created_at.cmp(&left.created_at))
|
||||
});
|
||||
return Ok(sessions);
|
||||
}
|
||||
|
||||
self.data
|
||||
.list_user_sessions(user_id)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn touch_user_session(
|
||||
&self,
|
||||
user_id: &str,
|
||||
session_id: &str,
|
||||
touched_at: chrono::DateTime<chrono::Utc>,
|
||||
ip_address: Option<&str>,
|
||||
user_agent: Option<&str>,
|
||||
) -> Result<bool, GatewayError> {
|
||||
#[cfg(test)]
|
||||
if let Some(store) = self.auth_session_store.as_ref() {
|
||||
let key = format!("{user_id}:{session_id}");
|
||||
let mut guard = store.lock().expect("auth session store should lock");
|
||||
if let Some(session) = guard.get_mut(&key) {
|
||||
session.last_seen_at = Some(touched_at);
|
||||
if let Some(ip_address) = ip_address {
|
||||
session.ip_address = Some(ip_address.to_string());
|
||||
}
|
||||
if let Some(user_agent) = user_agent {
|
||||
session.user_agent = Some(user_agent.chars().take(1000).collect());
|
||||
}
|
||||
session.updated_at = Some(touched_at);
|
||||
return Ok(true);
|
||||
}
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
self.data
|
||||
.touch_user_session(user_id, session_id, touched_at, ip_address, user_agent)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn update_user_session_device_label(
|
||||
&self,
|
||||
user_id: &str,
|
||||
session_id: &str,
|
||||
device_label: &str,
|
||||
updated_at: chrono::DateTime<chrono::Utc>,
|
||||
) -> Result<bool, GatewayError> {
|
||||
#[cfg(test)]
|
||||
if let Some(store) = self.auth_session_store.as_ref() {
|
||||
let key = format!("{user_id}:{session_id}");
|
||||
let mut guard = store.lock().expect("auth session store should lock");
|
||||
if let Some(session) = guard.get_mut(&key) {
|
||||
session.device_label = Some(device_label.chars().take(120).collect());
|
||||
session.updated_at = Some(updated_at);
|
||||
return Ok(true);
|
||||
}
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
self.data
|
||||
.update_user_session_device_label(user_id, session_id, device_label, updated_at)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn create_user_session(
|
||||
&self,
|
||||
session: crate::data::state::StoredUserSessionRecord,
|
||||
) -> Result<Option<crate::data::state::StoredUserSessionRecord>, GatewayError>
|
||||
{
|
||||
#[cfg(test)]
|
||||
if let Some(store) = self.auth_session_store.as_ref() {
|
||||
let now = session
|
||||
.created_at
|
||||
.or(session.updated_at)
|
||||
.or(session.last_seen_at)
|
||||
.unwrap_or_else(chrono::Utc::now);
|
||||
let mut guard = store.lock().expect("auth session store should lock");
|
||||
for existing in guard.values_mut() {
|
||||
if existing.user_id == session.user_id
|
||||
&& existing.client_device_id == session.client_device_id
|
||||
&& !existing.is_revoked()
|
||||
&& !existing.is_expired(now)
|
||||
{
|
||||
existing.revoked_at = Some(now);
|
||||
existing.revoke_reason = Some("replaced_by_new_login".to_string());
|
||||
existing.updated_at = Some(now);
|
||||
}
|
||||
}
|
||||
guard.insert(
|
||||
format!("{}:{}", session.user_id, session.id),
|
||||
session.clone(),
|
||||
);
|
||||
return Ok(Some(session));
|
||||
}
|
||||
|
||||
self.data
|
||||
.create_user_session(&session)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn rotate_user_session_refresh_token(
|
||||
&self,
|
||||
user_id: &str,
|
||||
session_id: &str,
|
||||
previous_refresh_token_hash: &str,
|
||||
next_refresh_token_hash: &str,
|
||||
rotated_at: chrono::DateTime<chrono::Utc>,
|
||||
expires_at: chrono::DateTime<chrono::Utc>,
|
||||
ip_address: Option<&str>,
|
||||
user_agent: Option<&str>,
|
||||
) -> Result<bool, GatewayError> {
|
||||
#[cfg(test)]
|
||||
if let Some(store) = self.auth_session_store.as_ref() {
|
||||
let key = format!("{user_id}:{session_id}");
|
||||
let mut guard = store.lock().expect("auth session store should lock");
|
||||
if let Some(session) = guard.get_mut(&key) {
|
||||
session.prev_refresh_token_hash = Some(previous_refresh_token_hash.to_string());
|
||||
session.refresh_token_hash = next_refresh_token_hash.to_string();
|
||||
session.rotated_at = Some(rotated_at);
|
||||
session.expires_at = Some(expires_at);
|
||||
session.last_seen_at = Some(rotated_at);
|
||||
if let Some(ip_address) = ip_address {
|
||||
session.ip_address = Some(ip_address.to_string());
|
||||
}
|
||||
if let Some(user_agent) = user_agent {
|
||||
session.user_agent = Some(user_agent.chars().take(1000).collect());
|
||||
}
|
||||
session.updated_at = Some(rotated_at);
|
||||
return Ok(true);
|
||||
}
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
self.data
|
||||
.rotate_user_session_refresh_token(
|
||||
user_id,
|
||||
session_id,
|
||||
previous_refresh_token_hash,
|
||||
next_refresh_token_hash,
|
||||
rotated_at,
|
||||
expires_at,
|
||||
ip_address,
|
||||
user_agent,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn revoke_user_session(
|
||||
&self,
|
||||
user_id: &str,
|
||||
session_id: &str,
|
||||
revoked_at: chrono::DateTime<chrono::Utc>,
|
||||
reason: &str,
|
||||
) -> Result<bool, GatewayError> {
|
||||
#[cfg(test)]
|
||||
if let Some(store) = self.auth_session_store.as_ref() {
|
||||
let key = format!("{user_id}:{session_id}");
|
||||
let mut guard = store.lock().expect("auth session store should lock");
|
||||
if let Some(session) = guard.get_mut(&key) {
|
||||
session.revoked_at = Some(revoked_at);
|
||||
session.revoke_reason = Some(reason.chars().take(100).collect());
|
||||
session.updated_at = Some(revoked_at);
|
||||
return Ok(true);
|
||||
}
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
self.data
|
||||
.revoke_user_session(user_id, session_id, revoked_at, reason)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn revoke_all_user_sessions(
|
||||
&self,
|
||||
user_id: &str,
|
||||
revoked_at: chrono::DateTime<chrono::Utc>,
|
||||
reason: &str,
|
||||
) -> Result<u64, GatewayError> {
|
||||
#[cfg(test)]
|
||||
if let Some(store) = self.auth_session_store.as_ref() {
|
||||
let prefix = format!("{user_id}:");
|
||||
let mut revoked = 0_u64;
|
||||
let mut guard = store.lock().expect("auth session store should lock");
|
||||
for (key, session) in guard.iter_mut() {
|
||||
if !key.starts_with(&prefix) || session.revoked_at.is_some() {
|
||||
continue;
|
||||
}
|
||||
session.revoked_at = Some(revoked_at);
|
||||
session.revoke_reason = Some(reason.chars().take(100).collect());
|
||||
session.updated_at = Some(revoked_at);
|
||||
revoked += 1;
|
||||
}
|
||||
return Ok(revoked);
|
||||
}
|
||||
|
||||
self.data
|
||||
.revoke_all_user_sessions(user_id, revoked_at, reason)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
}
|
||||
475
apps/aether-gateway/src/state/runtime/auth/user_lifecycle.rs
Normal file
475
apps/aether-gateway/src/state/runtime/auth/user_lifecycle.rs
Normal file
@@ -0,0 +1,475 @@
|
||||
use crate::{AppState, GatewayError};
|
||||
|
||||
impl AppState {
|
||||
pub(crate) async fn find_user_auth_by_id(
|
||||
&self,
|
||||
user_id: &str,
|
||||
) -> Result<Option<aether_data::repository::users::StoredUserAuthRecord>, GatewayError> {
|
||||
#[cfg(test)]
|
||||
if let Some(store) = self.auth_user_store.as_ref() {
|
||||
if let Some(user) = store
|
||||
.lock()
|
||||
.expect("auth user store should lock")
|
||||
.get(user_id)
|
||||
.cloned()
|
||||
{
|
||||
return Ok(Some(user));
|
||||
}
|
||||
}
|
||||
self.data
|
||||
.find_user_auth_by_id(user_id)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn find_user_auth_by_identifier(
|
||||
&self,
|
||||
identifier: &str,
|
||||
) -> Result<Option<aether_data::repository::users::StoredUserAuthRecord>, GatewayError> {
|
||||
#[cfg(test)]
|
||||
if let Some(store) = self.auth_user_store.as_ref() {
|
||||
let identifier = identifier.trim();
|
||||
if !identifier.is_empty() {
|
||||
if let Some(user) = store
|
||||
.lock()
|
||||
.expect("auth user store should lock")
|
||||
.values()
|
||||
.find(|user| {
|
||||
user.username == identifier || user.email.as_deref() == Some(identifier)
|
||||
})
|
||||
.cloned()
|
||||
{
|
||||
return Ok(Some(user));
|
||||
}
|
||||
}
|
||||
}
|
||||
self.data
|
||||
.find_user_auth_by_identifier(identifier)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn is_other_user_auth_email_taken(
|
||||
&self,
|
||||
email: &str,
|
||||
user_id: &str,
|
||||
) -> Result<bool, GatewayError> {
|
||||
#[cfg(test)]
|
||||
if let Some(store) = self.auth_user_store.as_ref() {
|
||||
if store
|
||||
.lock()
|
||||
.expect("auth user store should lock")
|
||||
.values()
|
||||
.any(|user| user.id != user_id && user.email.as_deref() == Some(email))
|
||||
{
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
|
||||
self.data
|
||||
.is_other_user_auth_email_taken(email, user_id)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn is_other_user_auth_username_taken(
|
||||
&self,
|
||||
username: &str,
|
||||
user_id: &str,
|
||||
) -> Result<bool, GatewayError> {
|
||||
#[cfg(test)]
|
||||
if let Some(store) = self.auth_user_store.as_ref() {
|
||||
if store
|
||||
.lock()
|
||||
.expect("auth user store should lock")
|
||||
.values()
|
||||
.any(|user| user.id != user_id && user.username == username)
|
||||
{
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
|
||||
self.data
|
||||
.is_other_user_auth_username_taken(username, user_id)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn update_local_auth_user_profile(
|
||||
&self,
|
||||
user_id: &str,
|
||||
email: Option<String>,
|
||||
username: Option<String>,
|
||||
) -> Result<Option<aether_data::repository::users::StoredUserAuthRecord>, GatewayError> {
|
||||
#[cfg(test)]
|
||||
if let Some(store) = self.auth_user_store.as_ref() {
|
||||
let existing = {
|
||||
store
|
||||
.lock()
|
||||
.expect("auth user store should lock")
|
||||
.get(user_id)
|
||||
.cloned()
|
||||
};
|
||||
let existing = match existing {
|
||||
Some(user) => Some(user),
|
||||
None => self
|
||||
.data
|
||||
.find_user_auth_by_id(user_id)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
};
|
||||
let Some(mut user) = existing else {
|
||||
return Ok(None);
|
||||
};
|
||||
if let Some(email) = email {
|
||||
user.email = Some(email);
|
||||
}
|
||||
if let Some(username) = username {
|
||||
user.username = username;
|
||||
}
|
||||
store
|
||||
.lock()
|
||||
.expect("auth user store should lock")
|
||||
.insert(user.id.clone(), user.clone());
|
||||
return Ok(Some(user));
|
||||
}
|
||||
|
||||
self.data
|
||||
.update_local_auth_user_profile(user_id, email, username)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn update_local_auth_user_password_hash(
|
||||
&self,
|
||||
user_id: &str,
|
||||
password_hash: String,
|
||||
updated_at: chrono::DateTime<chrono::Utc>,
|
||||
) -> Result<Option<aether_data::repository::users::StoredUserAuthRecord>, GatewayError> {
|
||||
#[cfg(test)]
|
||||
if let Some(store) = self.auth_user_store.as_ref() {
|
||||
let existing = {
|
||||
store
|
||||
.lock()
|
||||
.expect("auth user store should lock")
|
||||
.get(user_id)
|
||||
.cloned()
|
||||
};
|
||||
let existing = match existing {
|
||||
Some(user) => Some(user),
|
||||
None => self
|
||||
.data
|
||||
.find_user_auth_by_id(user_id)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
};
|
||||
let Some(mut user) = existing else {
|
||||
return Ok(None);
|
||||
};
|
||||
user.password_hash = Some(password_hash);
|
||||
store
|
||||
.lock()
|
||||
.expect("auth user store should lock")
|
||||
.insert(user.id.clone(), user.clone());
|
||||
return Ok(Some(user));
|
||||
}
|
||||
|
||||
self.data
|
||||
.update_local_auth_user_password_hash(user_id, password_hash, updated_at)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn create_local_auth_user(
|
||||
&self,
|
||||
email: Option<String>,
|
||||
email_verified: bool,
|
||||
username: String,
|
||||
password_hash: String,
|
||||
) -> Result<Option<aether_data::repository::users::StoredUserAuthRecord>, GatewayError> {
|
||||
#[cfg(test)]
|
||||
if let Some(store) = self.auth_user_store.as_ref() {
|
||||
let now = chrono::Utc::now();
|
||||
let user = aether_data::repository::users::StoredUserAuthRecord::new(
|
||||
uuid::Uuid::new_v4().to_string(),
|
||||
email,
|
||||
email_verified,
|
||||
username,
|
||||
Some(password_hash),
|
||||
"user".to_string(),
|
||||
"local".to_string(),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
true,
|
||||
false,
|
||||
Some(now),
|
||||
None,
|
||||
)
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?;
|
||||
store
|
||||
.lock()
|
||||
.expect("auth user store should lock")
|
||||
.insert(user.id.clone(), user.clone());
|
||||
return Ok(Some(user));
|
||||
}
|
||||
|
||||
self.data
|
||||
.create_local_auth_user(email, email_verified, username, password_hash)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) async fn create_local_auth_user_with_settings(
|
||||
&self,
|
||||
email: Option<String>,
|
||||
email_verified: bool,
|
||||
username: String,
|
||||
password_hash: String,
|
||||
role: String,
|
||||
allowed_providers: Option<Vec<String>>,
|
||||
allowed_api_formats: Option<Vec<String>>,
|
||||
allowed_models: Option<Vec<String>>,
|
||||
rate_limit: Option<i32>,
|
||||
) -> Result<Option<aether_data::repository::users::StoredUserAuthRecord>, GatewayError> {
|
||||
#[cfg(test)]
|
||||
if let Some(store) = self.auth_user_store.as_ref() {
|
||||
let now = chrono::Utc::now();
|
||||
let user = aether_data::repository::users::StoredUserAuthRecord::new(
|
||||
uuid::Uuid::new_v4().to_string(),
|
||||
email,
|
||||
email_verified,
|
||||
username,
|
||||
Some(password_hash),
|
||||
role,
|
||||
"local".to_string(),
|
||||
allowed_providers.map(serde_json::Value::from),
|
||||
allowed_api_formats.map(serde_json::Value::from),
|
||||
allowed_models.map(serde_json::Value::from),
|
||||
true,
|
||||
false,
|
||||
Some(now),
|
||||
None,
|
||||
)
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?;
|
||||
store
|
||||
.lock()
|
||||
.expect("auth user store should lock")
|
||||
.insert(user.id.clone(), user.clone());
|
||||
let _ = rate_limit;
|
||||
return Ok(Some(user));
|
||||
}
|
||||
|
||||
self.data
|
||||
.create_local_auth_user_with_settings(
|
||||
email,
|
||||
email_verified,
|
||||
username,
|
||||
password_hash,
|
||||
role,
|
||||
allowed_providers,
|
||||
allowed_api_formats,
|
||||
allowed_models,
|
||||
rate_limit,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) async fn update_local_auth_user_admin_fields(
|
||||
&self,
|
||||
user_id: &str,
|
||||
role: Option<String>,
|
||||
allowed_providers_present: bool,
|
||||
allowed_providers: Option<Vec<String>>,
|
||||
allowed_api_formats_present: bool,
|
||||
allowed_api_formats: Option<Vec<String>>,
|
||||
allowed_models_present: bool,
|
||||
allowed_models: Option<Vec<String>>,
|
||||
rate_limit: Option<i32>,
|
||||
is_active: Option<bool>,
|
||||
) -> Result<Option<aether_data::repository::users::StoredUserAuthRecord>, GatewayError> {
|
||||
#[cfg(test)]
|
||||
if let Some(store) = self.auth_user_store.as_ref() {
|
||||
let mut guard = store.lock().expect("auth user store should lock");
|
||||
let Some(user) = guard.get_mut(user_id) else {
|
||||
return Ok(None);
|
||||
};
|
||||
if let Some(role) = role {
|
||||
user.role = role;
|
||||
}
|
||||
if allowed_providers_present {
|
||||
user.allowed_providers = allowed_providers;
|
||||
}
|
||||
if allowed_api_formats_present {
|
||||
user.allowed_api_formats = allowed_api_formats;
|
||||
}
|
||||
if allowed_models_present {
|
||||
user.allowed_models = allowed_models;
|
||||
}
|
||||
if let Some(is_active) = is_active {
|
||||
user.is_active = is_active;
|
||||
}
|
||||
let _ = rate_limit;
|
||||
return Ok(Some(user.clone()));
|
||||
}
|
||||
|
||||
self.data
|
||||
.update_local_auth_user_admin_fields(
|
||||
user_id,
|
||||
role,
|
||||
allowed_providers_present,
|
||||
allowed_providers,
|
||||
allowed_api_formats_present,
|
||||
allowed_api_formats,
|
||||
allowed_models_present,
|
||||
allowed_models,
|
||||
rate_limit,
|
||||
is_active,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn touch_auth_user_last_login(
|
||||
&self,
|
||||
user_id: &str,
|
||||
logged_in_at: chrono::DateTime<chrono::Utc>,
|
||||
) -> Result<bool, GatewayError> {
|
||||
#[cfg(test)]
|
||||
if let Some(store) = self.auth_user_store.as_ref() {
|
||||
let mut guard = store.lock().expect("auth user store should lock");
|
||||
if let Some(user) = guard.get_mut(user_id) {
|
||||
user.last_login_at = Some(logged_in_at);
|
||||
return Ok(true);
|
||||
}
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
self.data
|
||||
.touch_auth_user_last_login(user_id, logged_in_at)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_local_auth_user(&self, user_id: &str) -> Result<bool, GatewayError> {
|
||||
#[cfg(test)]
|
||||
if let Some(store) = self.auth_user_store.as_ref() {
|
||||
let removed = store
|
||||
.lock()
|
||||
.expect("auth user store should lock")
|
||||
.remove(user_id)
|
||||
.is_some();
|
||||
if removed {
|
||||
if let Some(wallet_store) = self.auth_wallet_store.as_ref() {
|
||||
wallet_store
|
||||
.lock()
|
||||
.expect("auth wallet store should lock")
|
||||
.retain(|_, wallet| wallet.user_id.as_deref() != Some(user_id));
|
||||
}
|
||||
if let Some(session_store) = self.auth_session_store.as_ref() {
|
||||
let prefix = format!("{user_id}:");
|
||||
session_store
|
||||
.lock()
|
||||
.expect("auth session store should lock")
|
||||
.retain(|key, _| !key.starts_with(&prefix));
|
||||
}
|
||||
}
|
||||
return Ok(removed);
|
||||
}
|
||||
|
||||
self.data
|
||||
.delete_local_auth_user(user_id)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn register_local_auth_user(
|
||||
&self,
|
||||
email: Option<String>,
|
||||
email_verified: bool,
|
||||
username: String,
|
||||
password_hash: String,
|
||||
initial_gift_usd: f64,
|
||||
unlimited: bool,
|
||||
) -> Result<
|
||||
Option<(
|
||||
aether_data::repository::users::StoredUserAuthRecord,
|
||||
aether_data::repository::wallet::StoredWalletSnapshot,
|
||||
)>,
|
||||
GatewayError,
|
||||
> {
|
||||
#[cfg(test)]
|
||||
if let (Some(user_store), Some(wallet_store)) = (
|
||||
self.auth_user_store.as_ref(),
|
||||
self.auth_wallet_store.as_ref(),
|
||||
) {
|
||||
let now = chrono::Utc::now();
|
||||
let user = aether_data::repository::users::StoredUserAuthRecord::new(
|
||||
uuid::Uuid::new_v4().to_string(),
|
||||
email,
|
||||
email_verified,
|
||||
username,
|
||||
Some(password_hash),
|
||||
"user".to_string(),
|
||||
"local".to_string(),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
true,
|
||||
false,
|
||||
Some(now),
|
||||
None,
|
||||
)
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?;
|
||||
let gift_balance = if unlimited {
|
||||
0.0
|
||||
} else {
|
||||
initial_gift_usd.max(0.0)
|
||||
};
|
||||
let wallet = aether_data::repository::wallet::StoredWalletSnapshot::new(
|
||||
uuid::Uuid::new_v4().to_string(),
|
||||
Some(user.id.clone()),
|
||||
None,
|
||||
0.0,
|
||||
gift_balance,
|
||||
if unlimited {
|
||||
"unlimited".to_string()
|
||||
} else {
|
||||
"finite".to_string()
|
||||
},
|
||||
"USD".to_string(),
|
||||
"active".to_string(),
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
gift_balance,
|
||||
now.timestamp(),
|
||||
)
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?;
|
||||
user_store
|
||||
.lock()
|
||||
.expect("auth user store should lock")
|
||||
.insert(user.id.clone(), user.clone());
|
||||
wallet_store
|
||||
.lock()
|
||||
.expect("auth wallet store should lock")
|
||||
.insert(wallet.id.clone(), wallet.clone());
|
||||
return Ok(Some((user, wallet)));
|
||||
}
|
||||
|
||||
self.data
|
||||
.register_local_auth_user(
|
||||
email,
|
||||
email_verified,
|
||||
username,
|
||||
password_hash,
|
||||
initial_gift_usd,
|
||||
unlimited,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
}
|
||||
271
apps/aether-gateway/src/state/runtime/auth/user_provisioning.rs
Normal file
271
apps/aether-gateway/src/state/runtime/auth/user_provisioning.rs
Normal file
@@ -0,0 +1,271 @@
|
||||
use crate::{AppState, GatewayError};
|
||||
|
||||
impl AppState {
|
||||
pub(crate) async fn read_user_model_capability_settings(
|
||||
&self,
|
||||
user_id: &str,
|
||||
) -> Result<Option<serde_json::Value>, GatewayError> {
|
||||
#[cfg(test)]
|
||||
if let Some(store) = self.auth_user_model_capability_store.as_ref() {
|
||||
if let Some(settings) = store
|
||||
.lock()
|
||||
.expect("auth user model capability store should lock")
|
||||
.get(user_id)
|
||||
.cloned()
|
||||
{
|
||||
return Ok(Some(settings));
|
||||
}
|
||||
}
|
||||
|
||||
let users = self.list_non_admin_export_users().await?;
|
||||
Ok(users
|
||||
.into_iter()
|
||||
.find(|user| user.id == user_id)
|
||||
.and_then(|user| user.model_capability_settings))
|
||||
}
|
||||
|
||||
pub(crate) async fn update_user_model_capability_settings(
|
||||
&self,
|
||||
user_id: &str,
|
||||
settings: Option<serde_json::Value>,
|
||||
) -> Result<Option<serde_json::Value>, GatewayError> {
|
||||
#[cfg(test)]
|
||||
if let Some(store) = self.auth_user_model_capability_store.as_ref() {
|
||||
let mut guard = store
|
||||
.lock()
|
||||
.expect("auth user model capability store should lock");
|
||||
match settings {
|
||||
Some(value) => {
|
||||
guard.insert(user_id.to_string(), value.clone());
|
||||
return Ok(Some(value));
|
||||
}
|
||||
None => {
|
||||
guard.remove(user_id);
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.data
|
||||
.update_user_model_capability_settings(user_id, settings)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn find_active_provider_name(
|
||||
&self,
|
||||
provider_id: &str,
|
||||
) -> Result<Option<String>, GatewayError> {
|
||||
self.data
|
||||
.find_active_provider_name(provider_id)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) async fn get_or_create_ldap_auth_user(
|
||||
&self,
|
||||
email: String,
|
||||
username: String,
|
||||
ldap_dn: Option<String>,
|
||||
ldap_username: Option<String>,
|
||||
logged_in_at: chrono::DateTime<chrono::Utc>,
|
||||
initial_gift_usd: f64,
|
||||
unlimited: bool,
|
||||
) -> Result<Option<aether_data::repository::users::StoredUserAuthRecord>, GatewayError> {
|
||||
#[cfg(test)]
|
||||
if let (Some(user_store), Some(wallet_store)) = (
|
||||
self.auth_user_store.as_ref(),
|
||||
self.auth_wallet_store.as_ref(),
|
||||
) {
|
||||
let mut users = user_store.lock().expect("auth user store should lock");
|
||||
let existing_id = users
|
||||
.values()
|
||||
.find(|user| {
|
||||
user.email.as_deref() == Some(email.as_str())
|
||||
|| user.username == username
|
||||
|| ldap_username
|
||||
.as_deref()
|
||||
.is_some_and(|value| user.username == value)
|
||||
})
|
||||
.map(|user| user.id.clone());
|
||||
|
||||
if let Some(existing_id) = existing_id {
|
||||
let Some(user) = users.get_mut(&existing_id) else {
|
||||
return Ok(None);
|
||||
};
|
||||
if user.is_deleted || !user.is_active {
|
||||
return Ok(None);
|
||||
}
|
||||
if !user.auth_source.eq_ignore_ascii_case("ldap") {
|
||||
return Ok(None);
|
||||
}
|
||||
user.email = Some(email);
|
||||
user.email_verified = true;
|
||||
user.last_login_at = Some(logged_in_at);
|
||||
return Ok(Some(user.clone()));
|
||||
}
|
||||
|
||||
let base_username = ldap_username
|
||||
.as_deref()
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.unwrap_or(username.as_str())
|
||||
.trim()
|
||||
.to_string();
|
||||
let mut candidate_username = base_username.clone();
|
||||
while users
|
||||
.values()
|
||||
.any(|user| user.username == candidate_username)
|
||||
{
|
||||
let suffix = uuid::Uuid::new_v4().simple().to_string();
|
||||
candidate_username = format!(
|
||||
"{}_ldap_{}{}",
|
||||
base_username,
|
||||
logged_in_at.timestamp(),
|
||||
&suffix[..4]
|
||||
);
|
||||
}
|
||||
|
||||
let user = aether_data::repository::users::StoredUserAuthRecord::new(
|
||||
uuid::Uuid::new_v4().to_string(),
|
||||
Some(email),
|
||||
true,
|
||||
candidate_username,
|
||||
None,
|
||||
"user".to_string(),
|
||||
"ldap".to_string(),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
true,
|
||||
false,
|
||||
Some(logged_in_at),
|
||||
Some(logged_in_at),
|
||||
)
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?;
|
||||
users.insert(user.id.clone(), user.clone());
|
||||
drop(users);
|
||||
|
||||
let gift_balance = if unlimited {
|
||||
0.0
|
||||
} else {
|
||||
initial_gift_usd.max(0.0)
|
||||
};
|
||||
let wallet = aether_data::repository::wallet::StoredWalletSnapshot::new(
|
||||
uuid::Uuid::new_v4().to_string(),
|
||||
Some(user.id.clone()),
|
||||
None,
|
||||
0.0,
|
||||
gift_balance,
|
||||
if unlimited {
|
||||
"unlimited".to_string()
|
||||
} else {
|
||||
"finite".to_string()
|
||||
},
|
||||
"USD".to_string(),
|
||||
"active".to_string(),
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
gift_balance,
|
||||
logged_in_at.timestamp(),
|
||||
)
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?;
|
||||
wallet_store
|
||||
.lock()
|
||||
.expect("auth wallet store should lock")
|
||||
.insert(wallet.id.clone(), wallet);
|
||||
let _ = ldap_dn;
|
||||
return Ok(Some(user));
|
||||
}
|
||||
|
||||
self.data
|
||||
.get_or_create_ldap_auth_user(
|
||||
email,
|
||||
username,
|
||||
ldap_dn,
|
||||
ldap_username,
|
||||
logged_in_at,
|
||||
initial_gift_usd,
|
||||
unlimited,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn initialize_auth_user_wallet(
|
||||
&self,
|
||||
user_id: &str,
|
||||
initial_gift_usd: f64,
|
||||
unlimited: bool,
|
||||
) -> Result<Option<aether_data::repository::wallet::StoredWalletSnapshot>, GatewayError> {
|
||||
#[cfg(test)]
|
||||
if let Some(store) = self.auth_wallet_store.as_ref() {
|
||||
let gift_balance = if unlimited {
|
||||
0.0
|
||||
} else {
|
||||
initial_gift_usd.max(0.0)
|
||||
};
|
||||
let now_unix_secs = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs() as i64;
|
||||
let wallet = aether_data::repository::wallet::StoredWalletSnapshot::new(
|
||||
uuid::Uuid::new_v4().to_string(),
|
||||
Some(user_id.to_string()),
|
||||
None,
|
||||
0.0,
|
||||
gift_balance,
|
||||
if unlimited {
|
||||
"unlimited".to_string()
|
||||
} else {
|
||||
"finite".to_string()
|
||||
},
|
||||
"USD".to_string(),
|
||||
"active".to_string(),
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
gift_balance,
|
||||
now_unix_secs,
|
||||
)
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?;
|
||||
store
|
||||
.lock()
|
||||
.expect("auth wallet store should lock")
|
||||
.insert(wallet.id.clone(), wallet.clone());
|
||||
return Ok(Some(wallet));
|
||||
}
|
||||
|
||||
self.data
|
||||
.initialize_auth_user_wallet(user_id, initial_gift_usd, unlimited)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn update_auth_user_wallet_limit_mode(
|
||||
&self,
|
||||
user_id: &str,
|
||||
limit_mode: &str,
|
||||
) -> Result<Option<aether_data::repository::wallet::StoredWalletSnapshot>, GatewayError> {
|
||||
#[cfg(test)]
|
||||
if let Some(store) = self.auth_wallet_store.as_ref() {
|
||||
let mut guard = store.lock().expect("auth wallet store should lock");
|
||||
let Some((wallet_id, wallet)) = guard
|
||||
.iter_mut()
|
||||
.find(|(_, wallet)| wallet.user_id.as_deref() == Some(user_id))
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
let _ = wallet_id;
|
||||
wallet.limit_mode = limit_mode.to_string();
|
||||
wallet.updated_at_unix_secs = chrono::Utc::now().timestamp().max(0) as u64;
|
||||
return Ok(Some(wallet.clone()));
|
||||
}
|
||||
|
||||
self.data
|
||||
.update_auth_user_wallet_limit_mode(user_id, limit_mode)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
}
|
||||
397
apps/aether-gateway/src/state/runtime/billing/admin.rs
Normal file
397
apps/aether-gateway/src/state/runtime/billing/admin.rs
Normal file
@@ -0,0 +1,397 @@
|
||||
use super::{
|
||||
AdminBillingCollectorRecord, AdminBillingCollectorWriteInput, AdminBillingPresetApplyResult,
|
||||
AdminBillingRuleRecord, AdminBillingRuleWriteInput, AppState, GatewayError,
|
||||
LocalMutationOutcome,
|
||||
};
|
||||
use crate::query::billing as billing_query;
|
||||
|
||||
impl AppState {
|
||||
pub(crate) async fn admin_billing_enabled_default_value_exists(
|
||||
&self,
|
||||
api_format: &str,
|
||||
task_type: &str,
|
||||
dimension_name: &str,
|
||||
existing_id: Option<&str>,
|
||||
) -> Result<bool, GatewayError> {
|
||||
#[cfg(test)]
|
||||
if let Some(store) = self.admin_billing_collector_store.as_ref() {
|
||||
let exists = store
|
||||
.lock()
|
||||
.expect("admin billing collector store should lock")
|
||||
.values()
|
||||
.any(|collector| {
|
||||
collector.api_format == api_format
|
||||
&& collector.task_type == task_type
|
||||
&& collector.dimension_name == dimension_name
|
||||
&& collector.is_enabled
|
||||
&& collector.default_value.is_some()
|
||||
&& existing_id.is_none_or(|value| collector.id != value)
|
||||
});
|
||||
return Ok(exists);
|
||||
}
|
||||
|
||||
let Some(pool) = self.postgres_pool() else {
|
||||
return Ok(false);
|
||||
};
|
||||
billing_query::admin_billing_enabled_default_value_exists(
|
||||
&pool,
|
||||
api_format,
|
||||
task_type,
|
||||
dimension_name,
|
||||
existing_id,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn create_admin_billing_rule(
|
||||
&self,
|
||||
input: &AdminBillingRuleWriteInput,
|
||||
) -> Result<LocalMutationOutcome<AdminBillingRuleRecord>, GatewayError> {
|
||||
#[cfg(test)]
|
||||
if let Some(store) = self.admin_billing_rule_store.as_ref() {
|
||||
let now_unix_secs = chrono::Utc::now().timestamp().max(0) as u64;
|
||||
let record = AdminBillingRuleRecord {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
name: input.name.clone(),
|
||||
task_type: input.task_type.clone(),
|
||||
global_model_id: input.global_model_id.clone(),
|
||||
model_id: input.model_id.clone(),
|
||||
expression: input.expression.clone(),
|
||||
variables: input.variables.clone(),
|
||||
dimension_mappings: input.dimension_mappings.clone(),
|
||||
is_enabled: input.is_enabled,
|
||||
created_at_unix_secs: now_unix_secs,
|
||||
updated_at_unix_secs: now_unix_secs,
|
||||
};
|
||||
store
|
||||
.lock()
|
||||
.expect("admin billing rule store should lock")
|
||||
.insert(record.id.clone(), record.clone());
|
||||
return Ok(LocalMutationOutcome::Applied(record));
|
||||
}
|
||||
|
||||
let Some(pool) = self.postgres_pool() else {
|
||||
return Ok(LocalMutationOutcome::Unavailable);
|
||||
};
|
||||
billing_query::create_admin_billing_rule(&pool, input).await
|
||||
}
|
||||
|
||||
pub(crate) async fn list_admin_billing_rules(
|
||||
&self,
|
||||
task_type: Option<&str>,
|
||||
is_enabled: Option<bool>,
|
||||
page: u32,
|
||||
page_size: u32,
|
||||
) -> Result<Option<(Vec<AdminBillingRuleRecord>, u64)>, GatewayError> {
|
||||
#[cfg(test)]
|
||||
if let Some(store) = self.admin_billing_rule_store.as_ref() {
|
||||
let mut items = store
|
||||
.lock()
|
||||
.expect("admin billing rule store should lock")
|
||||
.values()
|
||||
.filter(|record| {
|
||||
task_type.is_none_or(|expected| record.task_type == expected)
|
||||
&& is_enabled.is_none_or(|expected| record.is_enabled == expected)
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
items.sort_by(|left, right| {
|
||||
right
|
||||
.updated_at_unix_secs
|
||||
.cmp(&left.updated_at_unix_secs)
|
||||
.then_with(|| right.id.cmp(&left.id))
|
||||
});
|
||||
let total = items.len() as u64;
|
||||
let offset = (page.saturating_sub(1) as usize) * (page_size as usize);
|
||||
let items = items
|
||||
.into_iter()
|
||||
.skip(offset)
|
||||
.take(page_size as usize)
|
||||
.collect::<Vec<_>>();
|
||||
return Ok(Some((items, total)));
|
||||
}
|
||||
|
||||
let Some(pool) = self.postgres_pool() else {
|
||||
return Ok(None);
|
||||
};
|
||||
let (items, total) =
|
||||
billing_query::list_admin_billing_rules(&pool, task_type, is_enabled, page, page_size)
|
||||
.await?;
|
||||
Ok(Some((items, total)))
|
||||
}
|
||||
|
||||
pub(crate) async fn read_admin_billing_rule(
|
||||
&self,
|
||||
rule_id: &str,
|
||||
) -> Result<Option<AdminBillingRuleRecord>, GatewayError> {
|
||||
#[cfg(test)]
|
||||
if let Some(store) = self.admin_billing_rule_store.as_ref() {
|
||||
return Ok(store
|
||||
.lock()
|
||||
.expect("admin billing rule store should lock")
|
||||
.get(rule_id)
|
||||
.cloned());
|
||||
}
|
||||
|
||||
let Some(pool) = self.postgres_pool() else {
|
||||
return Ok(None);
|
||||
};
|
||||
billing_query::find_admin_billing_rule(&pool, rule_id).await
|
||||
}
|
||||
|
||||
pub(crate) async fn update_admin_billing_rule(
|
||||
&self,
|
||||
rule_id: &str,
|
||||
input: &AdminBillingRuleWriteInput,
|
||||
) -> Result<LocalMutationOutcome<AdminBillingRuleRecord>, GatewayError> {
|
||||
#[cfg(test)]
|
||||
if let Some(store) = self.admin_billing_rule_store.as_ref() {
|
||||
let mut guard = store.lock().expect("admin billing rule store should lock");
|
||||
let Some(record) = guard.get_mut(rule_id) else {
|
||||
return Ok(LocalMutationOutcome::NotFound);
|
||||
};
|
||||
record.name = input.name.clone();
|
||||
record.task_type = input.task_type.clone();
|
||||
record.global_model_id = input.global_model_id.clone();
|
||||
record.model_id = input.model_id.clone();
|
||||
record.expression = input.expression.clone();
|
||||
record.variables = input.variables.clone();
|
||||
record.dimension_mappings = input.dimension_mappings.clone();
|
||||
record.is_enabled = input.is_enabled;
|
||||
record.updated_at_unix_secs = chrono::Utc::now().timestamp().max(0) as u64;
|
||||
return Ok(LocalMutationOutcome::Applied(record.clone()));
|
||||
}
|
||||
|
||||
let Some(pool) = self.postgres_pool() else {
|
||||
return Ok(LocalMutationOutcome::Unavailable);
|
||||
};
|
||||
billing_query::update_admin_billing_rule(&pool, rule_id, input).await
|
||||
}
|
||||
|
||||
pub(crate) async fn create_admin_billing_collector(
|
||||
&self,
|
||||
input: &AdminBillingCollectorWriteInput,
|
||||
) -> Result<LocalMutationOutcome<AdminBillingCollectorRecord>, GatewayError> {
|
||||
#[cfg(test)]
|
||||
if let Some(store) = self.admin_billing_collector_store.as_ref() {
|
||||
let now_unix_secs = chrono::Utc::now().timestamp().max(0) as u64;
|
||||
let record = AdminBillingCollectorRecord {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
api_format: input.api_format.clone(),
|
||||
task_type: input.task_type.clone(),
|
||||
dimension_name: input.dimension_name.clone(),
|
||||
source_type: input.source_type.clone(),
|
||||
source_path: input.source_path.clone(),
|
||||
value_type: input.value_type.clone(),
|
||||
transform_expression: input.transform_expression.clone(),
|
||||
default_value: input.default_value.clone(),
|
||||
priority: input.priority,
|
||||
is_enabled: input.is_enabled,
|
||||
created_at_unix_secs: now_unix_secs,
|
||||
updated_at_unix_secs: now_unix_secs,
|
||||
};
|
||||
store
|
||||
.lock()
|
||||
.expect("admin billing collector store should lock")
|
||||
.insert(record.id.clone(), record.clone());
|
||||
return Ok(LocalMutationOutcome::Applied(record));
|
||||
}
|
||||
|
||||
let Some(pool) = self.postgres_pool() else {
|
||||
return Ok(LocalMutationOutcome::Unavailable);
|
||||
};
|
||||
billing_query::create_admin_billing_collector(&pool, input).await
|
||||
}
|
||||
|
||||
pub(crate) async fn list_admin_billing_collectors(
|
||||
&self,
|
||||
api_format: Option<&str>,
|
||||
task_type: Option<&str>,
|
||||
dimension_name: Option<&str>,
|
||||
is_enabled: Option<bool>,
|
||||
page: u32,
|
||||
page_size: u32,
|
||||
) -> Result<Option<(Vec<AdminBillingCollectorRecord>, u64)>, GatewayError> {
|
||||
#[cfg(test)]
|
||||
if let Some(store) = self.admin_billing_collector_store.as_ref() {
|
||||
let mut items = store
|
||||
.lock()
|
||||
.expect("admin billing collector store should lock")
|
||||
.values()
|
||||
.filter(|record| {
|
||||
api_format.is_none_or(|expected| record.api_format == expected)
|
||||
&& task_type.is_none_or(|expected| record.task_type == expected)
|
||||
&& dimension_name.is_none_or(|expected| record.dimension_name == expected)
|
||||
&& is_enabled.is_none_or(|expected| record.is_enabled == expected)
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
items.sort_by(|left, right| {
|
||||
right
|
||||
.updated_at_unix_secs
|
||||
.cmp(&left.updated_at_unix_secs)
|
||||
.then_with(|| right.priority.cmp(&left.priority))
|
||||
.then_with(|| right.id.cmp(&left.id))
|
||||
});
|
||||
let total = items.len() as u64;
|
||||
let offset = (page.saturating_sub(1) as usize) * (page_size as usize);
|
||||
let items = items
|
||||
.into_iter()
|
||||
.skip(offset)
|
||||
.take(page_size as usize)
|
||||
.collect::<Vec<_>>();
|
||||
return Ok(Some((items, total)));
|
||||
}
|
||||
|
||||
let Some(pool) = self.postgres_pool() else {
|
||||
return Ok(None);
|
||||
};
|
||||
let (items, total) = billing_query::list_admin_billing_collectors(
|
||||
&pool,
|
||||
api_format,
|
||||
task_type,
|
||||
dimension_name,
|
||||
is_enabled,
|
||||
page,
|
||||
page_size,
|
||||
)
|
||||
.await?;
|
||||
Ok(Some((items, total)))
|
||||
}
|
||||
|
||||
pub(crate) async fn read_admin_billing_collector(
|
||||
&self,
|
||||
collector_id: &str,
|
||||
) -> Result<Option<AdminBillingCollectorRecord>, GatewayError> {
|
||||
#[cfg(test)]
|
||||
if let Some(store) = self.admin_billing_collector_store.as_ref() {
|
||||
return Ok(store
|
||||
.lock()
|
||||
.expect("admin billing collector store should lock")
|
||||
.get(collector_id)
|
||||
.cloned());
|
||||
}
|
||||
|
||||
let Some(pool) = self.postgres_pool() else {
|
||||
return Ok(None);
|
||||
};
|
||||
billing_query::find_admin_billing_collector(&pool, collector_id).await
|
||||
}
|
||||
|
||||
pub(crate) async fn update_admin_billing_collector(
|
||||
&self,
|
||||
collector_id: &str,
|
||||
input: &AdminBillingCollectorWriteInput,
|
||||
) -> Result<LocalMutationOutcome<AdminBillingCollectorRecord>, GatewayError> {
|
||||
#[cfg(test)]
|
||||
if let Some(store) = self.admin_billing_collector_store.as_ref() {
|
||||
let mut guard = store
|
||||
.lock()
|
||||
.expect("admin billing collector store should lock");
|
||||
let Some(record) = guard.get_mut(collector_id) else {
|
||||
return Ok(LocalMutationOutcome::NotFound);
|
||||
};
|
||||
record.api_format = input.api_format.clone();
|
||||
record.task_type = input.task_type.clone();
|
||||
record.dimension_name = input.dimension_name.clone();
|
||||
record.source_type = input.source_type.clone();
|
||||
record.source_path = input.source_path.clone();
|
||||
record.value_type = input.value_type.clone();
|
||||
record.transform_expression = input.transform_expression.clone();
|
||||
record.default_value = input.default_value.clone();
|
||||
record.priority = input.priority;
|
||||
record.is_enabled = input.is_enabled;
|
||||
record.updated_at_unix_secs = chrono::Utc::now().timestamp().max(0) as u64;
|
||||
return Ok(LocalMutationOutcome::Applied(record.clone()));
|
||||
}
|
||||
|
||||
let Some(pool) = self.postgres_pool() else {
|
||||
return Ok(LocalMutationOutcome::Unavailable);
|
||||
};
|
||||
billing_query::update_admin_billing_collector(&pool, collector_id, input).await
|
||||
}
|
||||
|
||||
pub(crate) async fn apply_admin_billing_preset(
|
||||
&self,
|
||||
preset: &str,
|
||||
mode: &str,
|
||||
collectors: &[AdminBillingCollectorWriteInput],
|
||||
) -> Result<LocalMutationOutcome<AdminBillingPresetApplyResult>, GatewayError> {
|
||||
#[cfg(test)]
|
||||
if let Some(store) = self.admin_billing_collector_store.as_ref() {
|
||||
let mut created = 0_u64;
|
||||
let mut updated = 0_u64;
|
||||
let mut skipped = 0_u64;
|
||||
let now_unix_secs = chrono::Utc::now().timestamp().max(0) as u64;
|
||||
let mut guard = store
|
||||
.lock()
|
||||
.expect("admin billing collector store should lock");
|
||||
for collector in collectors {
|
||||
let existing_id = guard
|
||||
.values()
|
||||
.find(|record| {
|
||||
record.api_format == collector.api_format
|
||||
&& record.task_type == collector.task_type
|
||||
&& record.dimension_name == collector.dimension_name
|
||||
&& record.priority == collector.priority
|
||||
&& record.is_enabled
|
||||
})
|
||||
.map(|record| record.id.clone());
|
||||
|
||||
match existing_id {
|
||||
Some(existing_id) if mode == "overwrite" => {
|
||||
if let Some(record) = guard.get_mut(&existing_id) {
|
||||
record.source_type = collector.source_type.clone();
|
||||
record.source_path = collector.source_path.clone();
|
||||
record.value_type = collector.value_type.clone();
|
||||
record.transform_expression = collector.transform_expression.clone();
|
||||
record.default_value = collector.default_value.clone();
|
||||
record.is_enabled = collector.is_enabled;
|
||||
record.updated_at_unix_secs = now_unix_secs;
|
||||
updated += 1;
|
||||
} else {
|
||||
skipped += 1;
|
||||
}
|
||||
}
|
||||
Some(_) => {
|
||||
skipped += 1;
|
||||
}
|
||||
None => {
|
||||
let record = AdminBillingCollectorRecord {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
api_format: collector.api_format.clone(),
|
||||
task_type: collector.task_type.clone(),
|
||||
dimension_name: collector.dimension_name.clone(),
|
||||
source_type: collector.source_type.clone(),
|
||||
source_path: collector.source_path.clone(),
|
||||
value_type: collector.value_type.clone(),
|
||||
transform_expression: collector.transform_expression.clone(),
|
||||
default_value: collector.default_value.clone(),
|
||||
priority: collector.priority,
|
||||
is_enabled: collector.is_enabled,
|
||||
created_at_unix_secs: now_unix_secs,
|
||||
updated_at_unix_secs: now_unix_secs,
|
||||
};
|
||||
guard.insert(record.id.clone(), record);
|
||||
created += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return Ok(LocalMutationOutcome::Applied(
|
||||
AdminBillingPresetApplyResult {
|
||||
preset: preset.to_string(),
|
||||
mode: mode.to_string(),
|
||||
created,
|
||||
updated,
|
||||
skipped,
|
||||
errors: Vec::new(),
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
let Some(pool) = self.postgres_pool() else {
|
||||
return Ok(LocalMutationOutcome::Unavailable);
|
||||
};
|
||||
billing_query::apply_admin_billing_preset(&pool, preset, mode, collectors).await
|
||||
}
|
||||
}
|
||||
464
apps/aether-gateway/src/state/runtime/billing/finance_queries.rs
Normal file
464
apps/aether-gateway/src/state/runtime/billing/finance_queries.rs
Normal file
@@ -0,0 +1,464 @@
|
||||
use aether_data::repository::wallet::{
|
||||
AdminPaymentOrderListQuery, AdminWalletLedgerQuery, AdminWalletListQuery,
|
||||
AdminWalletRefundRequestListQuery, StoredAdminPaymentCallback, StoredAdminPaymentOrder,
|
||||
StoredAdminWalletLedgerItem, StoredAdminWalletListItem, StoredAdminWalletRefund,
|
||||
StoredAdminWalletRefundRequestItem, StoredAdminWalletTransaction,
|
||||
};
|
||||
|
||||
use crate::state::AdminPaymentCallbackRecord;
|
||||
use crate::{
|
||||
AdminWalletMutationOutcome, AdminWalletPaymentOrderRecord, AdminWalletRefundRecord, AppState,
|
||||
GatewayError,
|
||||
};
|
||||
|
||||
impl AppState {
|
||||
pub(crate) async fn list_admin_wallets(
|
||||
&self,
|
||||
status: Option<&str>,
|
||||
owner_type: Option<&str>,
|
||||
limit: usize,
|
||||
offset: usize,
|
||||
) -> Result<(Vec<StoredAdminWalletListItem>, u64), GatewayError> {
|
||||
let page = self
|
||||
.data
|
||||
.list_admin_wallets(&AdminWalletListQuery {
|
||||
status: status.map(ToOwned::to_owned),
|
||||
owner_type: owner_type.map(ToOwned::to_owned),
|
||||
limit,
|
||||
offset,
|
||||
})
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?;
|
||||
Ok((page.items, page.total))
|
||||
}
|
||||
|
||||
pub(crate) async fn list_admin_wallet_ledger(
|
||||
&self,
|
||||
category: Option<&str>,
|
||||
reason_code: Option<&str>,
|
||||
owner_type: Option<&str>,
|
||||
limit: usize,
|
||||
offset: usize,
|
||||
) -> Result<(Vec<StoredAdminWalletLedgerItem>, u64), GatewayError> {
|
||||
let page = self
|
||||
.data
|
||||
.list_admin_wallet_ledger(&AdminWalletLedgerQuery {
|
||||
category: category.map(ToOwned::to_owned),
|
||||
reason_code: reason_code.map(ToOwned::to_owned),
|
||||
owner_type: owner_type.map(ToOwned::to_owned),
|
||||
limit,
|
||||
offset,
|
||||
})
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?;
|
||||
Ok((page.items, page.total))
|
||||
}
|
||||
|
||||
pub(crate) async fn list_admin_payment_orders(
|
||||
&self,
|
||||
status: Option<&str>,
|
||||
payment_method: Option<&str>,
|
||||
limit: usize,
|
||||
offset: usize,
|
||||
) -> Result<Option<(Vec<AdminWalletPaymentOrderRecord>, u64)>, GatewayError> {
|
||||
#[cfg(test)]
|
||||
if let Some(store) = self.admin_wallet_payment_order_store.as_ref() {
|
||||
let now_unix_secs = chrono::Utc::now().timestamp().max(0) as u64;
|
||||
let mut items = store
|
||||
.lock()
|
||||
.expect("admin wallet payment order store should lock")
|
||||
.values()
|
||||
.filter(|order| {
|
||||
payment_method.is_none_or(|expected| order.payment_method == expected)
|
||||
&& status.is_none_or(|expected| {
|
||||
let effective_status = if order.status == "pending"
|
||||
&& order
|
||||
.expires_at_unix_secs
|
||||
.is_some_and(|value| value < now_unix_secs)
|
||||
{
|
||||
"expired"
|
||||
} else {
|
||||
order.status.as_str()
|
||||
};
|
||||
effective_status == expected
|
||||
})
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
items.sort_by(|left, right| {
|
||||
right
|
||||
.created_at_unix_secs
|
||||
.cmp(&left.created_at_unix_secs)
|
||||
.then_with(|| right.id.cmp(&left.id))
|
||||
});
|
||||
let total = items.len() as u64;
|
||||
let items = items
|
||||
.into_iter()
|
||||
.skip(offset)
|
||||
.take(limit)
|
||||
.collect::<Vec<_>>();
|
||||
return Ok(Some((items, total)));
|
||||
}
|
||||
|
||||
let page = self
|
||||
.data
|
||||
.list_admin_payment_orders(&AdminPaymentOrderListQuery {
|
||||
status: status.map(ToOwned::to_owned),
|
||||
payment_method: payment_method.map(ToOwned::to_owned),
|
||||
limit,
|
||||
offset,
|
||||
})
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?;
|
||||
Ok(Some((
|
||||
page.items
|
||||
.into_iter()
|
||||
.map(stored_admin_payment_order_to_gateway)
|
||||
.collect(),
|
||||
page.total,
|
||||
)))
|
||||
}
|
||||
|
||||
pub(crate) async fn list_admin_payment_callbacks(
|
||||
&self,
|
||||
payment_method: Option<&str>,
|
||||
limit: usize,
|
||||
offset: usize,
|
||||
) -> Result<Option<(Vec<AdminPaymentCallbackRecord>, u64)>, GatewayError> {
|
||||
#[cfg(test)]
|
||||
if let Some(store) = self.admin_payment_callback_store.as_ref() {
|
||||
let mut items = store
|
||||
.lock()
|
||||
.expect("admin payment callback store should lock")
|
||||
.values()
|
||||
.filter(|callback| {
|
||||
payment_method.is_none_or(|expected| callback.payment_method == expected)
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
items.sort_by(|left, right| {
|
||||
right
|
||||
.created_at_unix_secs
|
||||
.cmp(&left.created_at_unix_secs)
|
||||
.then_with(|| right.id.cmp(&left.id))
|
||||
});
|
||||
let total = items.len() as u64;
|
||||
let items = items
|
||||
.into_iter()
|
||||
.skip(offset)
|
||||
.take(limit)
|
||||
.collect::<Vec<_>>();
|
||||
return Ok(Some((items, total)));
|
||||
}
|
||||
|
||||
let page = self
|
||||
.data
|
||||
.list_admin_payment_callbacks(payment_method, limit, offset)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?;
|
||||
Ok(Some((
|
||||
page.items
|
||||
.into_iter()
|
||||
.map(stored_admin_payment_callback_to_gateway)
|
||||
.collect(),
|
||||
page.total,
|
||||
)))
|
||||
}
|
||||
|
||||
pub(crate) async fn list_admin_wallet_transactions(
|
||||
&self,
|
||||
wallet_id: &str,
|
||||
limit: usize,
|
||||
offset: usize,
|
||||
) -> Result<(Vec<StoredAdminWalletTransaction>, u64), GatewayError> {
|
||||
#[cfg(test)]
|
||||
if let Some(store) = self.admin_wallet_transaction_store.as_ref() {
|
||||
let mut items = store
|
||||
.lock()
|
||||
.expect("admin wallet transaction store should lock")
|
||||
.values()
|
||||
.filter(|transaction| transaction.wallet_id == wallet_id)
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
items.sort_by(|left, right| {
|
||||
right
|
||||
.created_at_unix_secs
|
||||
.cmp(&left.created_at_unix_secs)
|
||||
.then_with(|| right.id.cmp(&left.id))
|
||||
});
|
||||
let total = items.len() as u64;
|
||||
let items = items
|
||||
.into_iter()
|
||||
.skip(offset)
|
||||
.take(limit)
|
||||
.map(|record| StoredAdminWalletTransaction {
|
||||
id: record.id,
|
||||
wallet_id: record.wallet_id,
|
||||
category: record.category,
|
||||
reason_code: record.reason_code,
|
||||
amount: record.amount,
|
||||
balance_before: record.balance_before,
|
||||
balance_after: record.balance_after,
|
||||
recharge_balance_before: record.recharge_balance_before,
|
||||
recharge_balance_after: record.recharge_balance_after,
|
||||
gift_balance_before: record.gift_balance_before,
|
||||
gift_balance_after: record.gift_balance_after,
|
||||
link_type: record.link_type,
|
||||
link_id: record.link_id,
|
||||
operator_id: record.operator_id,
|
||||
operator_name: None,
|
||||
operator_email: None,
|
||||
description: record.description,
|
||||
created_at_unix_secs: Some(record.created_at_unix_secs),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
return Ok((items, total));
|
||||
}
|
||||
|
||||
let page = self
|
||||
.data
|
||||
.list_admin_wallet_transactions(wallet_id, limit, offset)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?;
|
||||
Ok((page.items, page.total))
|
||||
}
|
||||
|
||||
pub(crate) async fn list_admin_wallet_refunds(
|
||||
&self,
|
||||
wallet_id: &str,
|
||||
limit: usize,
|
||||
offset: usize,
|
||||
) -> Result<(Vec<AdminWalletRefundRecord>, u64), GatewayError> {
|
||||
#[cfg(test)]
|
||||
if let Some(store) = self.admin_wallet_refund_store.as_ref() {
|
||||
let mut items = store
|
||||
.lock()
|
||||
.expect("admin wallet refund store should lock")
|
||||
.values()
|
||||
.filter(|refund| refund.wallet_id == wallet_id)
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
items.sort_by(|left, right| {
|
||||
right
|
||||
.created_at_unix_secs
|
||||
.cmp(&left.created_at_unix_secs)
|
||||
.then_with(|| right.id.cmp(&left.id))
|
||||
});
|
||||
let total = items.len() as u64;
|
||||
let items = items
|
||||
.into_iter()
|
||||
.skip(offset)
|
||||
.take(limit)
|
||||
.collect::<Vec<_>>();
|
||||
return Ok((items, total));
|
||||
}
|
||||
|
||||
let page = self
|
||||
.data
|
||||
.list_admin_wallet_refunds(wallet_id, limit, offset)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?;
|
||||
Ok((
|
||||
page.items
|
||||
.into_iter()
|
||||
.map(stored_admin_wallet_refund_to_gateway)
|
||||
.collect(),
|
||||
page.total,
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) async fn list_admin_wallet_refund_requests(
|
||||
&self,
|
||||
status: Option<&str>,
|
||||
limit: usize,
|
||||
offset: usize,
|
||||
) -> Result<(Vec<StoredAdminWalletRefundRequestItem>, u64), GatewayError> {
|
||||
#[cfg(test)]
|
||||
if let (Some(wallet_store), Some(refund_store)) = (
|
||||
self.auth_wallet_store.as_ref(),
|
||||
self.admin_wallet_refund_store.as_ref(),
|
||||
) {
|
||||
let wallets = wallet_store
|
||||
.lock()
|
||||
.expect("auth wallet store should lock")
|
||||
.clone();
|
||||
let mut items = refund_store
|
||||
.lock()
|
||||
.expect("admin wallet refund store should lock")
|
||||
.values()
|
||||
.filter(|refund| status.is_none_or(|expected| refund.status == expected))
|
||||
.filter(|refund| {
|
||||
wallets
|
||||
.get(&refund.wallet_id)
|
||||
.is_some_and(|wallet| wallet.user_id.is_some())
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
items.sort_by(|left, right| {
|
||||
right
|
||||
.created_at_unix_secs
|
||||
.cmp(&left.created_at_unix_secs)
|
||||
.then_with(|| right.id.cmp(&left.id))
|
||||
});
|
||||
let total = items.len() as u64;
|
||||
let items = items
|
||||
.into_iter()
|
||||
.skip(offset)
|
||||
.take(limit)
|
||||
.filter_map(|refund| {
|
||||
wallets.get(&refund.wallet_id).map(|wallet| {
|
||||
StoredAdminWalletRefundRequestItem {
|
||||
id: refund.id,
|
||||
refund_no: refund.refund_no,
|
||||
wallet_id: refund.wallet_id,
|
||||
user_id: refund.user_id,
|
||||
payment_order_id: refund.payment_order_id,
|
||||
source_type: refund.source_type,
|
||||
source_id: refund.source_id,
|
||||
refund_mode: refund.refund_mode,
|
||||
amount_usd: refund.amount_usd,
|
||||
status: refund.status,
|
||||
reason: refund.reason,
|
||||
failure_reason: refund.failure_reason,
|
||||
gateway_refund_id: refund.gateway_refund_id,
|
||||
payout_method: refund.payout_method,
|
||||
payout_reference: refund.payout_reference,
|
||||
payout_proof: refund.payout_proof,
|
||||
requested_by: refund.requested_by,
|
||||
approved_by: refund.approved_by,
|
||||
processed_by: refund.processed_by,
|
||||
wallet_user_id: wallet.user_id.clone(),
|
||||
wallet_user_name: None,
|
||||
wallet_api_key_id: wallet.api_key_id.clone(),
|
||||
api_key_name: None,
|
||||
wallet_status: wallet.status.clone(),
|
||||
created_at_unix_secs: Some(refund.created_at_unix_secs),
|
||||
updated_at_unix_secs: Some(refund.updated_at_unix_secs),
|
||||
processed_at_unix_secs: refund.processed_at_unix_secs,
|
||||
completed_at_unix_secs: refund.completed_at_unix_secs,
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
return Ok((items, total));
|
||||
}
|
||||
|
||||
let page = self
|
||||
.data
|
||||
.list_admin_wallet_refund_requests(&AdminWalletRefundRequestListQuery {
|
||||
status: status.map(ToOwned::to_owned),
|
||||
limit,
|
||||
offset,
|
||||
})
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?;
|
||||
Ok((page.items, page.total))
|
||||
}
|
||||
|
||||
pub(crate) async fn read_admin_payment_order(
|
||||
&self,
|
||||
order_id: &str,
|
||||
) -> Result<AdminWalletMutationOutcome<AdminWalletPaymentOrderRecord>, GatewayError> {
|
||||
#[cfg(test)]
|
||||
if let Some(store) = self.admin_wallet_payment_order_store.as_ref() {
|
||||
return Ok(store
|
||||
.lock()
|
||||
.expect("admin wallet payment order store should lock")
|
||||
.get(order_id)
|
||||
.cloned()
|
||||
.map(AdminWalletMutationOutcome::Applied)
|
||||
.unwrap_or(AdminWalletMutationOutcome::NotFound));
|
||||
}
|
||||
|
||||
if !self.has_wallet_data_reader() {
|
||||
return Ok(AdminWalletMutationOutcome::Unavailable);
|
||||
}
|
||||
|
||||
let order = self
|
||||
.data
|
||||
.find_admin_payment_order(order_id)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?;
|
||||
match order {
|
||||
Some(record) => Ok(AdminWalletMutationOutcome::Applied(
|
||||
stored_admin_payment_order_to_gateway(record),
|
||||
)),
|
||||
None => Ok(AdminWalletMutationOutcome::NotFound),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn stored_admin_payment_order_to_gateway(
|
||||
record: StoredAdminPaymentOrder,
|
||||
) -> AdminWalletPaymentOrderRecord {
|
||||
AdminWalletPaymentOrderRecord {
|
||||
id: record.id,
|
||||
order_no: record.order_no,
|
||||
wallet_id: record.wallet_id,
|
||||
user_id: record.user_id,
|
||||
amount_usd: record.amount_usd,
|
||||
pay_amount: record.pay_amount,
|
||||
pay_currency: record.pay_currency,
|
||||
exchange_rate: record.exchange_rate,
|
||||
refunded_amount_usd: record.refunded_amount_usd,
|
||||
refundable_amount_usd: record.refundable_amount_usd,
|
||||
payment_method: record.payment_method,
|
||||
gateway_order_id: record.gateway_order_id,
|
||||
status: record.status,
|
||||
gateway_response: record.gateway_response,
|
||||
created_at_unix_secs: record.created_at_unix_secs,
|
||||
paid_at_unix_secs: record.paid_at_unix_secs,
|
||||
credited_at_unix_secs: record.credited_at_unix_secs,
|
||||
expires_at_unix_secs: record.expires_at_unix_secs,
|
||||
}
|
||||
}
|
||||
|
||||
fn stored_admin_payment_callback_to_gateway(
|
||||
record: StoredAdminPaymentCallback,
|
||||
) -> AdminPaymentCallbackRecord {
|
||||
AdminPaymentCallbackRecord {
|
||||
id: record.id,
|
||||
payment_order_id: record.payment_order_id,
|
||||
payment_method: record.payment_method,
|
||||
callback_key: record.callback_key,
|
||||
order_no: record.order_no,
|
||||
gateway_order_id: record.gateway_order_id,
|
||||
payload_hash: record.payload_hash,
|
||||
signature_valid: record.signature_valid,
|
||||
status: record.status,
|
||||
payload: record.payload,
|
||||
error_message: record.error_message,
|
||||
created_at_unix_secs: record.created_at_unix_secs,
|
||||
processed_at_unix_secs: record.processed_at_unix_secs,
|
||||
}
|
||||
}
|
||||
|
||||
fn stored_admin_wallet_refund_to_gateway(
|
||||
record: StoredAdminWalletRefund,
|
||||
) -> AdminWalletRefundRecord {
|
||||
AdminWalletRefundRecord {
|
||||
id: record.id,
|
||||
refund_no: record.refund_no,
|
||||
wallet_id: record.wallet_id,
|
||||
user_id: record.user_id,
|
||||
payment_order_id: record.payment_order_id,
|
||||
source_type: record.source_type,
|
||||
source_id: record.source_id,
|
||||
refund_mode: record.refund_mode,
|
||||
amount_usd: record.amount_usd,
|
||||
status: record.status,
|
||||
reason: record.reason,
|
||||
failure_reason: record.failure_reason,
|
||||
gateway_refund_id: record.gateway_refund_id,
|
||||
payout_method: record.payout_method,
|
||||
payout_reference: record.payout_reference,
|
||||
payout_proof: record.payout_proof,
|
||||
requested_by: record.requested_by,
|
||||
approved_by: record.approved_by,
|
||||
processed_by: record.processed_by,
|
||||
created_at_unix_secs: record.created_at_unix_secs,
|
||||
updated_at_unix_secs: record.updated_at_unix_secs,
|
||||
processed_at_unix_secs: record.processed_at_unix_secs,
|
||||
completed_at_unix_secs: record.completed_at_unix_secs,
|
||||
}
|
||||
}
|
||||
8
apps/aether-gateway/src/state/runtime/billing/mod.rs
Normal file
8
apps/aether-gateway/src/state/runtime/billing/mod.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
use super::super::{
|
||||
AdminBillingCollectorRecord, AdminBillingCollectorWriteInput, AdminBillingPresetApplyResult,
|
||||
AdminBillingRuleRecord, AdminBillingRuleWriteInput, AppState, GatewayError,
|
||||
LocalMutationOutcome,
|
||||
};
|
||||
|
||||
mod admin;
|
||||
mod finance_queries;
|
||||
63
apps/aether-gateway/src/state/runtime/candidate_queries.rs
Normal file
63
apps/aether-gateway/src/state/runtime/candidate_queries.rs
Normal file
@@ -0,0 +1,63 @@
|
||||
use crate::{AppState, GatewayError};
|
||||
|
||||
impl AppState {
|
||||
pub(crate) async fn list_minimal_candidate_selection_rows_for_api_format(
|
||||
&self,
|
||||
api_format: &str,
|
||||
) -> Result<
|
||||
Vec<aether_data::repository::candidate_selection::StoredMinimalCandidateSelectionRow>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.data
|
||||
.list_minimal_candidate_selection_rows_for_api_format(api_format)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn list_minimal_candidate_selection_rows_for_api_format_and_global_model(
|
||||
&self,
|
||||
api_format: &str,
|
||||
global_model_name: &str,
|
||||
) -> Result<
|
||||
Vec<aether_data::repository::candidate_selection::StoredMinimalCandidateSelectionRow>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.data
|
||||
.list_minimal_candidate_selection_rows(api_format, global_model_name)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn read_provider_quota_snapshot(
|
||||
&self,
|
||||
provider_id: &str,
|
||||
) -> Result<Option<aether_data::repository::quota::StoredProviderQuotaSnapshot>, GatewayError>
|
||||
{
|
||||
self.data
|
||||
.find_provider_quota_by_provider_id(provider_id)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn read_recent_request_candidates(
|
||||
&self,
|
||||
limit: usize,
|
||||
) -> Result<Vec<aether_data::repository::candidates::StoredRequestCandidate>, GatewayError>
|
||||
{
|
||||
self.data
|
||||
.list_recent_request_candidates(limit)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn upsert_request_candidate(
|
||||
&self,
|
||||
candidate: aether_data::repository::candidates::UpsertRequestCandidateRecord,
|
||||
) -> Result<Option<aether_data::repository::candidates::StoredRequestCandidate>, GatewayError>
|
||||
{
|
||||
self.data
|
||||
.upsert_request_candidate(candidate)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
}
|
||||
92
apps/aether-gateway/src/state/runtime/gemini_files.rs
Normal file
92
apps/aether-gateway/src/state/runtime/gemini_files.rs
Normal file
@@ -0,0 +1,92 @@
|
||||
use crate::{AppState, GatewayError};
|
||||
|
||||
impl AppState {
|
||||
pub(crate) async fn upsert_gemini_file_mapping(
|
||||
&self,
|
||||
record: aether_data::repository::gemini_file_mappings::UpsertGeminiFileMappingRecord,
|
||||
) -> Result<
|
||||
Option<aether_data::repository::gemini_file_mappings::StoredGeminiFileMapping>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.data
|
||||
.upsert_gemini_file_mapping(record)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn list_gemini_file_mappings(
|
||||
&self,
|
||||
query: &aether_data::repository::gemini_file_mappings::GeminiFileMappingListQuery,
|
||||
) -> Result<
|
||||
aether_data::repository::gemini_file_mappings::StoredGeminiFileMappingListPage,
|
||||
GatewayError,
|
||||
> {
|
||||
self.data
|
||||
.list_gemini_file_mappings(query)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn summarize_gemini_file_mappings(
|
||||
&self,
|
||||
now_unix_secs: u64,
|
||||
) -> Result<aether_data::repository::gemini_file_mappings::GeminiFileMappingStats, GatewayError>
|
||||
{
|
||||
self.data
|
||||
.summarize_gemini_file_mappings(now_unix_secs)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_gemini_file_mapping_by_file_name(
|
||||
&self,
|
||||
file_name: &str,
|
||||
) -> Result<bool, GatewayError> {
|
||||
self.data
|
||||
.delete_gemini_file_mapping_by_file_name(file_name)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_gemini_file_mapping_by_id(
|
||||
&self,
|
||||
mapping_id: &str,
|
||||
) -> Result<
|
||||
Option<aether_data::repository::gemini_file_mappings::StoredGeminiFileMapping>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.data
|
||||
.delete_gemini_file_mapping_by_id(mapping_id)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_expired_gemini_file_mappings(
|
||||
&self,
|
||||
now_unix_secs: u64,
|
||||
) -> Result<usize, GatewayError> {
|
||||
self.data
|
||||
.delete_expired_gemini_file_mappings(now_unix_secs)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn cache_set_string_with_ttl(
|
||||
&self,
|
||||
key: &str,
|
||||
value: &str,
|
||||
ttl_seconds: u64,
|
||||
) -> Result<(), GatewayError> {
|
||||
self.data
|
||||
.cache_set_string_with_ttl(key, value, ttl_seconds)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn cache_delete_key(&self, key: &str) -> Result<(), GatewayError> {
|
||||
self.data
|
||||
.cache_delete_key(key)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
}
|
||||
159
apps/aether-gateway/src/state/runtime/mod.rs
Normal file
159
apps/aether-gateway/src/state/runtime/mod.rs
Normal file
@@ -0,0 +1,159 @@
|
||||
use super::super::error::GatewayError;
|
||||
use super::super::{scheduler, usage};
|
||||
use super::{
|
||||
AdminBillingCollectorRecord, AdminBillingCollectorWriteInput, AdminBillingPresetApplyResult,
|
||||
AdminBillingRuleRecord, AdminBillingRuleWriteInput, AdminPaymentCallbackRecord,
|
||||
AdminSecurityBlacklistEntry, AdminWalletMutationOutcome, AdminWalletPaymentOrderRecord,
|
||||
AdminWalletRefundRecord, AdminWalletTransactionRecord, AppState, LocalMutationOutcome,
|
||||
AUTH_API_KEY_LAST_USED_MAX_ENTRIES, AUTH_API_KEY_LAST_USED_TTL,
|
||||
};
|
||||
|
||||
mod announcements;
|
||||
mod api_key_exports;
|
||||
mod audit;
|
||||
mod auth;
|
||||
mod billing;
|
||||
mod candidate_queries;
|
||||
mod gemini_files;
|
||||
mod payments;
|
||||
mod security;
|
||||
mod shadow_results;
|
||||
mod usage_queries;
|
||||
mod user_preferences;
|
||||
mod wallet;
|
||||
|
||||
impl AppState {
|
||||
pub fn has_announcement_data_reader(&self) -> bool {
|
||||
self.data.has_announcement_reader()
|
||||
}
|
||||
|
||||
pub fn has_announcement_data_writer(&self) -> bool {
|
||||
self.data.has_announcement_writer()
|
||||
}
|
||||
|
||||
pub fn has_video_task_data_reader(&self) -> bool {
|
||||
self.data.has_video_task_reader()
|
||||
}
|
||||
|
||||
pub fn has_video_task_data_writer(&self) -> bool {
|
||||
self.data.has_video_task_writer()
|
||||
}
|
||||
|
||||
pub fn has_request_candidate_data_reader(&self) -> bool {
|
||||
self.data.has_request_candidate_reader()
|
||||
}
|
||||
|
||||
pub fn has_request_candidate_data_writer(&self) -> bool {
|
||||
self.data.has_request_candidate_writer()
|
||||
}
|
||||
|
||||
pub fn has_usage_data_reader(&self) -> bool {
|
||||
self.data.has_usage_reader()
|
||||
}
|
||||
|
||||
pub fn has_user_data_reader(&self) -> bool {
|
||||
self.data.has_user_reader()
|
||||
}
|
||||
|
||||
pub fn has_usage_data_writer(&self) -> bool {
|
||||
self.data.has_usage_writer()
|
||||
}
|
||||
|
||||
pub fn has_usage_worker_backend(&self) -> bool {
|
||||
self.data.has_usage_worker_runner()
|
||||
}
|
||||
|
||||
pub fn has_wallet_data_reader(&self) -> bool {
|
||||
self.data.has_wallet_reader()
|
||||
}
|
||||
|
||||
pub fn has_wallet_data_writer(&self) -> bool {
|
||||
self.data.has_wallet_writer()
|
||||
}
|
||||
|
||||
pub fn has_auth_user_write_capability(&self) -> bool {
|
||||
#[cfg(test)]
|
||||
if self.auth_user_store.is_some() {
|
||||
return true;
|
||||
}
|
||||
|
||||
self.postgres_pool().is_some()
|
||||
}
|
||||
|
||||
pub fn has_auth_wallet_write_capability(&self) -> bool {
|
||||
#[cfg(test)]
|
||||
if self.auth_wallet_store.is_some() {
|
||||
return true;
|
||||
}
|
||||
|
||||
self.postgres_pool().is_some()
|
||||
}
|
||||
|
||||
pub fn has_provider_quota_data_writer(&self) -> bool {
|
||||
self.data.has_provider_quota_writer()
|
||||
}
|
||||
|
||||
pub fn has_shadow_result_data_writer(&self) -> bool {
|
||||
self.data.has_shadow_result_writer()
|
||||
}
|
||||
|
||||
pub fn has_shadow_result_data_reader(&self) -> bool {
|
||||
self.data.has_shadow_result_reader()
|
||||
}
|
||||
|
||||
pub(crate) async fn count_active_admin_users(&self) -> Result<u64, GatewayError> {
|
||||
#[cfg(test)]
|
||||
if let Some(store) = self.auth_user_store.as_ref() {
|
||||
let total = store
|
||||
.lock()
|
||||
.expect("auth user store should lock")
|
||||
.values()
|
||||
.filter(|user| {
|
||||
user.role.eq_ignore_ascii_case("admin") && user.is_active && !user.is_deleted
|
||||
})
|
||||
.count() as u64;
|
||||
return Ok(total);
|
||||
}
|
||||
|
||||
self.data
|
||||
.count_active_admin_users()
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn count_user_pending_refunds(
|
||||
&self,
|
||||
user_id: &str,
|
||||
) -> Result<u64, GatewayError> {
|
||||
#[cfg(test)]
|
||||
{
|
||||
let _ = user_id;
|
||||
if self.auth_user_store.is_some() {
|
||||
return Ok(0);
|
||||
}
|
||||
}
|
||||
|
||||
self.data
|
||||
.count_user_pending_refunds(user_id)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn count_user_pending_payment_orders(
|
||||
&self,
|
||||
user_id: &str,
|
||||
) -> Result<u64, GatewayError> {
|
||||
#[cfg(test)]
|
||||
{
|
||||
let _ = user_id;
|
||||
if self.auth_user_store.is_some() {
|
||||
return Ok(0);
|
||||
}
|
||||
}
|
||||
|
||||
self.data
|
||||
.count_user_pending_payment_orders(user_id)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
}
|
||||
3
apps/aether-gateway/src/state/runtime/payments/mod.rs
Normal file
3
apps/aether-gateway/src/state/runtime/payments/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub(super) use super::wallet::admin_payment_gateway_response_map;
|
||||
|
||||
mod order_lifecycle;
|
||||
@@ -0,0 +1,259 @@
|
||||
use super::admin_payment_gateway_response_map;
|
||||
use crate::{
|
||||
AdminWalletMutationOutcome, AdminWalletPaymentOrderRecord, AppState, GatewayError,
|
||||
};
|
||||
|
||||
impl AppState {
|
||||
pub(crate) async fn admin_expire_payment_order(
|
||||
&self,
|
||||
order_id: &str,
|
||||
) -> Result<AdminWalletMutationOutcome<(AdminWalletPaymentOrderRecord, bool)>, GatewayError>
|
||||
{
|
||||
#[cfg(test)]
|
||||
if let Some(store) = self.admin_wallet_payment_order_store.as_ref() {
|
||||
let mut guard = store
|
||||
.lock()
|
||||
.expect("admin wallet payment order store should lock");
|
||||
let Some(order) = guard.get_mut(order_id) else {
|
||||
return Ok(AdminWalletMutationOutcome::NotFound);
|
||||
};
|
||||
if order.status == "credited" {
|
||||
return Ok(AdminWalletMutationOutcome::Invalid(
|
||||
"credited order cannot be expired".to_string(),
|
||||
));
|
||||
}
|
||||
if order.status == "expired" {
|
||||
return Ok(AdminWalletMutationOutcome::Applied((order.clone(), false)));
|
||||
}
|
||||
if order.status != "pending" {
|
||||
return Ok(AdminWalletMutationOutcome::Invalid(format!(
|
||||
"only pending order can be expired: {}",
|
||||
order.status
|
||||
)));
|
||||
}
|
||||
let mut gateway_response =
|
||||
admin_payment_gateway_response_map(order.gateway_response.take());
|
||||
gateway_response.insert(
|
||||
"expire_reason".to_string(),
|
||||
serde_json::Value::String("admin_mark_expired".to_string()),
|
||||
);
|
||||
gateway_response.insert(
|
||||
"expired_at".to_string(),
|
||||
serde_json::Value::String(chrono::Utc::now().to_rfc3339()),
|
||||
);
|
||||
order.status = "expired".to_string();
|
||||
order.gateway_response = Some(serde_json::Value::Object(gateway_response));
|
||||
return Ok(AdminWalletMutationOutcome::Applied((order.clone(), true)));
|
||||
}
|
||||
|
||||
match self.expire_admin_payment_order(order_id).await? {
|
||||
Some(aether_data::repository::wallet::WalletMutationOutcome::Applied((
|
||||
order,
|
||||
changed,
|
||||
))) => Ok(AdminWalletMutationOutcome::Applied((
|
||||
stored_admin_payment_order_to_gateway(order),
|
||||
changed,
|
||||
))),
|
||||
Some(aether_data::repository::wallet::WalletMutationOutcome::NotFound) => {
|
||||
Ok(AdminWalletMutationOutcome::NotFound)
|
||||
}
|
||||
Some(aether_data::repository::wallet::WalletMutationOutcome::Invalid(detail)) => {
|
||||
Ok(AdminWalletMutationOutcome::Invalid(detail))
|
||||
}
|
||||
None => Ok(AdminWalletMutationOutcome::Unavailable),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn admin_fail_payment_order(
|
||||
&self,
|
||||
order_id: &str,
|
||||
) -> Result<AdminWalletMutationOutcome<AdminWalletPaymentOrderRecord>, GatewayError> {
|
||||
#[cfg(test)]
|
||||
if let Some(store) = self.admin_wallet_payment_order_store.as_ref() {
|
||||
let mut guard = store
|
||||
.lock()
|
||||
.expect("admin wallet payment order store should lock");
|
||||
let Some(order) = guard.get_mut(order_id) else {
|
||||
return Ok(AdminWalletMutationOutcome::NotFound);
|
||||
};
|
||||
if order.status == "credited" {
|
||||
return Ok(AdminWalletMutationOutcome::Invalid(
|
||||
"credited order cannot be failed".to_string(),
|
||||
));
|
||||
}
|
||||
let mut gateway_response =
|
||||
admin_payment_gateway_response_map(order.gateway_response.take());
|
||||
gateway_response.insert(
|
||||
"failure_reason".to_string(),
|
||||
serde_json::Value::String("admin_mark_failed".to_string()),
|
||||
);
|
||||
gateway_response.insert(
|
||||
"failed_at".to_string(),
|
||||
serde_json::Value::String(chrono::Utc::now().to_rfc3339()),
|
||||
);
|
||||
order.status = "failed".to_string();
|
||||
order.gateway_response = Some(serde_json::Value::Object(gateway_response));
|
||||
return Ok(AdminWalletMutationOutcome::Applied(order.clone()));
|
||||
}
|
||||
|
||||
match self.fail_admin_payment_order(order_id).await? {
|
||||
Some(aether_data::repository::wallet::WalletMutationOutcome::Applied(order)) => Ok(
|
||||
AdminWalletMutationOutcome::Applied(stored_admin_payment_order_to_gateway(order)),
|
||||
),
|
||||
Some(aether_data::repository::wallet::WalletMutationOutcome::NotFound) => {
|
||||
Ok(AdminWalletMutationOutcome::NotFound)
|
||||
}
|
||||
Some(aether_data::repository::wallet::WalletMutationOutcome::Invalid(detail)) => {
|
||||
Ok(AdminWalletMutationOutcome::Invalid(detail))
|
||||
}
|
||||
None => Ok(AdminWalletMutationOutcome::Unavailable),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) async fn admin_credit_payment_order(
|
||||
&self,
|
||||
order_id: &str,
|
||||
gateway_order_id: Option<&str>,
|
||||
pay_amount: Option<f64>,
|
||||
pay_currency: Option<&str>,
|
||||
exchange_rate: Option<f64>,
|
||||
gateway_response_patch: Option<serde_json::Value>,
|
||||
operator_id: Option<&str>,
|
||||
) -> Result<AdminWalletMutationOutcome<(AdminWalletPaymentOrderRecord, bool)>, GatewayError>
|
||||
{
|
||||
#[cfg(test)]
|
||||
if let (Some(order_store), Some(wallet_store)) = (
|
||||
self.admin_wallet_payment_order_store.as_ref(),
|
||||
self.auth_wallet_store.as_ref(),
|
||||
) {
|
||||
let mut orders = order_store
|
||||
.lock()
|
||||
.expect("admin wallet payment order store should lock");
|
||||
let Some(order) = orders.get_mut(order_id) else {
|
||||
return Ok(AdminWalletMutationOutcome::NotFound);
|
||||
};
|
||||
if order.status == "credited" {
|
||||
return Ok(AdminWalletMutationOutcome::Applied((order.clone(), false)));
|
||||
}
|
||||
if matches!(order.status.as_str(), "failed" | "expired" | "refunded") {
|
||||
return Ok(AdminWalletMutationOutcome::Invalid(format!(
|
||||
"payment order is not creditable: {}",
|
||||
order.status
|
||||
)));
|
||||
}
|
||||
if order
|
||||
.expires_at_unix_secs
|
||||
.is_some_and(|value| value < chrono::Utc::now().timestamp().max(0) as u64)
|
||||
{
|
||||
return Ok(AdminWalletMutationOutcome::Invalid(
|
||||
"payment order expired".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut wallets = wallet_store.lock().expect("auth wallet store should lock");
|
||||
let Some(wallet) = wallets.get_mut(&order.wallet_id) else {
|
||||
return Ok(AdminWalletMutationOutcome::Invalid(
|
||||
"wallet not found".to_string(),
|
||||
));
|
||||
};
|
||||
if wallet.status != "active" {
|
||||
return Ok(AdminWalletMutationOutcome::Invalid(
|
||||
"wallet is not active".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let now_unix_secs = chrono::Utc::now().timestamp().max(0) as u64;
|
||||
let mut gateway_response =
|
||||
admin_payment_gateway_response_map(order.gateway_response.take());
|
||||
if let Some(serde_json::Value::Object(map)) = gateway_response_patch {
|
||||
gateway_response.extend(map);
|
||||
}
|
||||
gateway_response.insert("manual_credit".to_string(), serde_json::Value::Bool(true));
|
||||
gateway_response.insert(
|
||||
"credited_by".to_string(),
|
||||
operator_id
|
||||
.map(|value| serde_json::Value::String(value.to_string()))
|
||||
.unwrap_or(serde_json::Value::Null),
|
||||
);
|
||||
|
||||
wallet.balance += order.amount_usd;
|
||||
wallet.total_recharged += order.amount_usd;
|
||||
wallet.updated_at_unix_secs = now_unix_secs;
|
||||
|
||||
if let Some(value) = gateway_order_id {
|
||||
order.gateway_order_id = Some(value.to_string());
|
||||
}
|
||||
if let Some(value) = pay_amount {
|
||||
order.pay_amount = Some(value);
|
||||
}
|
||||
if let Some(value) = pay_currency {
|
||||
order.pay_currency = Some(value.to_string());
|
||||
}
|
||||
if let Some(value) = exchange_rate {
|
||||
order.exchange_rate = Some(value);
|
||||
}
|
||||
order.status = "credited".to_string();
|
||||
order.paid_at_unix_secs = order.paid_at_unix_secs.or(Some(now_unix_secs));
|
||||
order.credited_at_unix_secs = Some(now_unix_secs);
|
||||
order.refundable_amount_usd = order.amount_usd;
|
||||
order.gateway_response = Some(serde_json::Value::Object(gateway_response));
|
||||
return Ok(AdminWalletMutationOutcome::Applied((order.clone(), true)));
|
||||
}
|
||||
|
||||
match self
|
||||
.credit_admin_payment_order(
|
||||
aether_data::repository::wallet::CreditAdminPaymentOrderInput {
|
||||
order_id: order_id.to_string(),
|
||||
gateway_order_id: gateway_order_id.map(ToOwned::to_owned),
|
||||
pay_amount,
|
||||
pay_currency: pay_currency.map(ToOwned::to_owned),
|
||||
exchange_rate,
|
||||
gateway_response_patch,
|
||||
operator_id: operator_id.map(ToOwned::to_owned),
|
||||
},
|
||||
)
|
||||
.await?
|
||||
{
|
||||
Some(aether_data::repository::wallet::WalletMutationOutcome::Applied((
|
||||
order,
|
||||
changed,
|
||||
))) => Ok(AdminWalletMutationOutcome::Applied((
|
||||
stored_admin_payment_order_to_gateway(order),
|
||||
changed,
|
||||
))),
|
||||
Some(aether_data::repository::wallet::WalletMutationOutcome::NotFound) => {
|
||||
Ok(AdminWalletMutationOutcome::NotFound)
|
||||
}
|
||||
Some(aether_data::repository::wallet::WalletMutationOutcome::Invalid(detail)) => {
|
||||
Ok(AdminWalletMutationOutcome::Invalid(detail))
|
||||
}
|
||||
None => Ok(AdminWalletMutationOutcome::Unavailable),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn stored_admin_payment_order_to_gateway(
|
||||
order: aether_data::repository::wallet::StoredAdminPaymentOrder,
|
||||
) -> AdminWalletPaymentOrderRecord {
|
||||
AdminWalletPaymentOrderRecord {
|
||||
id: order.id,
|
||||
order_no: order.order_no,
|
||||
wallet_id: order.wallet_id,
|
||||
user_id: order.user_id,
|
||||
amount_usd: order.amount_usd,
|
||||
pay_amount: order.pay_amount,
|
||||
pay_currency: order.pay_currency,
|
||||
exchange_rate: order.exchange_rate,
|
||||
refunded_amount_usd: order.refunded_amount_usd,
|
||||
refundable_amount_usd: order.refundable_amount_usd,
|
||||
payment_method: order.payment_method,
|
||||
gateway_order_id: order.gateway_order_id,
|
||||
status: order.status,
|
||||
gateway_response: order.gateway_response,
|
||||
created_at_unix_secs: order.created_at_unix_secs,
|
||||
paid_at_unix_secs: order.paid_at_unix_secs,
|
||||
credited_at_unix_secs: order.credited_at_unix_secs,
|
||||
expires_at_unix_secs: order.expires_at_unix_secs,
|
||||
}
|
||||
}
|
||||
326
apps/aether-gateway/src/state/runtime/security/blacklist.rs
Normal file
326
apps/aether-gateway/src/state/runtime/security/blacklist.rs
Normal file
@@ -0,0 +1,326 @@
|
||||
use crate::state::AdminSecurityBlacklistEntry;
|
||||
use crate::{AppState, GatewayError};
|
||||
|
||||
impl AppState {
|
||||
pub(crate) async fn add_admin_security_blacklist(
|
||||
&self,
|
||||
ip_address: &str,
|
||||
reason: &str,
|
||||
ttl_seconds: Option<u64>,
|
||||
) -> Result<bool, GatewayError> {
|
||||
const ADMIN_SECURITY_BLACKLIST_PREFIX: &str = "ip:blacklist:";
|
||||
|
||||
if let Some(runner) = self.redis_kv_runner() {
|
||||
let mut connection = match runner.client().get_multiplexed_async_connection().await {
|
||||
Ok(value) => value,
|
||||
Err(_) => return Ok(false),
|
||||
};
|
||||
let key = runner
|
||||
.keyspace()
|
||||
.key(&format!("{ADMIN_SECURITY_BLACKLIST_PREFIX}{ip_address}"));
|
||||
let result = if let Some(ttl_seconds) = ttl_seconds {
|
||||
redis::cmd("SETEX")
|
||||
.arg(&key)
|
||||
.arg(ttl_seconds)
|
||||
.arg(reason)
|
||||
.query_async::<String>(&mut connection)
|
||||
.await
|
||||
} else {
|
||||
redis::cmd("SET")
|
||||
.arg(&key)
|
||||
.arg(reason)
|
||||
.query_async::<String>(&mut connection)
|
||||
.await
|
||||
};
|
||||
return Ok(result.is_ok());
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
if let Some(store) = self.admin_security_blacklist_store.as_ref() {
|
||||
store
|
||||
.lock()
|
||||
.expect("admin security blacklist store should lock")
|
||||
.insert(ip_address.to_string(), reason.to_string());
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
pub(crate) async fn remove_admin_security_blacklist(
|
||||
&self,
|
||||
ip_address: &str,
|
||||
) -> Result<bool, GatewayError> {
|
||||
const ADMIN_SECURITY_BLACKLIST_PREFIX: &str = "ip:blacklist:";
|
||||
|
||||
if let Some(runner) = self.redis_kv_runner() {
|
||||
let mut connection = match runner.client().get_multiplexed_async_connection().await {
|
||||
Ok(value) => value,
|
||||
Err(_) => return Ok(false),
|
||||
};
|
||||
let key = runner
|
||||
.keyspace()
|
||||
.key(&format!("{ADMIN_SECURITY_BLACKLIST_PREFIX}{ip_address}"));
|
||||
let deleted = match redis::cmd("DEL")
|
||||
.arg(&key)
|
||||
.query_async::<i64>(&mut connection)
|
||||
.await
|
||||
{
|
||||
Ok(value) => value,
|
||||
Err(_) => return Ok(false),
|
||||
};
|
||||
return Ok(deleted > 0);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
if let Some(store) = self.admin_security_blacklist_store.as_ref() {
|
||||
let removed = store
|
||||
.lock()
|
||||
.expect("admin security blacklist store should lock")
|
||||
.remove(ip_address)
|
||||
.is_some();
|
||||
return Ok(removed);
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
pub(crate) async fn admin_security_blacklist_stats(
|
||||
&self,
|
||||
) -> Result<(bool, usize, Option<String>), GatewayError> {
|
||||
const ADMIN_SECURITY_BLACKLIST_PREFIX: &str = "ip:blacklist:";
|
||||
|
||||
if let Some(runner) = self.redis_kv_runner() {
|
||||
let mut connection = match runner.client().get_multiplexed_async_connection().await {
|
||||
Ok(value) => value,
|
||||
Err(_) => return Ok((false, 0, Some("Redis 不可用".to_string()))),
|
||||
};
|
||||
let pattern = runner
|
||||
.keyspace()
|
||||
.key(&format!("{ADMIN_SECURITY_BLACKLIST_PREFIX}*"));
|
||||
let mut cursor = 0u64;
|
||||
let mut total = 0usize;
|
||||
loop {
|
||||
let (next_cursor, keys) = match redis::cmd("SCAN")
|
||||
.arg(cursor)
|
||||
.arg("MATCH")
|
||||
.arg(&pattern)
|
||||
.arg("COUNT")
|
||||
.arg(100)
|
||||
.query_async::<(u64, Vec<String>)>(&mut connection)
|
||||
.await
|
||||
{
|
||||
Ok(value) => value,
|
||||
Err(err) => return Ok((false, 0, Some(err.to_string()))),
|
||||
};
|
||||
total += keys.len();
|
||||
if next_cursor == 0 {
|
||||
break;
|
||||
}
|
||||
cursor = next_cursor;
|
||||
}
|
||||
return Ok((true, total, None));
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
if let Some(store) = self.admin_security_blacklist_store.as_ref() {
|
||||
let total = store
|
||||
.lock()
|
||||
.expect("admin security blacklist store should lock")
|
||||
.len();
|
||||
return Ok((true, total, None));
|
||||
}
|
||||
|
||||
Ok((false, 0, Some("Redis 不可用".to_string())))
|
||||
}
|
||||
|
||||
pub(crate) async fn list_admin_security_blacklist(
|
||||
&self,
|
||||
) -> Result<Vec<AdminSecurityBlacklistEntry>, GatewayError> {
|
||||
const ADMIN_SECURITY_BLACKLIST_PREFIX: &str = "ip:blacklist:";
|
||||
|
||||
if let Some(runner) = self.redis_kv_runner() {
|
||||
let mut connection = match runner.client().get_multiplexed_async_connection().await {
|
||||
Ok(value) => value,
|
||||
Err(_) => return Ok(Vec::new()),
|
||||
};
|
||||
let pattern = runner
|
||||
.keyspace()
|
||||
.key(&format!("{ADMIN_SECURITY_BLACKLIST_PREFIX}*"));
|
||||
let prefix = runner.keyspace().key(ADMIN_SECURITY_BLACKLIST_PREFIX);
|
||||
let mut cursor = 0u64;
|
||||
let mut entries = Vec::new();
|
||||
loop {
|
||||
let (next_cursor, keys) = match redis::cmd("SCAN")
|
||||
.arg(cursor)
|
||||
.arg("MATCH")
|
||||
.arg(&pattern)
|
||||
.arg("COUNT")
|
||||
.arg(100)
|
||||
.query_async::<(u64, Vec<String>)>(&mut connection)
|
||||
.await
|
||||
{
|
||||
Ok(value) => value,
|
||||
Err(_) => break,
|
||||
};
|
||||
for full_key in keys {
|
||||
let ip_address = full_key
|
||||
.strip_prefix(prefix.as_str())
|
||||
.map(|value| value.to_string())
|
||||
.unwrap_or_else(|| full_key.clone());
|
||||
let reason: Result<String, _> = redis::cmd("GET")
|
||||
.arg(&full_key)
|
||||
.query_async(&mut connection)
|
||||
.await;
|
||||
let reason = match reason {
|
||||
Ok(value) => value,
|
||||
Err(_) => continue,
|
||||
};
|
||||
let ttl = match redis::cmd("TTL")
|
||||
.arg(&full_key)
|
||||
.query_async::<i64>(&mut connection)
|
||||
.await
|
||||
{
|
||||
Ok(value) if value >= 0 => Some(value),
|
||||
_ => None,
|
||||
};
|
||||
entries.push(AdminSecurityBlacklistEntry {
|
||||
ip_address,
|
||||
reason,
|
||||
ttl_seconds: ttl,
|
||||
});
|
||||
}
|
||||
if next_cursor == 0 {
|
||||
break;
|
||||
}
|
||||
cursor = next_cursor;
|
||||
}
|
||||
entries.sort_by(|a, b| a.ip_address.cmp(&b.ip_address));
|
||||
return Ok(entries);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
if let Some(store) = self.admin_security_blacklist_store.as_ref() {
|
||||
let mut entries = store
|
||||
.lock()
|
||||
.expect("admin security blacklist store should lock")
|
||||
.iter()
|
||||
.map(|(ip, reason)| AdminSecurityBlacklistEntry {
|
||||
ip_address: ip.clone(),
|
||||
reason: reason.clone(),
|
||||
ttl_seconds: None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
entries.sort_by(|a, b| a.ip_address.cmp(&b.ip_address));
|
||||
return Ok(entries);
|
||||
}
|
||||
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
pub(crate) async fn add_admin_security_whitelist(
|
||||
&self,
|
||||
ip_address: &str,
|
||||
) -> Result<bool, GatewayError> {
|
||||
const ADMIN_SECURITY_WHITELIST_KEY: &str = "ip:whitelist";
|
||||
|
||||
if let Some(runner) = self.redis_kv_runner() {
|
||||
let mut connection = match runner.client().get_multiplexed_async_connection().await {
|
||||
Ok(value) => value,
|
||||
Err(_) => return Ok(false),
|
||||
};
|
||||
let key = runner.keyspace().key(ADMIN_SECURITY_WHITELIST_KEY);
|
||||
let added = match redis::cmd("SADD")
|
||||
.arg(&key)
|
||||
.arg(ip_address)
|
||||
.query_async::<i64>(&mut connection)
|
||||
.await
|
||||
{
|
||||
Ok(value) => value,
|
||||
Err(_) => return Ok(false),
|
||||
};
|
||||
return Ok(added >= 0);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
if let Some(store) = self.admin_security_whitelist_store.as_ref() {
|
||||
store
|
||||
.lock()
|
||||
.expect("admin security whitelist store should lock")
|
||||
.insert(ip_address.to_string());
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
pub(crate) async fn remove_admin_security_whitelist(
|
||||
&self,
|
||||
ip_address: &str,
|
||||
) -> Result<bool, GatewayError> {
|
||||
const ADMIN_SECURITY_WHITELIST_KEY: &str = "ip:whitelist";
|
||||
|
||||
if let Some(runner) = self.redis_kv_runner() {
|
||||
let mut connection = match runner.client().get_multiplexed_async_connection().await {
|
||||
Ok(value) => value,
|
||||
Err(_) => return Ok(false),
|
||||
};
|
||||
let key = runner.keyspace().key(ADMIN_SECURITY_WHITELIST_KEY);
|
||||
let removed = match redis::cmd("SREM")
|
||||
.arg(&key)
|
||||
.arg(ip_address)
|
||||
.query_async::<i64>(&mut connection)
|
||||
.await
|
||||
{
|
||||
Ok(value) => value,
|
||||
Err(_) => return Ok(false),
|
||||
};
|
||||
return Ok(removed > 0);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
if let Some(store) = self.admin_security_whitelist_store.as_ref() {
|
||||
let removed = store
|
||||
.lock()
|
||||
.expect("admin security whitelist store should lock")
|
||||
.remove(ip_address);
|
||||
return Ok(removed);
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
pub(crate) async fn list_admin_security_whitelist(&self) -> Result<Vec<String>, GatewayError> {
|
||||
const ADMIN_SECURITY_WHITELIST_KEY: &str = "ip:whitelist";
|
||||
|
||||
if let Some(runner) = self.redis_kv_runner() {
|
||||
let mut connection = match runner.client().get_multiplexed_async_connection().await {
|
||||
Ok(value) => value,
|
||||
Err(_) => return Ok(Vec::new()),
|
||||
};
|
||||
let key = runner.keyspace().key(ADMIN_SECURITY_WHITELIST_KEY);
|
||||
let mut whitelist = match redis::cmd("SMEMBERS")
|
||||
.arg(&key)
|
||||
.query_async::<Vec<String>>(&mut connection)
|
||||
.await
|
||||
{
|
||||
Ok(value) => value,
|
||||
Err(_) => return Ok(Vec::new()),
|
||||
};
|
||||
whitelist.sort();
|
||||
return Ok(whitelist);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
if let Some(store) = self.admin_security_whitelist_store.as_ref() {
|
||||
return Ok(store
|
||||
.lock()
|
||||
.expect("admin security whitelist store should lock")
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect());
|
||||
}
|
||||
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
1
apps/aether-gateway/src/state/runtime/security/mod.rs
Normal file
1
apps/aether-gateway/src/state/runtime/security/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
mod blacklist;
|
||||
25
apps/aether-gateway/src/state/runtime/shadow_results.rs
Normal file
25
apps/aether-gateway/src/state/runtime/shadow_results.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
use crate::{AppState, GatewayError};
|
||||
|
||||
impl AppState {
|
||||
pub(crate) async fn record_shadow_result_sample(
|
||||
&self,
|
||||
sample: aether_data::repository::shadow_results::RecordShadowResultSample,
|
||||
) -> Result<Option<aether_data::repository::shadow_results::StoredShadowResult>, GatewayError>
|
||||
{
|
||||
self.data
|
||||
.record_shadow_result_sample(sample)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn list_recent_shadow_results(
|
||||
&self,
|
||||
limit: usize,
|
||||
) -> Result<Vec<aether_data::repository::shadow_results::StoredShadowResult>, GatewayError>
|
||||
{
|
||||
self.data
|
||||
.list_recent_shadow_results(limit)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
}
|
||||
78
apps/aether-gateway/src/state/runtime/usage_queries.rs
Normal file
78
apps/aether-gateway/src/state/runtime/usage_queries.rs
Normal file
@@ -0,0 +1,78 @@
|
||||
use crate::{AppState, GatewayError};
|
||||
|
||||
impl AppState {
|
||||
pub(crate) async fn read_request_candidates_by_request_id(
|
||||
&self,
|
||||
request_id: &str,
|
||||
) -> Result<Vec<aether_data::repository::candidates::StoredRequestCandidate>, GatewayError>
|
||||
{
|
||||
self.data
|
||||
.list_request_candidates_by_request_id(request_id)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn read_request_candidates_by_provider_id(
|
||||
&self,
|
||||
provider_id: &str,
|
||||
limit: usize,
|
||||
) -> Result<Vec<aether_data::repository::candidates::StoredRequestCandidate>, GatewayError>
|
||||
{
|
||||
self.data
|
||||
.list_request_candidates_by_provider_id(provider_id, limit)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn summarize_provider_usage_since(
|
||||
&self,
|
||||
provider_id: &str,
|
||||
since_unix_secs: u64,
|
||||
) -> Result<aether_data::repository::usage::StoredProviderUsageSummary, GatewayError> {
|
||||
self.data
|
||||
.summarize_provider_usage_since(provider_id, since_unix_secs)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn list_usage_audits(
|
||||
&self,
|
||||
query: &aether_data::repository::usage::UsageAuditListQuery,
|
||||
) -> Result<Vec<aether_data::repository::usage::StoredRequestUsageAudit>, GatewayError> {
|
||||
self.data
|
||||
.list_usage_audits(query)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn list_recent_usage_audits(
|
||||
&self,
|
||||
user_id: Option<&str>,
|
||||
limit: usize,
|
||||
) -> Result<Vec<aether_data::repository::usage::StoredRequestUsageAudit>, GatewayError> {
|
||||
self.data
|
||||
.list_recent_usage_audits(user_id, limit)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn summarize_usage_total_tokens_by_api_key_ids(
|
||||
&self,
|
||||
api_key_ids: &[String],
|
||||
) -> Result<std::collections::BTreeMap<String, u64>, GatewayError> {
|
||||
self.data
|
||||
.summarize_usage_total_tokens_by_api_key_ids(api_key_ids)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn list_users_by_ids(
|
||||
&self,
|
||||
user_ids: &[String],
|
||||
) -> Result<Vec<aether_data::repository::users::StoredUserSummary>, GatewayError> {
|
||||
self.data
|
||||
.list_users_by_ids(user_ids)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
}
|
||||
25
apps/aether-gateway/src/state/runtime/user_preferences.rs
Normal file
25
apps/aether-gateway/src/state/runtime/user_preferences.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
use crate::{AppState, GatewayError};
|
||||
|
||||
impl AppState {
|
||||
pub(crate) async fn read_user_preferences(
|
||||
&self,
|
||||
user_id: &str,
|
||||
) -> Result<Option<crate::data::state::StoredUserPreferenceRecord>, GatewayError>
|
||||
{
|
||||
self.data
|
||||
.read_user_preferences(user_id)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn write_user_preferences(
|
||||
&self,
|
||||
preferences: &crate::data::state::StoredUserPreferenceRecord,
|
||||
) -> Result<Option<crate::data::state::StoredUserPreferenceRecord>, GatewayError>
|
||||
{
|
||||
self.data
|
||||
.write_user_preferences(preferences)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
use crate::{
|
||||
AdminWalletPaymentOrderRecord, AdminWalletTransactionRecord, AppState, GatewayError,
|
||||
};
|
||||
|
||||
use super::admin_wallet_build_order_no;
|
||||
|
||||
impl AppState {
|
||||
pub(crate) async fn admin_adjust_wallet_balance(
|
||||
&self,
|
||||
wallet_id: &str,
|
||||
amount_usd: f64,
|
||||
balance_type: &str,
|
||||
operator_id: Option<&str>,
|
||||
description: Option<&str>,
|
||||
) -> Result<
|
||||
Option<(
|
||||
aether_data::repository::wallet::StoredWalletSnapshot,
|
||||
AdminWalletTransactionRecord,
|
||||
)>,
|
||||
GatewayError,
|
||||
> {
|
||||
#[cfg(test)]
|
||||
if let Some(store) = self.auth_wallet_store.as_ref() {
|
||||
let mut guard = store.lock().expect("auth wallet store should lock");
|
||||
let Some(wallet) = guard.get_mut(wallet_id) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let before_recharge = wallet.balance;
|
||||
let before_gift = wallet.gift_balance;
|
||||
let before_total = before_recharge + before_gift;
|
||||
let mut after_recharge = before_recharge;
|
||||
let mut after_gift = before_gift;
|
||||
|
||||
if amount_usd > 0.0 {
|
||||
if balance_type.eq_ignore_ascii_case("gift") {
|
||||
after_gift += amount_usd;
|
||||
} else {
|
||||
after_recharge += amount_usd;
|
||||
}
|
||||
} else {
|
||||
let mut remaining = -amount_usd;
|
||||
let consume_positive_bucket = |balance: &mut f64, to_consume: &mut f64| {
|
||||
if *to_consume <= 0.0 {
|
||||
return;
|
||||
}
|
||||
let available = (*balance).max(0.0);
|
||||
let consumed = available.min(*to_consume);
|
||||
*balance -= consumed;
|
||||
*to_consume -= consumed;
|
||||
};
|
||||
if balance_type.eq_ignore_ascii_case("gift") {
|
||||
consume_positive_bucket(&mut after_gift, &mut remaining);
|
||||
consume_positive_bucket(&mut after_recharge, &mut remaining);
|
||||
} else {
|
||||
consume_positive_bucket(&mut after_recharge, &mut remaining);
|
||||
consume_positive_bucket(&mut after_gift, &mut remaining);
|
||||
}
|
||||
if remaining > 0.0 {
|
||||
after_recharge -= remaining;
|
||||
}
|
||||
}
|
||||
|
||||
wallet.balance = after_recharge;
|
||||
wallet.gift_balance = after_gift;
|
||||
wallet.total_adjusted += amount_usd;
|
||||
wallet.updated_at_unix_secs = chrono::Utc::now().timestamp().max(0) as u64;
|
||||
|
||||
let transaction = AdminWalletTransactionRecord {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
wallet_id: wallet.id.clone(),
|
||||
category: "adjust".to_string(),
|
||||
reason_code: "adjust_admin".to_string(),
|
||||
amount: amount_usd,
|
||||
balance_before: before_total,
|
||||
balance_after: after_recharge + after_gift,
|
||||
recharge_balance_before: before_recharge,
|
||||
recharge_balance_after: after_recharge,
|
||||
gift_balance_before: before_gift,
|
||||
gift_balance_after: after_gift,
|
||||
link_type: Some("admin_action".to_string()),
|
||||
link_id: Some(wallet.id.clone()),
|
||||
operator_id: operator_id.map(ToOwned::to_owned),
|
||||
description: Some(
|
||||
description
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.unwrap_or("管理员调账")
|
||||
.to_string(),
|
||||
),
|
||||
created_at_unix_secs: chrono::Utc::now().timestamp().max(0) as u64,
|
||||
};
|
||||
return Ok(Some((wallet.clone(), transaction)));
|
||||
}
|
||||
|
||||
Ok(self
|
||||
.adjust_wallet_balance(aether_data::repository::wallet::AdjustWalletBalanceInput {
|
||||
wallet_id: wallet_id.to_string(),
|
||||
amount_usd,
|
||||
balance_type: balance_type.to_string(),
|
||||
operator_id: operator_id.map(ToOwned::to_owned),
|
||||
description: description.map(ToOwned::to_owned),
|
||||
})
|
||||
.await?
|
||||
.map(|(wallet, transaction)| {
|
||||
(wallet, stored_wallet_transaction_to_gateway(transaction))
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) async fn admin_create_manual_wallet_recharge(
|
||||
&self,
|
||||
wallet_id: &str,
|
||||
amount_usd: f64,
|
||||
payment_method: &str,
|
||||
operator_id: Option<&str>,
|
||||
description: Option<&str>,
|
||||
) -> Result<
|
||||
Option<(
|
||||
aether_data::repository::wallet::StoredWalletSnapshot,
|
||||
AdminWalletPaymentOrderRecord,
|
||||
)>,
|
||||
GatewayError,
|
||||
> {
|
||||
#[cfg(test)]
|
||||
if let Some(store) = self.auth_wallet_store.as_ref() {
|
||||
let mut guard = store.lock().expect("auth wallet store should lock");
|
||||
let Some(wallet) = guard.get_mut(wallet_id) else {
|
||||
return Ok(None);
|
||||
};
|
||||
wallet.balance += amount_usd;
|
||||
wallet.total_recharged += amount_usd;
|
||||
wallet.updated_at_unix_secs = chrono::Utc::now().timestamp().max(0) as u64;
|
||||
let now = chrono::Utc::now();
|
||||
let created_at = now.timestamp().max(0) as u64;
|
||||
let order = AdminWalletPaymentOrderRecord {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
order_no: admin_wallet_build_order_no(now),
|
||||
wallet_id: wallet.id.clone(),
|
||||
user_id: wallet.user_id.clone(),
|
||||
amount_usd,
|
||||
pay_amount: None,
|
||||
pay_currency: None,
|
||||
exchange_rate: None,
|
||||
refunded_amount_usd: 0.0,
|
||||
refundable_amount_usd: amount_usd,
|
||||
payment_method: payment_method.to_string(),
|
||||
gateway_order_id: None,
|
||||
status: "credited".to_string(),
|
||||
gateway_response: Some(serde_json::json!({
|
||||
"source": "manual",
|
||||
"operator_id": operator_id,
|
||||
"description": description,
|
||||
})),
|
||||
created_at_unix_secs: created_at,
|
||||
paid_at_unix_secs: Some(created_at),
|
||||
credited_at_unix_secs: Some(created_at),
|
||||
expires_at_unix_secs: None,
|
||||
};
|
||||
return Ok(Some((wallet.clone(), order)));
|
||||
}
|
||||
|
||||
let now = chrono::Utc::now();
|
||||
let order_no = admin_wallet_build_order_no(now);
|
||||
Ok(self
|
||||
.create_manual_wallet_recharge(
|
||||
aether_data::repository::wallet::CreateManualWalletRechargeInput {
|
||||
wallet_id: wallet_id.to_string(),
|
||||
amount_usd,
|
||||
payment_method: payment_method.to_string(),
|
||||
operator_id: operator_id.map(ToOwned::to_owned),
|
||||
description: description.map(ToOwned::to_owned),
|
||||
order_no,
|
||||
},
|
||||
)
|
||||
.await?
|
||||
.map(|(wallet, order)| (wallet, stored_admin_payment_order_to_gateway(order))))
|
||||
}
|
||||
}
|
||||
|
||||
fn stored_wallet_transaction_to_gateway(
|
||||
transaction: aether_data::repository::wallet::StoredAdminWalletTransaction,
|
||||
) -> AdminWalletTransactionRecord {
|
||||
AdminWalletTransactionRecord {
|
||||
id: transaction.id,
|
||||
wallet_id: transaction.wallet_id,
|
||||
category: transaction.category,
|
||||
reason_code: transaction.reason_code,
|
||||
amount: transaction.amount,
|
||||
balance_before: transaction.balance_before,
|
||||
balance_after: transaction.balance_after,
|
||||
recharge_balance_before: transaction.recharge_balance_before,
|
||||
recharge_balance_after: transaction.recharge_balance_after,
|
||||
gift_balance_before: transaction.gift_balance_before,
|
||||
gift_balance_after: transaction.gift_balance_after,
|
||||
link_type: transaction.link_type,
|
||||
link_id: transaction.link_id,
|
||||
operator_id: transaction.operator_id,
|
||||
description: transaction.description,
|
||||
created_at_unix_secs: transaction.created_at_unix_secs.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn stored_admin_payment_order_to_gateway(
|
||||
order: aether_data::repository::wallet::StoredAdminPaymentOrder,
|
||||
) -> AdminWalletPaymentOrderRecord {
|
||||
AdminWalletPaymentOrderRecord {
|
||||
id: order.id,
|
||||
order_no: order.order_no,
|
||||
wallet_id: order.wallet_id,
|
||||
user_id: order.user_id,
|
||||
amount_usd: order.amount_usd,
|
||||
pay_amount: order.pay_amount,
|
||||
pay_currency: order.pay_currency,
|
||||
exchange_rate: order.exchange_rate,
|
||||
refunded_amount_usd: order.refunded_amount_usd,
|
||||
refundable_amount_usd: order.refundable_amount_usd,
|
||||
payment_method: order.payment_method,
|
||||
gateway_order_id: order.gateway_order_id,
|
||||
status: order.status,
|
||||
gateway_response: order.gateway_response,
|
||||
created_at_unix_secs: order.created_at_unix_secs,
|
||||
paid_at_unix_secs: order.paid_at_unix_secs,
|
||||
credited_at_unix_secs: order.credited_at_unix_secs,
|
||||
expires_at_unix_secs: order.expires_at_unix_secs,
|
||||
}
|
||||
}
|
||||
294
apps/aether-gateway/src/state/runtime/wallet/billing.rs
Normal file
294
apps/aether-gateway/src/state/runtime/wallet/billing.rs
Normal file
@@ -0,0 +1,294 @@
|
||||
use sqlx::Row;
|
||||
|
||||
use super::{
|
||||
AdminBillingCollectorRecord, AdminBillingRuleRecord, AdminWalletPaymentOrderRecord,
|
||||
AdminWalletRefundRecord, GatewayError,
|
||||
};
|
||||
|
||||
pub(crate) fn admin_wallet_build_order_no(now: chrono::DateTime<chrono::Utc>) -> String {
|
||||
format!(
|
||||
"po_{}_{}",
|
||||
now.format("%Y%m%d%H%M%S%6f"),
|
||||
&uuid::Uuid::new_v4().simple().to_string()[..12]
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn admin_payment_gateway_response_map(
|
||||
value: Option<serde_json::Value>,
|
||||
) -> serde_json::Map<String, serde_json::Value> {
|
||||
match value {
|
||||
Some(serde_json::Value::Object(map)) => map,
|
||||
_ => serde_json::Map::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn admin_wallet_snapshot_from_row(
|
||||
row: &sqlx::postgres::PgRow,
|
||||
) -> Result<aether_data::repository::wallet::StoredWalletSnapshot, GatewayError> {
|
||||
aether_data::repository::wallet::StoredWalletSnapshot::new(
|
||||
row.try_get("id")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
row.try_get("user_id")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
row.try_get("api_key_id")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
row.try_get("balance")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
row.try_get("gift_balance")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
row.try_get("limit_mode")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
row.try_get("currency")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
row.try_get("status")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
row.try_get("total_recharged")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
row.try_get("total_consumed")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
row.try_get("total_refunded")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
row.try_get("total_adjusted")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
row.try_get("updated_at_unix_secs")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
)
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(super) fn admin_wallet_payment_order_from_row(
|
||||
row: &sqlx::postgres::PgRow,
|
||||
) -> Result<AdminWalletPaymentOrderRecord, GatewayError> {
|
||||
Ok(AdminWalletPaymentOrderRecord {
|
||||
id: row
|
||||
.try_get("id")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
order_no: row
|
||||
.try_get("order_no")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
wallet_id: row
|
||||
.try_get("wallet_id")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
user_id: row
|
||||
.try_get("user_id")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
amount_usd: row
|
||||
.try_get("amount_usd")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
pay_amount: row
|
||||
.try_get("pay_amount")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
pay_currency: row
|
||||
.try_get("pay_currency")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
exchange_rate: row
|
||||
.try_get("exchange_rate")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
refunded_amount_usd: row
|
||||
.try_get("refunded_amount_usd")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
refundable_amount_usd: row
|
||||
.try_get("refundable_amount_usd")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
payment_method: row
|
||||
.try_get("payment_method")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
gateway_order_id: row
|
||||
.try_get("gateway_order_id")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
status: row
|
||||
.try_get("status")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
gateway_response: row
|
||||
.try_get("gateway_response")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
created_at_unix_secs: row
|
||||
.try_get::<i64, _>("created_at_unix_secs")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?
|
||||
.max(0) as u64,
|
||||
paid_at_unix_secs: row
|
||||
.try_get::<Option<i64>, _>("paid_at_unix_secs")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?
|
||||
.map(|value| value.max(0) as u64),
|
||||
credited_at_unix_secs: row
|
||||
.try_get::<Option<i64>, _>("credited_at_unix_secs")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?
|
||||
.map(|value| value.max(0) as u64),
|
||||
expires_at_unix_secs: row
|
||||
.try_get::<Option<i64>, _>("expires_at_unix_secs")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?
|
||||
.map(|value| value.max(0) as u64),
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn admin_wallet_refund_from_row(
|
||||
row: &sqlx::postgres::PgRow,
|
||||
) -> Result<AdminWalletRefundRecord, GatewayError> {
|
||||
Ok(AdminWalletRefundRecord {
|
||||
id: row
|
||||
.try_get("id")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
refund_no: row
|
||||
.try_get("refund_no")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
wallet_id: row
|
||||
.try_get("wallet_id")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
user_id: row
|
||||
.try_get("user_id")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
payment_order_id: row
|
||||
.try_get("payment_order_id")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
source_type: row
|
||||
.try_get("source_type")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
source_id: row
|
||||
.try_get("source_id")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
refund_mode: row
|
||||
.try_get("refund_mode")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
amount_usd: row
|
||||
.try_get("amount_usd")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
status: row
|
||||
.try_get("status")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
reason: row
|
||||
.try_get("reason")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
failure_reason: row
|
||||
.try_get("failure_reason")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
gateway_refund_id: row
|
||||
.try_get("gateway_refund_id")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
payout_method: row
|
||||
.try_get("payout_method")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
payout_reference: row
|
||||
.try_get("payout_reference")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
payout_proof: row
|
||||
.try_get("payout_proof")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
requested_by: row
|
||||
.try_get("requested_by")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
approved_by: row
|
||||
.try_get("approved_by")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
processed_by: row
|
||||
.try_get("processed_by")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
created_at_unix_secs: row
|
||||
.try_get::<i64, _>("created_at_unix_secs")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?
|
||||
.max(0) as u64,
|
||||
updated_at_unix_secs: row
|
||||
.try_get::<i64, _>("updated_at_unix_secs")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?
|
||||
.max(0) as u64,
|
||||
processed_at_unix_secs: row
|
||||
.try_get::<Option<i64>, _>("processed_at_unix_secs")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?
|
||||
.map(|value| value.max(0) as u64),
|
||||
completed_at_unix_secs: row
|
||||
.try_get::<Option<i64>, _>("completed_at_unix_secs")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?
|
||||
.map(|value| value.max(0) as u64),
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn admin_billing_rule_from_row(
|
||||
row: &sqlx::postgres::PgRow,
|
||||
) -> Result<AdminBillingRuleRecord, GatewayError> {
|
||||
Ok(AdminBillingRuleRecord {
|
||||
id: row
|
||||
.try_get("id")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
name: row
|
||||
.try_get("name")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
task_type: row
|
||||
.try_get("task_type")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
global_model_id: row
|
||||
.try_get("global_model_id")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
model_id: row
|
||||
.try_get("model_id")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
expression: row
|
||||
.try_get("expression")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
variables: row
|
||||
.try_get::<Option<serde_json::Value>, _>("variables")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?
|
||||
.unwrap_or_else(|| serde_json::json!({})),
|
||||
dimension_mappings: row
|
||||
.try_get::<Option<serde_json::Value>, _>("dimension_mappings")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?
|
||||
.unwrap_or_else(|| serde_json::json!({})),
|
||||
is_enabled: row
|
||||
.try_get("is_enabled")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
created_at_unix_secs: row
|
||||
.try_get::<i64, _>("created_at_unix_secs")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?
|
||||
.max(0) as u64,
|
||||
updated_at_unix_secs: row
|
||||
.try_get::<i64, _>("updated_at_unix_secs")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?
|
||||
.max(0) as u64,
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn admin_billing_collector_from_row(
|
||||
row: &sqlx::postgres::PgRow,
|
||||
) -> Result<AdminBillingCollectorRecord, GatewayError> {
|
||||
Ok(AdminBillingCollectorRecord {
|
||||
id: row
|
||||
.try_get("id")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
api_format: row
|
||||
.try_get("api_format")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
task_type: row
|
||||
.try_get("task_type")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
dimension_name: row
|
||||
.try_get("dimension_name")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
source_type: row
|
||||
.try_get("source_type")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
source_path: row
|
||||
.try_get("source_path")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
value_type: row
|
||||
.try_get("value_type")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
transform_expression: row
|
||||
.try_get("transform_expression")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
default_value: row
|
||||
.try_get("default_value")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
priority: row
|
||||
.try_get("priority")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
is_enabled: row
|
||||
.try_get("is_enabled")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
created_at_unix_secs: row
|
||||
.try_get::<i64, _>("created_at_unix_secs")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?
|
||||
.max(0) as u64,
|
||||
updated_at_unix_secs: row
|
||||
.try_get::<i64, _>("updated_at_unix_secs")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?
|
||||
.max(0) as u64,
|
||||
})
|
||||
}
|
||||
13
apps/aether-gateway/src/state/runtime/wallet/mod.rs
Normal file
13
apps/aether-gateway/src/state/runtime/wallet/mod.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
use super::super::{
|
||||
AdminBillingCollectorRecord, AdminBillingRuleRecord, AdminWalletMutationOutcome,
|
||||
AdminWalletPaymentOrderRecord, AdminWalletRefundRecord, AdminWalletTransactionRecord, AppState,
|
||||
GatewayError,
|
||||
};
|
||||
|
||||
mod balance_mutations;
|
||||
mod billing;
|
||||
mod mutations;
|
||||
mod reads;
|
||||
mod refund_lifecycle;
|
||||
|
||||
pub(super) use self::billing::{admin_payment_gateway_response_map, admin_wallet_build_order_no};
|
||||
173
apps/aether-gateway/src/state/runtime/wallet/mutations.rs
Normal file
173
apps/aether-gateway/src/state/runtime/wallet/mutations.rs
Normal file
@@ -0,0 +1,173 @@
|
||||
use aether_data::repository::wallet::{
|
||||
AdjustWalletBalanceInput, CompleteAdminWalletRefundInput, CreateManualWalletRechargeInput,
|
||||
CreateWalletRechargeOrderInput, CreateWalletRechargeOrderOutcome,
|
||||
CreateWalletRefundRequestInput, CreateWalletRefundRequestOutcome, CreditAdminPaymentOrderInput,
|
||||
FailAdminWalletRefundInput, ProcessAdminWalletRefundInput, ProcessPaymentCallbackInput,
|
||||
ProcessPaymentCallbackOutcome, WalletMutationOutcome,
|
||||
};
|
||||
|
||||
use crate::{AppState, GatewayError};
|
||||
|
||||
impl AppState {
|
||||
pub(crate) async fn create_wallet_recharge_order(
|
||||
&self,
|
||||
input: CreateWalletRechargeOrderInput,
|
||||
) -> Result<Option<CreateWalletRechargeOrderOutcome>, GatewayError> {
|
||||
self.data
|
||||
.create_wallet_recharge_order(input)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn create_wallet_refund_request(
|
||||
&self,
|
||||
input: CreateWalletRefundRequestInput,
|
||||
) -> Result<Option<CreateWalletRefundRequestOutcome>, GatewayError> {
|
||||
self.data
|
||||
.create_wallet_refund_request(input)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn process_payment_callback(
|
||||
&self,
|
||||
input: ProcessPaymentCallbackInput,
|
||||
) -> Result<Option<ProcessPaymentCallbackOutcome>, GatewayError> {
|
||||
self.data
|
||||
.process_payment_callback(input)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn adjust_wallet_balance(
|
||||
&self,
|
||||
input: AdjustWalletBalanceInput,
|
||||
) -> Result<
|
||||
Option<(
|
||||
aether_data::repository::wallet::StoredWalletSnapshot,
|
||||
aether_data::repository::wallet::StoredAdminWalletTransaction,
|
||||
)>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.data
|
||||
.adjust_wallet_balance(input)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn create_manual_wallet_recharge(
|
||||
&self,
|
||||
input: CreateManualWalletRechargeInput,
|
||||
) -> Result<
|
||||
Option<(
|
||||
aether_data::repository::wallet::StoredWalletSnapshot,
|
||||
aether_data::repository::wallet::StoredAdminPaymentOrder,
|
||||
)>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.data
|
||||
.create_manual_wallet_recharge(input)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn process_admin_wallet_refund(
|
||||
&self,
|
||||
input: ProcessAdminWalletRefundInput,
|
||||
) -> Result<
|
||||
Option<
|
||||
WalletMutationOutcome<(
|
||||
aether_data::repository::wallet::StoredWalletSnapshot,
|
||||
aether_data::repository::wallet::StoredAdminWalletRefund,
|
||||
aether_data::repository::wallet::StoredAdminWalletTransaction,
|
||||
)>,
|
||||
>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.data
|
||||
.process_admin_wallet_refund(input)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn complete_admin_wallet_refund(
|
||||
&self,
|
||||
input: CompleteAdminWalletRefundInput,
|
||||
) -> Result<
|
||||
Option<WalletMutationOutcome<aether_data::repository::wallet::StoredAdminWalletRefund>>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.data
|
||||
.complete_admin_wallet_refund(input)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn fail_admin_wallet_refund(
|
||||
&self,
|
||||
input: FailAdminWalletRefundInput,
|
||||
) -> Result<
|
||||
Option<
|
||||
WalletMutationOutcome<(
|
||||
aether_data::repository::wallet::StoredWalletSnapshot,
|
||||
aether_data::repository::wallet::StoredAdminWalletRefund,
|
||||
Option<aether_data::repository::wallet::StoredAdminWalletTransaction>,
|
||||
)>,
|
||||
>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.data
|
||||
.fail_admin_wallet_refund(input)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn expire_admin_payment_order(
|
||||
&self,
|
||||
order_id: &str,
|
||||
) -> Result<
|
||||
Option<
|
||||
WalletMutationOutcome<(
|
||||
aether_data::repository::wallet::StoredAdminPaymentOrder,
|
||||
bool,
|
||||
)>,
|
||||
>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.data
|
||||
.expire_admin_payment_order(order_id)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn fail_admin_payment_order(
|
||||
&self,
|
||||
order_id: &str,
|
||||
) -> Result<
|
||||
Option<WalletMutationOutcome<aether_data::repository::wallet::StoredAdminPaymentOrder>>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.data
|
||||
.fail_admin_payment_order(order_id)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn credit_admin_payment_order(
|
||||
&self,
|
||||
input: CreditAdminPaymentOrderInput,
|
||||
) -> Result<
|
||||
Option<
|
||||
WalletMutationOutcome<(
|
||||
aether_data::repository::wallet::StoredAdminPaymentOrder,
|
||||
bool,
|
||||
)>,
|
||||
>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.data
|
||||
.credit_admin_payment_order(input)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
}
|
||||
152
apps/aether-gateway/src/state/runtime/wallet/reads.rs
Normal file
152
apps/aether-gateway/src/state/runtime/wallet/reads.rs
Normal file
@@ -0,0 +1,152 @@
|
||||
use crate::{AppState, GatewayError};
|
||||
|
||||
impl AppState {
|
||||
pub(crate) async fn find_wallet(
|
||||
&self,
|
||||
lookup: aether_data::repository::wallet::WalletLookupKey<'_>,
|
||||
) -> Result<Option<aether_data::repository::wallet::StoredWalletSnapshot>, GatewayError> {
|
||||
#[cfg(test)]
|
||||
if let Some(store) = self.auth_wallet_store.as_ref() {
|
||||
let wallet = {
|
||||
let wallets = store.lock().expect("auth wallet store should lock");
|
||||
match lookup {
|
||||
aether_data::repository::wallet::WalletLookupKey::WalletId(wallet_id) => {
|
||||
wallets.get(wallet_id).cloned()
|
||||
}
|
||||
aether_data::repository::wallet::WalletLookupKey::UserId(user_id) => wallets
|
||||
.values()
|
||||
.find(|wallet| wallet.user_id.as_deref() == Some(user_id))
|
||||
.cloned(),
|
||||
aether_data::repository::wallet::WalletLookupKey::ApiKeyId(api_key_id) => {
|
||||
wallets
|
||||
.values()
|
||||
.find(|wallet| wallet.api_key_id.as_deref() == Some(api_key_id))
|
||||
.cloned()
|
||||
}
|
||||
}
|
||||
};
|
||||
if wallet.is_some() {
|
||||
return Ok(wallet);
|
||||
}
|
||||
}
|
||||
|
||||
self.data
|
||||
.find_wallet(lookup)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn read_wallet_snapshot_for_auth(
|
||||
&self,
|
||||
user_id: &str,
|
||||
api_key_id: &str,
|
||||
api_key_is_standalone: bool,
|
||||
) -> Result<Option<aether_data::repository::wallet::StoredWalletSnapshot>, GatewayError> {
|
||||
let lookup = if api_key_is_standalone {
|
||||
if api_key_id.trim().is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(aether_data::repository::wallet::WalletLookupKey::ApiKeyId(
|
||||
api_key_id,
|
||||
))
|
||||
}
|
||||
} else if !user_id.trim().is_empty() {
|
||||
Some(aether_data::repository::wallet::WalletLookupKey::UserId(
|
||||
user_id,
|
||||
))
|
||||
} else if !api_key_id.trim().is_empty() {
|
||||
Some(aether_data::repository::wallet::WalletLookupKey::ApiKeyId(
|
||||
api_key_id,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let Some(lookup) = lookup else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
self.find_wallet(lookup).await
|
||||
}
|
||||
|
||||
pub(crate) async fn list_wallet_snapshots_by_user_ids(
|
||||
&self,
|
||||
user_ids: &[String],
|
||||
) -> Result<Vec<aether_data::repository::wallet::StoredWalletSnapshot>, GatewayError> {
|
||||
self.data
|
||||
.list_wallets_by_user_ids(user_ids)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn list_wallet_snapshots_by_api_key_ids(
|
||||
&self,
|
||||
api_key_ids: &[String],
|
||||
) -> Result<Vec<aether_data::repository::wallet::StoredWalletSnapshot>, GatewayError> {
|
||||
self.data
|
||||
.list_wallets_by_api_key_ids(api_key_ids)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn find_wallet_today_usage(
|
||||
&self,
|
||||
wallet_id: &str,
|
||||
billing_timezone: &str,
|
||||
) -> Result<Option<aether_data::repository::wallet::StoredWalletDailyUsageLedger>, GatewayError>
|
||||
{
|
||||
self.data
|
||||
.find_wallet_today_usage(wallet_id, billing_timezone)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn list_wallet_daily_usage_history(
|
||||
&self,
|
||||
wallet_id: &str,
|
||||
billing_timezone: &str,
|
||||
limit: usize,
|
||||
) -> Result<aether_data::repository::wallet::StoredWalletDailyUsageLedgerPage, GatewayError>
|
||||
{
|
||||
self.data
|
||||
.list_wallet_daily_usage_history(wallet_id, billing_timezone, limit)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn list_wallet_payment_orders_by_user_id(
|
||||
&self,
|
||||
user_id: &str,
|
||||
limit: usize,
|
||||
offset: usize,
|
||||
) -> Result<aether_data::repository::wallet::StoredAdminPaymentOrderPage, GatewayError> {
|
||||
self.data
|
||||
.list_wallet_payment_orders_by_user_id(user_id, limit, offset)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn find_wallet_payment_order_by_user_id(
|
||||
&self,
|
||||
user_id: &str,
|
||||
order_id: &str,
|
||||
) -> Result<Option<aether_data::repository::wallet::StoredAdminPaymentOrder>, GatewayError>
|
||||
{
|
||||
self.data
|
||||
.find_wallet_payment_order_by_user_id(user_id, order_id)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn find_wallet_refund(
|
||||
&self,
|
||||
wallet_id: &str,
|
||||
refund_id: &str,
|
||||
) -> Result<Option<aether_data::repository::wallet::StoredAdminWalletRefund>, GatewayError>
|
||||
{
|
||||
self.data
|
||||
.find_wallet_refund(wallet_id, refund_id)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
}
|
||||
446
apps/aether-gateway/src/state/runtime/wallet/refund_lifecycle.rs
Normal file
446
apps/aether-gateway/src/state/runtime/wallet/refund_lifecycle.rs
Normal file
@@ -0,0 +1,446 @@
|
||||
use super::{
|
||||
AdminWalletMutationOutcome, AdminWalletRefundRecord, AdminWalletTransactionRecord, AppState,
|
||||
GatewayError,
|
||||
};
|
||||
|
||||
impl AppState {
|
||||
pub(crate) async fn admin_process_wallet_refund(
|
||||
&self,
|
||||
wallet_id: &str,
|
||||
refund_id: &str,
|
||||
operator_id: Option<&str>,
|
||||
) -> Result<
|
||||
AdminWalletMutationOutcome<(
|
||||
aether_data::repository::wallet::StoredWalletSnapshot,
|
||||
AdminWalletRefundRecord,
|
||||
AdminWalletTransactionRecord,
|
||||
)>,
|
||||
GatewayError,
|
||||
> {
|
||||
#[cfg(test)]
|
||||
if let (Some(wallet_store), Some(refund_store)) = (
|
||||
self.auth_wallet_store.as_ref(),
|
||||
self.admin_wallet_refund_store.as_ref(),
|
||||
) {
|
||||
let Some(wallet) = wallet_store
|
||||
.lock()
|
||||
.expect("auth wallet store should lock")
|
||||
.get(wallet_id)
|
||||
.cloned()
|
||||
else {
|
||||
return Ok(AdminWalletMutationOutcome::NotFound);
|
||||
};
|
||||
let Some(refund) = refund_store
|
||||
.lock()
|
||||
.expect("admin wallet refund store should lock")
|
||||
.get(refund_id)
|
||||
.filter(|refund| refund.wallet_id == wallet_id)
|
||||
.cloned()
|
||||
else {
|
||||
return Ok(AdminWalletMutationOutcome::NotFound);
|
||||
};
|
||||
if !matches!(refund.status.as_str(), "approved" | "pending_approval") {
|
||||
return Ok(AdminWalletMutationOutcome::Invalid(
|
||||
"refund status is not approvable".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let amount_usd = refund.amount_usd;
|
||||
let mut updated_wallet = wallet.clone();
|
||||
let before_recharge = updated_wallet.balance;
|
||||
let before_gift = updated_wallet.gift_balance;
|
||||
let before_total = before_recharge + before_gift;
|
||||
let after_recharge = before_recharge - amount_usd;
|
||||
if after_recharge < 0.0 {
|
||||
return Ok(AdminWalletMutationOutcome::Invalid(
|
||||
"refund amount exceeds refundable recharge balance".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut updated_order = None;
|
||||
if let Some(payment_order_id) = refund.payment_order_id.clone() {
|
||||
let Some(order_store) = self.admin_wallet_payment_order_store.as_ref() else {
|
||||
return Ok(AdminWalletMutationOutcome::Unavailable);
|
||||
};
|
||||
let Some(order) = order_store
|
||||
.lock()
|
||||
.expect("admin wallet payment order store should lock")
|
||||
.get(&payment_order_id)
|
||||
.cloned()
|
||||
else {
|
||||
return Ok(AdminWalletMutationOutcome::Invalid(
|
||||
"payment order not found".to_string(),
|
||||
));
|
||||
};
|
||||
if amount_usd > order.refundable_amount_usd {
|
||||
return Ok(AdminWalletMutationOutcome::Invalid(
|
||||
"refund amount exceeds refundable amount".to_string(),
|
||||
));
|
||||
}
|
||||
let mut order = order;
|
||||
order.refunded_amount_usd += amount_usd;
|
||||
order.refundable_amount_usd -= amount_usd;
|
||||
updated_order = Some(order);
|
||||
}
|
||||
|
||||
let now_unix_secs = chrono::Utc::now().timestamp().max(0) as u64;
|
||||
updated_wallet.balance = after_recharge;
|
||||
updated_wallet.total_refunded = (updated_wallet.total_refunded + amount_usd).max(0.0);
|
||||
updated_wallet.updated_at_unix_secs = now_unix_secs;
|
||||
|
||||
let transaction = AdminWalletTransactionRecord {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
wallet_id: updated_wallet.id.clone(),
|
||||
category: "refund".to_string(),
|
||||
reason_code: "refund_out".to_string(),
|
||||
amount: -amount_usd,
|
||||
balance_before: before_total,
|
||||
balance_after: after_recharge + before_gift,
|
||||
recharge_balance_before: before_recharge,
|
||||
recharge_balance_after: after_recharge,
|
||||
gift_balance_before: before_gift,
|
||||
gift_balance_after: before_gift,
|
||||
link_type: Some("refund_request".to_string()),
|
||||
link_id: Some(refund.id.clone()),
|
||||
operator_id: operator_id.map(ToOwned::to_owned),
|
||||
description: Some("退款占款".to_string()),
|
||||
created_at_unix_secs: now_unix_secs,
|
||||
};
|
||||
|
||||
let mut updated_refund = refund.clone();
|
||||
updated_refund.status = "processing".to_string();
|
||||
updated_refund.approved_by = operator_id.map(ToOwned::to_owned);
|
||||
updated_refund.processed_by = operator_id.map(ToOwned::to_owned);
|
||||
updated_refund.processed_at_unix_secs = Some(now_unix_secs);
|
||||
updated_refund.updated_at_unix_secs = now_unix_secs;
|
||||
|
||||
wallet_store
|
||||
.lock()
|
||||
.expect("auth wallet store should lock")
|
||||
.insert(updated_wallet.id.clone(), updated_wallet.clone());
|
||||
refund_store
|
||||
.lock()
|
||||
.expect("admin wallet refund store should lock")
|
||||
.insert(updated_refund.id.clone(), updated_refund.clone());
|
||||
if let Some(updated_order) = updated_order {
|
||||
self.admin_wallet_payment_order_store
|
||||
.as_ref()
|
||||
.expect("admin wallet payment order store should exist")
|
||||
.lock()
|
||||
.expect("admin wallet payment order store should lock")
|
||||
.insert(updated_order.id.clone(), updated_order);
|
||||
}
|
||||
|
||||
return Ok(AdminWalletMutationOutcome::Applied((
|
||||
updated_wallet,
|
||||
updated_refund,
|
||||
transaction,
|
||||
)));
|
||||
}
|
||||
|
||||
match self
|
||||
.process_admin_wallet_refund(
|
||||
aether_data::repository::wallet::ProcessAdminWalletRefundInput {
|
||||
wallet_id: wallet_id.to_string(),
|
||||
refund_id: refund_id.to_string(),
|
||||
operator_id: operator_id.map(ToOwned::to_owned),
|
||||
},
|
||||
)
|
||||
.await?
|
||||
{
|
||||
Some(aether_data::repository::wallet::WalletMutationOutcome::Applied((
|
||||
wallet,
|
||||
refund,
|
||||
transaction,
|
||||
))) => Ok(AdminWalletMutationOutcome::Applied((
|
||||
wallet,
|
||||
stored_admin_wallet_refund_to_gateway(refund),
|
||||
stored_admin_wallet_transaction_to_gateway(transaction),
|
||||
))),
|
||||
Some(aether_data::repository::wallet::WalletMutationOutcome::NotFound) => {
|
||||
Ok(AdminWalletMutationOutcome::NotFound)
|
||||
}
|
||||
Some(aether_data::repository::wallet::WalletMutationOutcome::Invalid(detail)) => {
|
||||
Ok(AdminWalletMutationOutcome::Invalid(detail))
|
||||
}
|
||||
None => Ok(AdminWalletMutationOutcome::Unavailable),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn admin_complete_wallet_refund(
|
||||
&self,
|
||||
wallet_id: &str,
|
||||
refund_id: &str,
|
||||
gateway_refund_id: Option<&str>,
|
||||
payout_reference: Option<&str>,
|
||||
payout_proof: Option<serde_json::Value>,
|
||||
) -> Result<AdminWalletMutationOutcome<AdminWalletRefundRecord>, GatewayError> {
|
||||
#[cfg(test)]
|
||||
if let Some(refund_store) = self.admin_wallet_refund_store.as_ref() {
|
||||
let Some(refund) = refund_store
|
||||
.lock()
|
||||
.expect("admin wallet refund store should lock")
|
||||
.get(refund_id)
|
||||
.filter(|refund| refund.wallet_id == wallet_id)
|
||||
.cloned()
|
||||
else {
|
||||
return Ok(AdminWalletMutationOutcome::NotFound);
|
||||
};
|
||||
if refund.status != "processing" {
|
||||
return Ok(AdminWalletMutationOutcome::Invalid(
|
||||
"refund status must be processing before completion".to_string(),
|
||||
));
|
||||
}
|
||||
let now_unix_secs = chrono::Utc::now().timestamp().max(0) as u64;
|
||||
let mut updated_refund = refund;
|
||||
updated_refund.status = "succeeded".to_string();
|
||||
updated_refund.gateway_refund_id = gateway_refund_id.map(ToOwned::to_owned);
|
||||
updated_refund.payout_reference = payout_reference.map(ToOwned::to_owned);
|
||||
updated_refund.payout_proof = payout_proof;
|
||||
updated_refund.completed_at_unix_secs = Some(now_unix_secs);
|
||||
updated_refund.updated_at_unix_secs = now_unix_secs;
|
||||
refund_store
|
||||
.lock()
|
||||
.expect("admin wallet refund store should lock")
|
||||
.insert(updated_refund.id.clone(), updated_refund.clone());
|
||||
return Ok(AdminWalletMutationOutcome::Applied(updated_refund));
|
||||
}
|
||||
|
||||
match self
|
||||
.complete_admin_wallet_refund(
|
||||
aether_data::repository::wallet::CompleteAdminWalletRefundInput {
|
||||
wallet_id: wallet_id.to_string(),
|
||||
refund_id: refund_id.to_string(),
|
||||
gateway_refund_id: gateway_refund_id.map(ToOwned::to_owned),
|
||||
payout_reference: payout_reference.map(ToOwned::to_owned),
|
||||
payout_proof,
|
||||
},
|
||||
)
|
||||
.await?
|
||||
{
|
||||
Some(aether_data::repository::wallet::WalletMutationOutcome::Applied(refund)) => Ok(
|
||||
AdminWalletMutationOutcome::Applied(stored_admin_wallet_refund_to_gateway(refund)),
|
||||
),
|
||||
Some(aether_data::repository::wallet::WalletMutationOutcome::NotFound) => {
|
||||
Ok(AdminWalletMutationOutcome::NotFound)
|
||||
}
|
||||
Some(aether_data::repository::wallet::WalletMutationOutcome::Invalid(detail)) => {
|
||||
Ok(AdminWalletMutationOutcome::Invalid(detail))
|
||||
}
|
||||
None => Ok(AdminWalletMutationOutcome::Unavailable),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn admin_fail_wallet_refund(
|
||||
&self,
|
||||
wallet_id: &str,
|
||||
refund_id: &str,
|
||||
reason: &str,
|
||||
operator_id: Option<&str>,
|
||||
) -> Result<
|
||||
AdminWalletMutationOutcome<(
|
||||
aether_data::repository::wallet::StoredWalletSnapshot,
|
||||
AdminWalletRefundRecord,
|
||||
Option<AdminWalletTransactionRecord>,
|
||||
)>,
|
||||
GatewayError,
|
||||
> {
|
||||
#[cfg(test)]
|
||||
if let (Some(wallet_store), Some(refund_store)) = (
|
||||
self.auth_wallet_store.as_ref(),
|
||||
self.admin_wallet_refund_store.as_ref(),
|
||||
) {
|
||||
let Some(wallet) = wallet_store
|
||||
.lock()
|
||||
.expect("auth wallet store should lock")
|
||||
.get(wallet_id)
|
||||
.cloned()
|
||||
else {
|
||||
return Ok(AdminWalletMutationOutcome::NotFound);
|
||||
};
|
||||
let Some(refund) = refund_store
|
||||
.lock()
|
||||
.expect("admin wallet refund store should lock")
|
||||
.get(refund_id)
|
||||
.filter(|refund| refund.wallet_id == wallet_id)
|
||||
.cloned()
|
||||
else {
|
||||
return Ok(AdminWalletMutationOutcome::NotFound);
|
||||
};
|
||||
|
||||
let now_unix_secs = chrono::Utc::now().timestamp().max(0) as u64;
|
||||
if matches!(refund.status.as_str(), "pending_approval" | "approved") {
|
||||
let mut updated_refund = refund;
|
||||
updated_refund.status = "failed".to_string();
|
||||
updated_refund.failure_reason = Some(reason.to_string());
|
||||
updated_refund.updated_at_unix_secs = now_unix_secs;
|
||||
refund_store
|
||||
.lock()
|
||||
.expect("admin wallet refund store should lock")
|
||||
.insert(updated_refund.id.clone(), updated_refund.clone());
|
||||
return Ok(AdminWalletMutationOutcome::Applied((
|
||||
wallet,
|
||||
updated_refund,
|
||||
None,
|
||||
)));
|
||||
}
|
||||
if refund.status != "processing" {
|
||||
return Ok(AdminWalletMutationOutcome::Invalid(format!(
|
||||
"cannot fail refund in status: {}",
|
||||
refund.status
|
||||
)));
|
||||
}
|
||||
|
||||
let amount_usd = refund.amount_usd;
|
||||
let before_recharge = wallet.balance;
|
||||
let before_gift = wallet.gift_balance;
|
||||
let before_total = before_recharge + before_gift;
|
||||
let after_recharge = before_recharge + amount_usd;
|
||||
|
||||
let mut updated_wallet = wallet.clone();
|
||||
updated_wallet.balance = after_recharge;
|
||||
updated_wallet.total_refunded = (updated_wallet.total_refunded - amount_usd).max(0.0);
|
||||
updated_wallet.updated_at_unix_secs = now_unix_secs;
|
||||
|
||||
let transaction = AdminWalletTransactionRecord {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
wallet_id: updated_wallet.id.clone(),
|
||||
category: "refund".to_string(),
|
||||
reason_code: "refund_revert".to_string(),
|
||||
amount: amount_usd,
|
||||
balance_before: before_total,
|
||||
balance_after: after_recharge + before_gift,
|
||||
recharge_balance_before: before_recharge,
|
||||
recharge_balance_after: after_recharge,
|
||||
gift_balance_before: before_gift,
|
||||
gift_balance_after: before_gift,
|
||||
link_type: Some("refund_request".to_string()),
|
||||
link_id: Some(refund.id.clone()),
|
||||
operator_id: operator_id.map(ToOwned::to_owned),
|
||||
description: Some("退款失败回补".to_string()),
|
||||
created_at_unix_secs: now_unix_secs,
|
||||
};
|
||||
|
||||
if let Some(payment_order_id) = refund.payment_order_id.clone() {
|
||||
let Some(order_store) = self.admin_wallet_payment_order_store.as_ref() else {
|
||||
return Ok(AdminWalletMutationOutcome::Unavailable);
|
||||
};
|
||||
let maybe_order = order_store
|
||||
.lock()
|
||||
.expect("admin wallet payment order store should lock")
|
||||
.get(&payment_order_id)
|
||||
.cloned();
|
||||
if let Some(mut order) = maybe_order {
|
||||
order.refunded_amount_usd -= amount_usd;
|
||||
order.refundable_amount_usd += amount_usd;
|
||||
order_store
|
||||
.lock()
|
||||
.expect("admin wallet payment order store should lock")
|
||||
.insert(order.id.clone(), order);
|
||||
}
|
||||
}
|
||||
|
||||
let mut updated_refund = refund;
|
||||
updated_refund.status = "failed".to_string();
|
||||
updated_refund.failure_reason = Some(reason.to_string());
|
||||
updated_refund.updated_at_unix_secs = now_unix_secs;
|
||||
|
||||
wallet_store
|
||||
.lock()
|
||||
.expect("auth wallet store should lock")
|
||||
.insert(updated_wallet.id.clone(), updated_wallet.clone());
|
||||
refund_store
|
||||
.lock()
|
||||
.expect("admin wallet refund store should lock")
|
||||
.insert(updated_refund.id.clone(), updated_refund.clone());
|
||||
|
||||
return Ok(AdminWalletMutationOutcome::Applied((
|
||||
updated_wallet,
|
||||
updated_refund,
|
||||
Some(transaction),
|
||||
)));
|
||||
}
|
||||
|
||||
match self
|
||||
.fail_admin_wallet_refund(
|
||||
aether_data::repository::wallet::FailAdminWalletRefundInput {
|
||||
wallet_id: wallet_id.to_string(),
|
||||
refund_id: refund_id.to_string(),
|
||||
reason: reason.to_string(),
|
||||
operator_id: operator_id.map(ToOwned::to_owned),
|
||||
},
|
||||
)
|
||||
.await?
|
||||
{
|
||||
Some(aether_data::repository::wallet::WalletMutationOutcome::Applied((
|
||||
wallet,
|
||||
refund,
|
||||
transaction,
|
||||
))) => Ok(AdminWalletMutationOutcome::Applied((
|
||||
wallet,
|
||||
stored_admin_wallet_refund_to_gateway(refund),
|
||||
transaction.map(stored_admin_wallet_transaction_to_gateway),
|
||||
))),
|
||||
Some(aether_data::repository::wallet::WalletMutationOutcome::NotFound) => {
|
||||
Ok(AdminWalletMutationOutcome::NotFound)
|
||||
}
|
||||
Some(aether_data::repository::wallet::WalletMutationOutcome::Invalid(detail)) => {
|
||||
Ok(AdminWalletMutationOutcome::Invalid(detail))
|
||||
}
|
||||
None => Ok(AdminWalletMutationOutcome::Unavailable),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn stored_admin_wallet_refund_to_gateway(
|
||||
refund: aether_data::repository::wallet::StoredAdminWalletRefund,
|
||||
) -> AdminWalletRefundRecord {
|
||||
AdminWalletRefundRecord {
|
||||
id: refund.id,
|
||||
refund_no: refund.refund_no,
|
||||
wallet_id: refund.wallet_id,
|
||||
user_id: refund.user_id,
|
||||
payment_order_id: refund.payment_order_id,
|
||||
source_type: refund.source_type,
|
||||
source_id: refund.source_id,
|
||||
refund_mode: refund.refund_mode,
|
||||
amount_usd: refund.amount_usd,
|
||||
status: refund.status,
|
||||
reason: refund.reason,
|
||||
failure_reason: refund.failure_reason,
|
||||
gateway_refund_id: refund.gateway_refund_id,
|
||||
payout_method: refund.payout_method,
|
||||
payout_reference: refund.payout_reference,
|
||||
payout_proof: refund.payout_proof,
|
||||
requested_by: refund.requested_by,
|
||||
approved_by: refund.approved_by,
|
||||
processed_by: refund.processed_by,
|
||||
created_at_unix_secs: refund.created_at_unix_secs,
|
||||
updated_at_unix_secs: refund.updated_at_unix_secs,
|
||||
processed_at_unix_secs: refund.processed_at_unix_secs,
|
||||
completed_at_unix_secs: refund.completed_at_unix_secs,
|
||||
}
|
||||
}
|
||||
|
||||
fn stored_admin_wallet_transaction_to_gateway(
|
||||
transaction: aether_data::repository::wallet::StoredAdminWalletTransaction,
|
||||
) -> AdminWalletTransactionRecord {
|
||||
AdminWalletTransactionRecord {
|
||||
id: transaction.id,
|
||||
wallet_id: transaction.wallet_id,
|
||||
category: transaction.category,
|
||||
reason_code: transaction.reason_code,
|
||||
amount: transaction.amount,
|
||||
balance_before: transaction.balance_before,
|
||||
balance_after: transaction.balance_after,
|
||||
recharge_balance_before: transaction.recharge_balance_before,
|
||||
recharge_balance_after: transaction.recharge_balance_after,
|
||||
gift_balance_before: transaction.gift_balance_before,
|
||||
gift_balance_after: transaction.gift_balance_after,
|
||||
link_type: transaction.link_type,
|
||||
link_id: transaction.link_id,
|
||||
operator_id: transaction.operator_id,
|
||||
description: transaction.description,
|
||||
created_at_unix_secs: transaction.created_at_unix_secs.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
612
apps/aether-gateway/src/state/testing.rs
Normal file
612
apps/aether-gateway/src/state/testing.rs
Normal file
@@ -0,0 +1,612 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex as StdMutex};
|
||||
|
||||
use serde_json::json;
|
||||
|
||||
use super::{AppState, GatewayDataState};
|
||||
use crate::{provider_transport, usage};
|
||||
|
||||
#[cfg(test)]
|
||||
impl AppState {
|
||||
pub(crate) fn with_data_state_for_tests(mut self, data_state: GatewayDataState) -> Self {
|
||||
self.replace_data_state(Arc::new(data_state));
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_tunnel_identity_for_tests(
|
||||
mut self,
|
||||
instance_id: &str,
|
||||
relay_base_url: Option<&str>,
|
||||
) -> Self {
|
||||
self.tunnel = crate::tunnel::EmbeddedTunnelState::with_data_and_directory(
|
||||
Arc::clone(&self.data),
|
||||
crate::tunnel::TunnelAttachmentDirectory::for_tests(
|
||||
instance_id,
|
||||
relay_base_url,
|
||||
90,
|
||||
),
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_video_task_data_reader_for_tests(
|
||||
mut self,
|
||||
repository: Arc<dyn aether_data::repository::video_tasks::VideoTaskReadRepository>,
|
||||
) -> Self {
|
||||
self.replace_data_state(Arc::new(
|
||||
GatewayDataState::with_video_task_reader_for_tests(repository),
|
||||
));
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_video_task_data_repository_for_tests<T>(mut self, repository: Arc<T>) -> Self
|
||||
where
|
||||
T: aether_data::repository::video_tasks::VideoTaskRepository + 'static,
|
||||
{
|
||||
self.replace_data_state(Arc::new(
|
||||
GatewayDataState::with_video_task_repository_for_tests(repository),
|
||||
));
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_video_task_repository_and_provider_transport_for_tests<T>(
|
||||
mut self,
|
||||
repository: Arc<T>,
|
||||
provider_catalog_repository: Arc<
|
||||
dyn aether_data::repository::provider_catalog::ProviderCatalogReadRepository,
|
||||
>,
|
||||
encryption_key: impl Into<String>,
|
||||
) -> Self
|
||||
where
|
||||
T: aether_data::repository::video_tasks::VideoTaskRepository + 'static,
|
||||
{
|
||||
self.replace_data_state(Arc::new(
|
||||
GatewayDataState::with_video_task_repository_and_provider_transport_for_tests(
|
||||
repository,
|
||||
provider_catalog_repository,
|
||||
encryption_key,
|
||||
),
|
||||
));
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_request_candidate_data_reader_for_tests(
|
||||
mut self,
|
||||
repository: Arc<dyn aether_data::repository::candidates::RequestCandidateReadRepository>,
|
||||
) -> Self {
|
||||
self.replace_data_state(Arc::new(
|
||||
GatewayDataState::with_request_candidate_reader_for_tests(repository),
|
||||
));
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_decision_trace_data_readers_for_tests(
|
||||
mut self,
|
||||
request_candidate_repository: Arc<
|
||||
dyn aether_data::repository::candidates::RequestCandidateReadRepository,
|
||||
>,
|
||||
provider_catalog_repository: Arc<
|
||||
dyn aether_data::repository::provider_catalog::ProviderCatalogReadRepository,
|
||||
>,
|
||||
) -> Self {
|
||||
self.replace_data_state(Arc::new(
|
||||
GatewayDataState::with_decision_trace_readers_for_tests(
|
||||
request_candidate_repository,
|
||||
provider_catalog_repository,
|
||||
),
|
||||
));
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_request_audit_data_readers_for_tests(
|
||||
mut self,
|
||||
auth_api_key_repository: Arc<dyn aether_data::repository::auth::AuthApiKeyReadRepository>,
|
||||
request_candidate_repository: Arc<
|
||||
dyn aether_data::repository::candidates::RequestCandidateReadRepository,
|
||||
>,
|
||||
provider_catalog_repository: Arc<
|
||||
dyn aether_data::repository::provider_catalog::ProviderCatalogReadRepository,
|
||||
>,
|
||||
usage_repository: Arc<dyn aether_data::repository::usage::UsageReadRepository>,
|
||||
) -> Self {
|
||||
self.replace_data_state(Arc::new(
|
||||
GatewayDataState::with_request_audit_readers_for_tests(
|
||||
auth_api_key_repository,
|
||||
request_candidate_repository,
|
||||
provider_catalog_repository,
|
||||
usage_repository,
|
||||
),
|
||||
));
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_auth_api_key_data_reader_for_tests(
|
||||
mut self,
|
||||
repository: Arc<dyn aether_data::repository::auth::AuthApiKeyReadRepository>,
|
||||
) -> Self {
|
||||
self.replace_data_state(Arc::new(
|
||||
GatewayDataState::with_auth_api_key_reader_for_tests(repository),
|
||||
));
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_frontdoor_system_default_rpm_for_tests(mut self, limit: u32) -> Self {
|
||||
self.frontdoor_user_rpm = Arc::new(
|
||||
(*self.frontdoor_user_rpm)
|
||||
.clone()
|
||||
.with_system_default_limit_for_tests(limit),
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_usage_data_reader_for_tests(
|
||||
mut self,
|
||||
repository: Arc<dyn aether_data::repository::usage::UsageReadRepository>,
|
||||
) -> Self {
|
||||
self.replace_data_state(Arc::new(GatewayDataState::with_usage_reader_for_tests(
|
||||
repository,
|
||||
)));
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_user_data_reader_for_tests(
|
||||
mut self,
|
||||
repository: Arc<dyn aether_data::repository::users::UserReadRepository>,
|
||||
) -> Self {
|
||||
self.replace_data_state(Arc::new(GatewayDataState::with_user_reader_for_tests(
|
||||
repository,
|
||||
)));
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_usage_data_repository_for_tests<T>(mut self, repository: Arc<T>) -> Self
|
||||
where
|
||||
T: aether_data::repository::usage::UsageRepository + 'static,
|
||||
{
|
||||
self.replace_data_state(Arc::new(GatewayDataState::with_usage_repository_for_tests(
|
||||
repository,
|
||||
)));
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_usage_runtime_for_tests(
|
||||
mut self,
|
||||
config: usage::UsageRuntimeConfig,
|
||||
) -> Self {
|
||||
self.usage_runtime =
|
||||
Arc::new(usage::UsageRuntime::new(config).expect("usage runtime config should build"));
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_oauth_refresh_coordinator_for_tests(
|
||||
mut self,
|
||||
coordinator: provider_transport::LocalOAuthRefreshCoordinator,
|
||||
) -> Self {
|
||||
self.oauth_refresh = Arc::new(coordinator);
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_provider_oauth_state_entry_for_tests(
|
||||
mut self,
|
||||
nonce: &str,
|
||||
payload: serde_json::Value,
|
||||
) -> Self {
|
||||
let store = self
|
||||
.provider_oauth_state_store
|
||||
.get_or_insert_with(|| Arc::new(StdMutex::new(HashMap::new())));
|
||||
store
|
||||
.lock()
|
||||
.expect("provider oauth state store should lock")
|
||||
.insert(format!("provider_oauth_state:{nonce}"), payload.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_provider_oauth_device_session_entry_for_tests(
|
||||
mut self,
|
||||
session_id: &str,
|
||||
payload: serde_json::Value,
|
||||
) -> Self {
|
||||
let store = self
|
||||
.provider_oauth_device_session_store
|
||||
.get_or_insert_with(|| Arc::new(StdMutex::new(HashMap::new())));
|
||||
store
|
||||
.lock()
|
||||
.expect("provider oauth device session store should lock")
|
||||
.insert(
|
||||
format!("device_auth_session:{session_id}"),
|
||||
payload.to_string(),
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_provider_oauth_batch_task_entry_for_tests(
|
||||
mut self,
|
||||
task_id: &str,
|
||||
payload: serde_json::Value,
|
||||
) -> Self {
|
||||
let store = self
|
||||
.provider_oauth_batch_task_store
|
||||
.get_or_insert_with(|| Arc::new(StdMutex::new(HashMap::new())));
|
||||
store
|
||||
.lock()
|
||||
.expect("provider oauth batch task store should lock")
|
||||
.insert(
|
||||
format!("provider_oauth_batch_task:{task_id}"),
|
||||
payload.to_string(),
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_auth_session_for_tests(
|
||||
self,
|
||||
session: crate::data::state::StoredUserSessionRecord,
|
||||
) -> Self {
|
||||
self.with_auth_sessions_for_tests([session])
|
||||
}
|
||||
|
||||
pub(crate) fn with_auth_sessions_for_tests<I>(mut self, sessions: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = crate::data::state::StoredUserSessionRecord>,
|
||||
{
|
||||
let store = self
|
||||
.auth_session_store
|
||||
.get_or_insert_with(|| Arc::new(StdMutex::new(HashMap::new())));
|
||||
let mut guard = store.lock().expect("auth session store should lock");
|
||||
for session in sessions {
|
||||
guard.insert(format!("{}:{}", session.user_id, session.id), session);
|
||||
}
|
||||
drop(guard);
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_auth_users_for_tests<I>(mut self, users: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = aether_data::repository::users::StoredUserAuthRecord>,
|
||||
{
|
||||
let store = self
|
||||
.auth_user_store
|
||||
.get_or_insert_with(|| Arc::new(StdMutex::new(HashMap::new())));
|
||||
let mut guard = store.lock().expect("auth user store should lock");
|
||||
for user in users {
|
||||
guard.insert(user.id.clone(), user);
|
||||
}
|
||||
drop(guard);
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn without_auth_user_store_for_tests(mut self) -> Self {
|
||||
self.auth_user_store = None;
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn without_auth_user_model_capability_store_for_tests(mut self) -> Self {
|
||||
self.auth_user_model_capability_store = None;
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_auth_wallets_for_tests<I>(mut self, wallets: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = aether_data::repository::wallet::StoredWalletSnapshot>,
|
||||
{
|
||||
let store = self
|
||||
.auth_wallet_store
|
||||
.get_or_insert_with(|| Arc::new(StdMutex::new(HashMap::new())));
|
||||
let mut guard = store.lock().expect("auth wallet store should lock");
|
||||
for wallet in wallets {
|
||||
guard.insert(wallet.id.clone(), wallet);
|
||||
}
|
||||
drop(guard);
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_admin_wallet_payment_orders_for_tests<I>(mut self, orders: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = crate::AdminWalletPaymentOrderRecord>,
|
||||
{
|
||||
let store = self
|
||||
.admin_wallet_payment_order_store
|
||||
.get_or_insert_with(|| Arc::new(StdMutex::new(HashMap::new())));
|
||||
let mut guard = store
|
||||
.lock()
|
||||
.expect("admin wallet payment order store should lock");
|
||||
for order in orders {
|
||||
guard.insert(order.id.clone(), order);
|
||||
}
|
||||
drop(guard);
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_admin_payment_callbacks_for_tests<I>(mut self, callbacks: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = crate::state::AdminPaymentCallbackRecord>,
|
||||
{
|
||||
let store = self
|
||||
.admin_payment_callback_store
|
||||
.get_or_insert_with(|| Arc::new(StdMutex::new(HashMap::new())));
|
||||
let mut guard = store
|
||||
.lock()
|
||||
.expect("admin payment callback store should lock");
|
||||
for callback in callbacks {
|
||||
guard.insert(callback.id.clone(), callback);
|
||||
}
|
||||
drop(guard);
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_admin_wallet_transactions_for_tests<I>(mut self, transactions: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = crate::AdminWalletTransactionRecord>,
|
||||
{
|
||||
let store = self
|
||||
.admin_wallet_transaction_store
|
||||
.get_or_insert_with(|| Arc::new(StdMutex::new(HashMap::new())));
|
||||
let mut guard = store
|
||||
.lock()
|
||||
.expect("admin wallet transaction store should lock");
|
||||
for transaction in transactions {
|
||||
guard.insert(transaction.id.clone(), transaction);
|
||||
}
|
||||
drop(guard);
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_admin_wallet_refunds_for_tests<I>(mut self, refunds: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = crate::AdminWalletRefundRecord>,
|
||||
{
|
||||
let store = self
|
||||
.admin_wallet_refund_store
|
||||
.get_or_insert_with(|| Arc::new(StdMutex::new(HashMap::new())));
|
||||
let mut guard = store.lock().expect("admin wallet refund store should lock");
|
||||
for refund in refunds {
|
||||
guard.insert(refund.id.clone(), refund);
|
||||
}
|
||||
drop(guard);
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_admin_billing_rules_for_tests<I>(mut self, rules: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = crate::AdminBillingRuleRecord>,
|
||||
{
|
||||
let store = self
|
||||
.admin_billing_rule_store
|
||||
.get_or_insert_with(|| Arc::new(StdMutex::new(HashMap::new())));
|
||||
let mut guard = store.lock().expect("admin billing rule store should lock");
|
||||
for rule in rules {
|
||||
guard.insert(rule.id.clone(), rule);
|
||||
}
|
||||
drop(guard);
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_admin_billing_collectors_for_tests<I>(mut self, collectors: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = crate::AdminBillingCollectorRecord>,
|
||||
{
|
||||
let store = self
|
||||
.admin_billing_collector_store
|
||||
.get_or_insert_with(|| Arc::new(StdMutex::new(HashMap::new())));
|
||||
let mut guard = store
|
||||
.lock()
|
||||
.expect("admin billing collector store should lock");
|
||||
for collector in collectors {
|
||||
guard.insert(collector.id.clone(), collector);
|
||||
}
|
||||
drop(guard);
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_admin_security_blacklist_for_tests<I>(mut self, entries: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = (String, String)>,
|
||||
{
|
||||
let store = self
|
||||
.admin_security_blacklist_store
|
||||
.get_or_insert_with(|| Arc::new(StdMutex::new(HashMap::new())));
|
||||
let mut guard = store
|
||||
.lock()
|
||||
.expect("admin security blacklist store should lock");
|
||||
for (ip_address, reason) in entries {
|
||||
guard.insert(ip_address, reason);
|
||||
}
|
||||
drop(guard);
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_admin_security_whitelist_for_tests<I>(mut self, entries: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = String>,
|
||||
{
|
||||
let store = self
|
||||
.admin_security_whitelist_store
|
||||
.get_or_insert_with(|| Arc::new(StdMutex::new(std::collections::BTreeSet::new())));
|
||||
let mut guard = store
|
||||
.lock()
|
||||
.expect("admin security whitelist store should lock");
|
||||
for ip_address in entries {
|
||||
guard.insert(ip_address);
|
||||
}
|
||||
drop(guard);
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_admin_monitoring_cache_affinity_entry_for_tests(
|
||||
mut self,
|
||||
cache_key: &str,
|
||||
payload: serde_json::Value,
|
||||
) -> Self {
|
||||
let store = self
|
||||
.admin_monitoring_cache_affinity_store
|
||||
.get_or_insert_with(|| Arc::new(StdMutex::new(HashMap::new())));
|
||||
store
|
||||
.lock()
|
||||
.expect("admin monitoring cache affinity store should lock")
|
||||
.insert(cache_key.to_string(), payload.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn list_admin_monitoring_cache_affinity_entries_for_tests(
|
||||
&self,
|
||||
) -> Vec<(String, String)> {
|
||||
self.admin_monitoring_cache_affinity_store
|
||||
.as_ref()
|
||||
.map(|store| {
|
||||
store
|
||||
.lock()
|
||||
.expect("admin monitoring cache affinity store should lock")
|
||||
.iter()
|
||||
.map(|(key, value)| (key.clone(), value.clone()))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub(crate) fn remove_admin_monitoring_cache_affinity_entries_for_tests(
|
||||
&self,
|
||||
raw_keys: &[String],
|
||||
) -> usize {
|
||||
let Some(store) = self.admin_monitoring_cache_affinity_store.as_ref() else {
|
||||
return 0;
|
||||
};
|
||||
let mut guard = store
|
||||
.lock()
|
||||
.expect("admin monitoring cache affinity store should lock");
|
||||
raw_keys
|
||||
.iter()
|
||||
.filter(|raw_key| guard.remove(raw_key.as_str()).is_some())
|
||||
.count()
|
||||
}
|
||||
|
||||
pub(crate) fn with_admin_monitoring_redis_key_for_tests(
|
||||
mut self,
|
||||
cache_key: &str,
|
||||
payload: serde_json::Value,
|
||||
) -> Self {
|
||||
let store = self
|
||||
.admin_monitoring_redis_key_store
|
||||
.get_or_insert_with(|| Arc::new(StdMutex::new(HashMap::new())));
|
||||
store
|
||||
.lock()
|
||||
.expect("admin monitoring redis key store should lock")
|
||||
.insert(cache_key.to_string(), payload.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn list_admin_monitoring_redis_keys_for_tests(&self) -> Vec<String> {
|
||||
self.admin_monitoring_redis_key_store
|
||||
.as_ref()
|
||||
.map(|store| {
|
||||
store
|
||||
.lock()
|
||||
.expect("admin monitoring redis key store should lock")
|
||||
.keys()
|
||||
.cloned()
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub(crate) fn remove_admin_monitoring_redis_keys_for_tests(
|
||||
&self,
|
||||
raw_keys: &[String],
|
||||
) -> usize {
|
||||
let Some(store) = self.admin_monitoring_redis_key_store.as_ref() else {
|
||||
return 0;
|
||||
};
|
||||
let mut guard = store
|
||||
.lock()
|
||||
.expect("admin monitoring redis key store should lock");
|
||||
raw_keys
|
||||
.iter()
|
||||
.filter(|raw_key| guard.remove(raw_key.as_str()).is_some())
|
||||
.count()
|
||||
}
|
||||
|
||||
pub(crate) fn with_auth_email_verification_pending_for_tests(
|
||||
mut self,
|
||||
email: &str,
|
||||
code: &str,
|
||||
created_at: chrono::DateTime<chrono::Utc>,
|
||||
) -> Self {
|
||||
let store = self
|
||||
.auth_email_verification_store
|
||||
.get_or_insert_with(|| Arc::new(StdMutex::new(HashMap::new())));
|
||||
store
|
||||
.lock()
|
||||
.expect("auth email verification store should lock")
|
||||
.insert(
|
||||
format!("email:verification:{}", email.trim().to_ascii_lowercase()),
|
||||
json!({
|
||||
"code": code,
|
||||
"created_at": created_at.to_rfc3339(),
|
||||
})
|
||||
.to_string(),
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_auth_email_verified_for_tests(mut self, email: &str) -> Self {
|
||||
let store = self
|
||||
.auth_email_verification_store
|
||||
.get_or_insert_with(|| Arc::new(StdMutex::new(HashMap::new())));
|
||||
store
|
||||
.lock()
|
||||
.expect("auth email verification store should lock")
|
||||
.insert(
|
||||
format!("email:verified:{}", email.trim().to_ascii_lowercase()),
|
||||
"verified".to_string(),
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_auth_user_model_capability_settings_for_tests(
|
||||
mut self,
|
||||
user_id: &str,
|
||||
settings: serde_json::Value,
|
||||
) -> Self {
|
||||
let store = self
|
||||
.auth_user_model_capability_store
|
||||
.get_or_insert_with(|| Arc::new(StdMutex::new(HashMap::new())));
|
||||
store
|
||||
.lock()
|
||||
.expect("auth user model capability store should lock")
|
||||
.insert(user_id.to_string(), settings);
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_provider_oauth_token_url_for_tests(
|
||||
self,
|
||||
provider_type: &str,
|
||||
token_url: impl Into<String>,
|
||||
) -> Self {
|
||||
self.provider_oauth_token_url_overrides
|
||||
.lock()
|
||||
.expect("provider oauth token url overrides should lock")
|
||||
.insert(provider_type.trim().to_ascii_lowercase(), token_url.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_shadow_result_data_writer_for_tests(
|
||||
mut self,
|
||||
repository: Arc<dyn aether_data::repository::shadow_results::ShadowResultWriteRepository>,
|
||||
) -> Self {
|
||||
self.replace_data_state(Arc::new(
|
||||
GatewayDataState::with_shadow_result_writer_for_tests(repository),
|
||||
));
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_shadow_result_data_repository_for_tests<T>(
|
||||
mut self,
|
||||
repository: Arc<T>,
|
||||
) -> Self
|
||||
where
|
||||
T: aether_data::repository::shadow_results::ShadowResultRepository + 'static,
|
||||
{
|
||||
self.replace_data_state(Arc::new(
|
||||
GatewayDataState::with_shadow_result_repository_for_tests(repository),
|
||||
));
|
||||
self
|
||||
}
|
||||
}
|
||||
56
apps/aether-gateway/src/state/types.rs
Normal file
56
apps/aether-gateway/src/state/types.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub(crate) struct LocalProviderDeleteTaskState {
|
||||
pub task_id: String,
|
||||
pub provider_id: String,
|
||||
pub status: String,
|
||||
pub stage: String,
|
||||
pub total_keys: usize,
|
||||
pub deleted_keys: usize,
|
||||
pub total_endpoints: usize,
|
||||
pub deleted_endpoints: usize,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) enum LocalMutationOutcome<T> {
|
||||
Applied(T),
|
||||
NotFound,
|
||||
Invalid(String),
|
||||
Unavailable,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub(crate) struct LocalExecutionRuntimeMissDiagnostic {
|
||||
pub(crate) reason: String,
|
||||
pub(crate) route_family: Option<String>,
|
||||
pub(crate) route_kind: Option<String>,
|
||||
pub(crate) public_path: Option<String>,
|
||||
pub(crate) plan_kind: Option<String>,
|
||||
pub(crate) requested_model: Option<String>,
|
||||
pub(crate) candidate_count: Option<usize>,
|
||||
pub(crate) skipped_candidate_count: Option<usize>,
|
||||
pub(crate) skip_reasons: std::collections::BTreeMap<String, usize>,
|
||||
}
|
||||
|
||||
impl LocalExecutionRuntimeMissDiagnostic {
|
||||
pub(crate) fn skip_reasons_summary(&self) -> Option<String> {
|
||||
if self.skip_reasons.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some(
|
||||
self.skip_reasons
|
||||
.iter()
|
||||
.map(|(reason, count)| format!("{reason}={count}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(","),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) enum AdminWalletMutationOutcome<T> {
|
||||
Applied(T),
|
||||
NotFound,
|
||||
Invalid(String),
|
||||
Unavailable,
|
||||
}
|
||||
179
apps/aether-gateway/src/state/video.rs
Normal file
179
apps/aether-gateway/src/state/video.rs
Normal file
@@ -0,0 +1,179 @@
|
||||
use super::{AppState, GatewayError};
|
||||
|
||||
use crate::{async_task, video_tasks};
|
||||
|
||||
impl AppState {
|
||||
pub(crate) async fn read_data_backed_video_task_response(
|
||||
&self,
|
||||
route_family: Option<&str>,
|
||||
request_path: &str,
|
||||
) -> Result<Option<video_tasks::LocalVideoTaskReadResponse>, GatewayError> {
|
||||
self.data
|
||||
.read_video_task_response(route_family, request_path)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn find_video_task_by_id(
|
||||
&self,
|
||||
task_id: &str,
|
||||
) -> Result<Option<aether_data::repository::video_tasks::StoredVideoTask>, GatewayError> {
|
||||
self.data
|
||||
.find_video_task(aether_data::repository::video_tasks::VideoTaskLookupKey::Id(task_id))
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn find_video_task_by_short_id(
|
||||
&self,
|
||||
short_id: &str,
|
||||
) -> Result<Option<aether_data::repository::video_tasks::StoredVideoTask>, GatewayError> {
|
||||
self.data
|
||||
.find_video_task(
|
||||
aether_data::repository::video_tasks::VideoTaskLookupKey::ShortId(short_id),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn upsert_video_task_snapshot(
|
||||
&self,
|
||||
snapshot: &video_tasks::LocalVideoTaskSnapshot,
|
||||
) -> Result<Option<aether_data::repository::video_tasks::StoredVideoTask>, GatewayError> {
|
||||
self.data
|
||||
.upsert_video_task(snapshot.to_upsert_record())
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn hydrate_video_task_for_route(
|
||||
&self,
|
||||
route_family: Option<&str>,
|
||||
request_path: &str,
|
||||
) -> Result<bool, GatewayError> {
|
||||
let lookup =
|
||||
video_tasks::resolve_video_task_hydration_lookup_key(route_family, request_path);
|
||||
let Some(lookup) = lookup else {
|
||||
return Ok(false);
|
||||
};
|
||||
let Some(task) = self
|
||||
.data
|
||||
.find_video_task(lookup)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?
|
||||
else {
|
||||
return Ok(false);
|
||||
};
|
||||
if self.video_tasks.hydrate_from_stored_task(&task) {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
let Some(snapshot) = self.reconstruct_video_task_snapshot(&task).await? else {
|
||||
return Ok(false);
|
||||
};
|
||||
self.video_tasks.record_snapshot(snapshot);
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub(crate) async fn reconstruct_video_task_snapshot(
|
||||
&self,
|
||||
task: &aether_data::repository::video_tasks::StoredVideoTask,
|
||||
) -> Result<Option<video_tasks::LocalVideoTaskSnapshot>, GatewayError> {
|
||||
crate::provider_transport::reconstruct_local_video_task_snapshot(self, task)
|
||||
.await
|
||||
.map_err(GatewayError::Internal)
|
||||
}
|
||||
|
||||
pub(crate) async fn claim_due_video_tasks(
|
||||
&self,
|
||||
now_unix_secs: u64,
|
||||
claim_until_unix_secs: u64,
|
||||
limit: usize,
|
||||
) -> Result<Vec<aether_data::repository::video_tasks::StoredVideoTask>, GatewayError> {
|
||||
self.data
|
||||
.claim_due_video_tasks(now_unix_secs, claim_until_unix_secs, limit)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn update_active_video_task(
|
||||
&self,
|
||||
task: aether_data::repository::video_tasks::UpsertVideoTask,
|
||||
) -> Result<Option<aether_data::repository::video_tasks::StoredVideoTask>, GatewayError> {
|
||||
self.data
|
||||
.update_active_video_task(task)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn list_video_task_page(
|
||||
&self,
|
||||
filter: &aether_data::repository::video_tasks::VideoTaskQueryFilter,
|
||||
offset: usize,
|
||||
limit: usize,
|
||||
) -> Result<Vec<aether_data::repository::video_tasks::StoredVideoTask>, GatewayError> {
|
||||
self.data
|
||||
.list_video_task_page(filter, offset, limit)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn count_video_tasks(
|
||||
&self,
|
||||
filter: &aether_data::repository::video_tasks::VideoTaskQueryFilter,
|
||||
) -> Result<u64, GatewayError> {
|
||||
self.data
|
||||
.count_video_tasks(filter)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn count_video_tasks_by_status(
|
||||
&self,
|
||||
filter: &aether_data::repository::video_tasks::VideoTaskQueryFilter,
|
||||
) -> Result<Vec<aether_data::repository::video_tasks::VideoTaskStatusCount>, GatewayError> {
|
||||
self.data
|
||||
.count_video_tasks_by_status(filter)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn count_distinct_video_task_users(
|
||||
&self,
|
||||
filter: &aether_data::repository::video_tasks::VideoTaskQueryFilter,
|
||||
) -> Result<u64, GatewayError> {
|
||||
self.data
|
||||
.count_distinct_video_task_users(filter)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn top_video_task_models(
|
||||
&self,
|
||||
filter: &aether_data::repository::video_tasks::VideoTaskQueryFilter,
|
||||
limit: usize,
|
||||
) -> Result<Vec<aether_data::repository::video_tasks::VideoTaskModelCount>, GatewayError> {
|
||||
self.data
|
||||
.top_video_task_models(filter, limit)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn count_video_tasks_created_since(
|
||||
&self,
|
||||
filter: &aether_data::repository::video_tasks::VideoTaskQueryFilter,
|
||||
created_since_unix_secs: u64,
|
||||
) -> Result<u64, GatewayError> {
|
||||
self.data
|
||||
.count_video_tasks_created_since(filter, created_since_unix_secs)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn execute_video_task_refresh_plan(
|
||||
&self,
|
||||
refresh_plan: &video_tasks::LocalVideoTaskReadRefreshPlan,
|
||||
) -> Result<bool, GatewayError> {
|
||||
async_task::execute_video_task_refresh_plan(self, refresh_plan).await
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user