mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-08 18:37:46 +08:00
feat: 扩展 Rust gateway 全功能模块,新增 billing/crypto/wallet crate 及完整数据层
- 新增 aether-billing、aether-crypto、aether-wallet 独立 crate - aether-data 扩展 repository 层:announcements、auth_modules、billing、 candidate_selection、gemini_file_mappings、global_models、management_tokens、 oauth_providers、proxy_nodes、quota、users、wallet 等模块 - aether-gateway 新增 api/auth/billing/control/middleware/scheduler/usage/ video_tasks/hooks/maintenance/model_fetch/provider_transport 等功能模块 - 重构 executor decision 和 gateway state 为模块目录结构 - 新增 gateway router、frontdoor 路由层及对应测试 - Python 侧 API 路由重构,新增 compat/support 模块 - 前端 Logo 组件更新及 Provider 管理页面调整
This commit is contained in:
@@ -0,0 +1,363 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::RwLock;
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use super::types::{
|
||||
StoredUserAuthRecord, StoredUserExportRow, StoredUserSummary, UserExportListQuery,
|
||||
UserExportSummary, UserReadRepository,
|
||||
};
|
||||
use crate::DataLayerError;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct InMemoryUserReadRepository {
|
||||
by_id: RwLock<BTreeMap<String, StoredUserSummary>>,
|
||||
auth_by_id: RwLock<BTreeMap<String, StoredUserAuthRecord>>,
|
||||
auth_by_identifier: RwLock<BTreeMap<String, String>>,
|
||||
export_rows: RwLock<Vec<StoredUserExportRow>>,
|
||||
}
|
||||
|
||||
impl InMemoryUserReadRepository {
|
||||
pub fn seed<I>(items: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = StoredUserSummary>,
|
||||
{
|
||||
let mut by_id = BTreeMap::new();
|
||||
for item in items {
|
||||
by_id.insert(item.id.clone(), item);
|
||||
}
|
||||
Self {
|
||||
by_id: RwLock::new(by_id),
|
||||
auth_by_id: RwLock::new(BTreeMap::new()),
|
||||
auth_by_identifier: RwLock::new(BTreeMap::new()),
|
||||
export_rows: RwLock::new(Vec::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn seed_auth_users<I>(items: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = StoredUserAuthRecord>,
|
||||
{
|
||||
let mut by_id = BTreeMap::new();
|
||||
let mut auth_by_id = BTreeMap::new();
|
||||
let mut auth_by_identifier = BTreeMap::new();
|
||||
for item in items {
|
||||
let summary = item
|
||||
.to_summary()
|
||||
.expect("in-memory auth user should convert to summary");
|
||||
by_id.insert(summary.id.clone(), summary);
|
||||
auth_by_identifier.insert(item.username.clone(), item.id.clone());
|
||||
if let Some(email) = item.email.as_ref() {
|
||||
auth_by_identifier.insert(email.clone(), item.id.clone());
|
||||
}
|
||||
auth_by_id.insert(item.id.clone(), item);
|
||||
}
|
||||
Self {
|
||||
by_id: RwLock::new(by_id),
|
||||
auth_by_id: RwLock::new(auth_by_id),
|
||||
auth_by_identifier: RwLock::new(auth_by_identifier),
|
||||
export_rows: RwLock::new(Vec::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn seed_export_users<I>(items: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = StoredUserExportRow>,
|
||||
{
|
||||
Self {
|
||||
by_id: RwLock::new(BTreeMap::new()),
|
||||
auth_by_id: RwLock::new(BTreeMap::new()),
|
||||
auth_by_identifier: RwLock::new(BTreeMap::new()),
|
||||
export_rows: RwLock::new(items.into_iter().collect()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_export_users<I>(self, items: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = StoredUserExportRow>,
|
||||
{
|
||||
let rows = items.into_iter().collect();
|
||||
*self.export_rows.write().expect("user repository lock") = rows;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl UserReadRepository for InMemoryUserReadRepository {
|
||||
async fn list_users_by_ids(
|
||||
&self,
|
||||
user_ids: &[String],
|
||||
) -> Result<Vec<StoredUserSummary>, DataLayerError> {
|
||||
let index = self.by_id.read().expect("user repository lock");
|
||||
Ok(user_ids
|
||||
.iter()
|
||||
.filter_map(|user_id| index.get(user_id).cloned())
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn list_non_admin_export_users(
|
||||
&self,
|
||||
) -> Result<Vec<StoredUserExportRow>, DataLayerError> {
|
||||
Ok(self
|
||||
.export_rows
|
||||
.read()
|
||||
.expect("user repository lock")
|
||||
.iter()
|
||||
.filter(|row| !row.role.eq_ignore_ascii_case("admin"))
|
||||
.cloned()
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn list_export_users(&self) -> Result<Vec<StoredUserExportRow>, DataLayerError> {
|
||||
Ok(self
|
||||
.export_rows
|
||||
.read()
|
||||
.expect("user repository lock")
|
||||
.clone())
|
||||
}
|
||||
|
||||
async fn list_export_users_page(
|
||||
&self,
|
||||
query: &UserExportListQuery,
|
||||
) -> Result<Vec<StoredUserExportRow>, DataLayerError> {
|
||||
let mut rows = self
|
||||
.export_rows
|
||||
.read()
|
||||
.expect("user repository lock")
|
||||
.clone();
|
||||
if let Some(role) = query.role.as_deref() {
|
||||
rows.retain(|row| row.role.eq_ignore_ascii_case(role));
|
||||
}
|
||||
if let Some(is_active) = query.is_active {
|
||||
rows.retain(|row| row.is_active == is_active);
|
||||
}
|
||||
Ok(rows
|
||||
.into_iter()
|
||||
.skip(query.skip)
|
||||
.take(query.limit)
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn summarize_export_users(&self) -> Result<UserExportSummary, DataLayerError> {
|
||||
let rows = self.export_rows.read().expect("user repository lock");
|
||||
Ok(UserExportSummary {
|
||||
total: rows.len() as u64,
|
||||
active: rows.iter().filter(|row| row.is_active).count() as u64,
|
||||
})
|
||||
}
|
||||
|
||||
async fn find_export_user_by_id(
|
||||
&self,
|
||||
user_id: &str,
|
||||
) -> Result<Option<StoredUserExportRow>, DataLayerError> {
|
||||
Ok(self
|
||||
.export_rows
|
||||
.read()
|
||||
.expect("user repository lock")
|
||||
.iter()
|
||||
.find(|row| row.id == user_id)
|
||||
.cloned())
|
||||
}
|
||||
|
||||
async fn find_user_auth_by_id(
|
||||
&self,
|
||||
user_id: &str,
|
||||
) -> Result<Option<StoredUserAuthRecord>, DataLayerError> {
|
||||
Ok(self
|
||||
.auth_by_id
|
||||
.read()
|
||||
.expect("user repository lock")
|
||||
.get(user_id)
|
||||
.cloned())
|
||||
}
|
||||
|
||||
async fn list_user_auth_by_ids(
|
||||
&self,
|
||||
user_ids: &[String],
|
||||
) -> Result<Vec<StoredUserAuthRecord>, DataLayerError> {
|
||||
let auth_by_id = self.auth_by_id.read().expect("user repository lock");
|
||||
Ok(user_ids
|
||||
.iter()
|
||||
.filter_map(|user_id| auth_by_id.get(user_id).cloned())
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn find_user_auth_by_identifier(
|
||||
&self,
|
||||
identifier: &str,
|
||||
) -> Result<Option<StoredUserAuthRecord>, DataLayerError> {
|
||||
let auth_by_identifier = self
|
||||
.auth_by_identifier
|
||||
.read()
|
||||
.expect("user repository lock");
|
||||
let Some(user_id) = auth_by_identifier.get(identifier) else {
|
||||
return Ok(None);
|
||||
};
|
||||
Ok(self
|
||||
.auth_by_id
|
||||
.read()
|
||||
.expect("user repository lock")
|
||||
.get(user_id)
|
||||
.cloned())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::repository::users::{UserExportListQuery, UserReadRepository};
|
||||
|
||||
#[tokio::test]
|
||||
async fn lists_seeded_users() {
|
||||
let user = StoredUserSummary::new(
|
||||
"user-1".to_string(),
|
||||
"alice".to_string(),
|
||||
Some("[email protected]".to_string()),
|
||||
"user".to_string(),
|
||||
true,
|
||||
false,
|
||||
)
|
||||
.expect("user should build");
|
||||
let repository = InMemoryUserReadRepository::seed(vec![user.clone()]);
|
||||
let rows = repository
|
||||
.list_users_by_ids(&["user-1".to_string()])
|
||||
.await
|
||||
.expect("lookup should succeed");
|
||||
assert_eq!(rows.len(), 1);
|
||||
assert_eq!(rows[0], user);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn lists_seeded_non_admin_export_users() {
|
||||
let user = StoredUserExportRow::new(
|
||||
"user-1".to_string(),
|
||||
Some("[email protected]".to_string()),
|
||||
true,
|
||||
"alice".to_string(),
|
||||
Some("hash".to_string()),
|
||||
"user".to_string(),
|
||||
"local".to_string(),
|
||||
Some(serde_json::json!(["openai"])),
|
||||
Some(serde_json::json!(["openai:chat"])),
|
||||
Some(serde_json::json!(["gpt-4.1"])),
|
||||
Some(60),
|
||||
Some(serde_json::json!({"gpt-4.1": {"cache_1h": true}})),
|
||||
true,
|
||||
)
|
||||
.expect("user export row should build");
|
||||
let repository = InMemoryUserReadRepository::seed_export_users(vec![user.clone()]);
|
||||
|
||||
let rows = repository
|
||||
.list_non_admin_export_users()
|
||||
.await
|
||||
.expect("export should succeed");
|
||||
|
||||
assert_eq!(rows, vec![user]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn finds_seeded_auth_user_by_id_and_identifier() {
|
||||
let user = StoredUserAuthRecord::new(
|
||||
"user-1".to_string(),
|
||||
Some("[email protected]".to_string()),
|
||||
true,
|
||||
"alice".to_string(),
|
||||
Some("hash".to_string()),
|
||||
"user".to_string(),
|
||||
"local".to_string(),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
true,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.expect("auth user should build");
|
||||
let repository = InMemoryUserReadRepository::seed_auth_users(vec![user.clone()]);
|
||||
|
||||
let by_id = repository
|
||||
.find_user_auth_by_id("user-1")
|
||||
.await
|
||||
.expect("lookup by id should succeed");
|
||||
let by_email = repository
|
||||
.find_user_auth_by_identifier("[email protected]")
|
||||
.await
|
||||
.expect("lookup by email should succeed");
|
||||
let by_username = repository
|
||||
.find_user_auth_by_identifier("alice")
|
||||
.await
|
||||
.expect("lookup by username should succeed");
|
||||
|
||||
assert_eq!(by_id, Some(user.clone()));
|
||||
assert_eq!(by_email, Some(user.clone()));
|
||||
assert_eq!(by_username, Some(user));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn paginates_export_users_in_memory() {
|
||||
let repository = InMemoryUserReadRepository::seed_export_users(vec![
|
||||
StoredUserExportRow::new(
|
||||
"user-1".to_string(),
|
||||
Some("[email protected]".to_string()),
|
||||
true,
|
||||
"alice".to_string(),
|
||||
Some("hash".to_string()),
|
||||
"user".to_string(),
|
||||
"local".to_string(),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(60),
|
||||
None,
|
||||
true,
|
||||
)
|
||||
.expect("user export row should build"),
|
||||
StoredUserExportRow::new(
|
||||
"user-2".to_string(),
|
||||
Some("[email protected]".to_string()),
|
||||
true,
|
||||
"bob".to_string(),
|
||||
Some("hash".to_string()),
|
||||
"admin".to_string(),
|
||||
"local".to_string(),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(30),
|
||||
None,
|
||||
true,
|
||||
)
|
||||
.expect("user export row should build"),
|
||||
StoredUserExportRow::new(
|
||||
"user-3".to_string(),
|
||||
Some("[email protected]".to_string()),
|
||||
true,
|
||||
"carol".to_string(),
|
||||
Some("hash".to_string()),
|
||||
"user".to_string(),
|
||||
"local".to_string(),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(10),
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.expect("user export row should build"),
|
||||
]);
|
||||
|
||||
let rows = repository
|
||||
.list_export_users_page(&UserExportListQuery {
|
||||
skip: 0,
|
||||
limit: 10,
|
||||
role: Some("user".to_string()),
|
||||
is_active: Some(true),
|
||||
})
|
||||
.await
|
||||
.expect("paged export should succeed");
|
||||
|
||||
assert_eq!(rows.len(), 1);
|
||||
assert_eq!(rows[0].id, "user-1");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user