mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 01:40:21 +08:00
feat(proxy): 实现代理节点批量升级回滚、隧道重定向跟随及远程配置管理
核心功能: - 新增代理节点批量升级回滚工作流,支持分批升级、健康探针、跳过/重试/取消等操作 - proxy 隧道流处理器支持 HTTP 重定向跟随(最多 10 跳),区分 307/308 可重播与不可重播请求体 - proxy 协议新增 follow_redirects / http1_only 字段,网关侧同步支持 - 新增代理节点远端配置变更接口(名称、允许端口、调度状态、升级目标等) - 新增代理节点注册/反注册/心跳的 Admin API,及节点过期清理维护任务 - gateway 隧道 owner-relay 支持流式代理大请求体,新增 5 MiB 默认限制 - 新增 ProxyNodeRegistrationMutation / ProxyNodeRemoteConfigMutation 数据类型 - proxy 配置新增重定向重播预算、心跳间隔等参数,TUI 安装向导同步更新 - 前端 ProxyNodes 页面新增批量升级操作面板及滚动进度展示
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::RwLock;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
@@ -13,6 +14,7 @@ use crate::DataLayerError;
|
||||
#[derive(Debug, Default)]
|
||||
pub struct InMemoryManagementTokenRepository {
|
||||
items: RwLock<Vec<StoredManagementTokenWithUser>>,
|
||||
hashes: RwLock<BTreeMap<String, String>>,
|
||||
}
|
||||
|
||||
impl InMemoryManagementTokenRepository {
|
||||
@@ -22,6 +24,18 @@ impl InMemoryManagementTokenRepository {
|
||||
{
|
||||
Self {
|
||||
items: RwLock::new(items.into_iter().collect()),
|
||||
hashes: RwLock::new(BTreeMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn seed_with_hashes<I, J>(items: I, hashes: J) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = StoredManagementTokenWithUser>,
|
||||
J: IntoIterator<Item = (String, String)>,
|
||||
{
|
||||
Self {
|
||||
items: RwLock::new(items.into_iter().collect()),
|
||||
hashes: RwLock::new(hashes.into_iter().collect()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +45,10 @@ impl InMemoryManagementTokenRepository {
|
||||
.ok()
|
||||
.map(|duration| duration.as_secs())
|
||||
}
|
||||
|
||||
fn remove_hash_for_token(hashes: &mut BTreeMap<String, String>, token_id: &str) {
|
||||
hashes.retain(|_, existing_token_id| existing_token_id != token_id);
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -77,6 +95,24 @@ impl ManagementTokenReadRepository for InMemoryManagementTokenRepository {
|
||||
let items = self.items.read().expect("management token repository lock");
|
||||
Ok(items.iter().find(|item| item.token.id == token_id).cloned())
|
||||
}
|
||||
|
||||
async fn get_management_token_with_user_by_hash(
|
||||
&self,
|
||||
token_hash: &str,
|
||||
) -> Result<Option<StoredManagementTokenWithUser>, DataLayerError> {
|
||||
let token_id = {
|
||||
let hashes = self
|
||||
.hashes
|
||||
.read()
|
||||
.expect("management token repository lock");
|
||||
hashes.get(token_hash).cloned()
|
||||
};
|
||||
let Some(token_id) = token_id else {
|
||||
return Ok(None);
|
||||
};
|
||||
let items = self.items.read().expect("management token repository lock");
|
||||
Ok(items.iter().find(|item| item.token.id == token_id).cloned())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -91,6 +127,10 @@ impl ManagementTokenWriteRepository for InMemoryManagementTokenRepository {
|
||||
.items
|
||||
.write()
|
||||
.expect("management token repository lock");
|
||||
let mut hashes = self
|
||||
.hashes
|
||||
.write()
|
||||
.expect("management token repository lock");
|
||||
if items
|
||||
.iter()
|
||||
.any(|item| item.token.user_id == record.user_id && item.token.name == record.name)
|
||||
@@ -118,6 +158,7 @@ impl ManagementTokenWriteRepository for InMemoryManagementTokenRepository {
|
||||
token.clone(),
|
||||
record.user.clone(),
|
||||
));
|
||||
hashes.insert(record.token_hash.clone(), record.id.clone());
|
||||
Ok(token)
|
||||
}
|
||||
|
||||
@@ -183,8 +224,13 @@ impl ManagementTokenWriteRepository for InMemoryManagementTokenRepository {
|
||||
.items
|
||||
.write()
|
||||
.expect("management token repository lock");
|
||||
let mut hashes = self
|
||||
.hashes
|
||||
.write()
|
||||
.expect("management token repository lock");
|
||||
let original_len = items.len();
|
||||
items.retain(|item| item.token.id != token_id);
|
||||
Self::remove_hash_for_token(&mut hashes, token_id);
|
||||
Ok(items.len() != original_len)
|
||||
}
|
||||
|
||||
@@ -215,16 +261,41 @@ impl ManagementTokenWriteRepository for InMemoryManagementTokenRepository {
|
||||
.items
|
||||
.write()
|
||||
.expect("management token repository lock");
|
||||
let mut hashes = self
|
||||
.hashes
|
||||
.write()
|
||||
.expect("management token repository lock");
|
||||
let Some(item) = items
|
||||
.iter_mut()
|
||||
.find(|item| item.token.id == mutation.token_id)
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
Self::remove_hash_for_token(&mut hashes, &mutation.token_id);
|
||||
hashes.insert(mutation.token_hash.clone(), mutation.token_id.clone());
|
||||
item.token.token_prefix = mutation.token_prefix.clone();
|
||||
item.token.updated_at_unix_secs = Self::now_unix_secs();
|
||||
Ok(Some(item.token.clone()))
|
||||
}
|
||||
|
||||
async fn record_management_token_usage(
|
||||
&self,
|
||||
token_id: &str,
|
||||
last_used_ip: Option<&str>,
|
||||
) -> Result<Option<StoredManagementToken>, DataLayerError> {
|
||||
let mut items = self
|
||||
.items
|
||||
.write()
|
||||
.expect("management token repository lock");
|
||||
let Some(item) = items.iter_mut().find(|item| item.token.id == token_id) else {
|
||||
return Ok(None);
|
||||
};
|
||||
item.token.last_used_at_unix_secs = Self::now_unix_secs();
|
||||
item.token.last_used_ip = last_used_ip.map(ToOwned::to_owned);
|
||||
item.token.usage_count = item.token.usage_count.saturating_add(1);
|
||||
item.token.updated_at_unix_secs = Self::now_unix_secs();
|
||||
Ok(Some(item.token.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -254,10 +325,16 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn lists_filters_and_mutates_management_tokens() {
|
||||
let repository = InMemoryManagementTokenRepository::seed(vec![
|
||||
sample_token("token-1", "user-1", true),
|
||||
sample_token("token-2", "user-2", false),
|
||||
]);
|
||||
let repository = InMemoryManagementTokenRepository::seed_with_hashes(
|
||||
vec![
|
||||
sample_token("token-1", "user-1", true),
|
||||
sample_token("token-2", "user-2", false),
|
||||
],
|
||||
vec![
|
||||
("hash-1".to_string(), "token-1".to_string()),
|
||||
("hash-2".to_string(), "token-2".to_string()),
|
||||
],
|
||||
);
|
||||
|
||||
let page = repository
|
||||
.list_management_tokens(&ManagementTokenListQuery {
|
||||
@@ -333,10 +410,31 @@ mod tests {
|
||||
.expect("token should exist");
|
||||
assert_eq!(regenerated.token_prefix.as_deref(), Some("ae_5678"));
|
||||
|
||||
let by_hash = repository
|
||||
.get_management_token_with_user_by_hash("hash-3b")
|
||||
.await
|
||||
.expect("lookup by hash should succeed")
|
||||
.expect("token should exist");
|
||||
assert_eq!(by_hash.token.id, "token-3");
|
||||
|
||||
let used = repository
|
||||
.record_management_token_usage("token-3", Some("127.0.0.1"))
|
||||
.await
|
||||
.expect("usage update should succeed")
|
||||
.expect("token should exist");
|
||||
assert_eq!(used.last_used_ip.as_deref(), Some("127.0.0.1"));
|
||||
assert_eq!(used.usage_count, 1);
|
||||
|
||||
let deleted = repository
|
||||
.delete_management_token("token-1")
|
||||
.await
|
||||
.expect("delete should succeed");
|
||||
assert!(deleted);
|
||||
|
||||
let deleted_by_hash = repository
|
||||
.get_management_token_with_user_by_hash("hash-1")
|
||||
.await
|
||||
.expect("hash lookup should succeed");
|
||||
assert!(deleted_by_hash.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,6 +69,31 @@ WHERE mt.id = $1
|
||||
LIMIT 1
|
||||
"#;
|
||||
|
||||
const GET_MANAGEMENT_TOKEN_WITH_USER_BY_HASH_SQL: &str = r#"
|
||||
SELECT
|
||||
mt.id,
|
||||
mt.user_id,
|
||||
mt.name,
|
||||
mt.description,
|
||||
mt.token_prefix,
|
||||
mt.allowed_ips,
|
||||
EXTRACT(EPOCH FROM mt.expires_at)::bigint AS expires_at_unix_secs,
|
||||
EXTRACT(EPOCH FROM mt.last_used_at)::bigint AS last_used_at_unix_secs,
|
||||
mt.last_used_ip,
|
||||
COALESCE(mt.usage_count, 0) AS usage_count,
|
||||
mt.is_active,
|
||||
EXTRACT(EPOCH FROM mt.created_at)::bigint AS created_at_unix_ms,
|
||||
EXTRACT(EPOCH FROM mt.updated_at)::bigint AS updated_at_unix_secs,
|
||||
u.id AS user_row_id,
|
||||
u.email AS user_email,
|
||||
u.username AS user_username,
|
||||
u.role::text AS user_role
|
||||
FROM management_tokens mt
|
||||
JOIN users u ON u.id = mt.user_id
|
||||
WHERE mt.token_hash = $1
|
||||
LIMIT 1
|
||||
"#;
|
||||
|
||||
const DELETE_MANAGEMENT_TOKEN_SQL: &str = r#"
|
||||
DELETE FROM management_tokens
|
||||
WHERE id = $1
|
||||
@@ -196,6 +221,29 @@ RETURNING
|
||||
EXTRACT(EPOCH FROM updated_at)::bigint AS updated_at_unix_secs
|
||||
"#;
|
||||
|
||||
const RECORD_MANAGEMENT_TOKEN_USAGE_SQL: &str = r#"
|
||||
UPDATE management_tokens
|
||||
SET last_used_at = NOW(),
|
||||
last_used_ip = $2,
|
||||
usage_count = COALESCE(usage_count, 0) + 1,
|
||||
updated_at = NOW()
|
||||
WHERE id = $1
|
||||
RETURNING
|
||||
id,
|
||||
user_id,
|
||||
name,
|
||||
description,
|
||||
token_prefix,
|
||||
allowed_ips,
|
||||
EXTRACT(EPOCH FROM expires_at)::bigint AS expires_at_unix_secs,
|
||||
EXTRACT(EPOCH FROM last_used_at)::bigint AS last_used_at_unix_secs,
|
||||
last_used_ip,
|
||||
COALESCE(usage_count, 0) AS usage_count,
|
||||
is_active,
|
||||
EXTRACT(EPOCH FROM created_at)::bigint AS created_at_unix_ms,
|
||||
EXTRACT(EPOCH FROM updated_at)::bigint AS updated_at_unix_secs
|
||||
"#;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SqlxManagementTokenRepository {
|
||||
pool: PgPool,
|
||||
@@ -250,6 +298,18 @@ impl ManagementTokenReadRepository for SqlxManagementTokenRepository {
|
||||
.map_postgres_err()?;
|
||||
row.as_ref().map(map_token_with_user_row).transpose()
|
||||
}
|
||||
|
||||
async fn get_management_token_with_user_by_hash(
|
||||
&self,
|
||||
token_hash: &str,
|
||||
) -> Result<Option<StoredManagementTokenWithUser>, DataLayerError> {
|
||||
let row = sqlx::query(GET_MANAGEMENT_TOKEN_WITH_USER_BY_HASH_SQL)
|
||||
.bind(token_hash)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
row.as_ref().map(map_token_with_user_row).transpose()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -341,6 +401,20 @@ impl ManagementTokenWriteRepository for SqlxManagementTokenRepository {
|
||||
.map_postgres_err()?;
|
||||
row.as_ref().map(map_token_row).transpose()
|
||||
}
|
||||
|
||||
async fn record_management_token_usage(
|
||||
&self,
|
||||
token_id: &str,
|
||||
last_used_ip: Option<&str>,
|
||||
) -> Result<Option<StoredManagementToken>, DataLayerError> {
|
||||
let row = sqlx::query(RECORD_MANAGEMENT_TOKEN_USAGE_SQL)
|
||||
.bind(token_id)
|
||||
.bind(last_used_ip)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
row.as_ref().map(map_token_row).transpose()
|
||||
}
|
||||
}
|
||||
|
||||
fn optional_unix_secs(value: Option<i64>) -> Option<u64> {
|
||||
|
||||
@@ -306,6 +306,11 @@ pub trait ManagementTokenReadRepository: Send + Sync {
|
||||
&self,
|
||||
token_id: &str,
|
||||
) -> Result<Option<StoredManagementTokenWithUser>, crate::DataLayerError>;
|
||||
|
||||
async fn get_management_token_with_user_by_hash(
|
||||
&self,
|
||||
token_hash: &str,
|
||||
) -> Result<Option<StoredManagementTokenWithUser>, crate::DataLayerError>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -332,4 +337,10 @@ pub trait ManagementTokenWriteRepository: Send + Sync {
|
||||
&self,
|
||||
mutation: &RegenerateManagementTokenSecret,
|
||||
) -> Result<Option<StoredManagementToken>, crate::DataLayerError>;
|
||||
|
||||
async fn record_management_token_usage(
|
||||
&self,
|
||||
token_id: &str,
|
||||
last_used_ip: Option<&str>,
|
||||
) -> Result<Option<StoredManagementToken>, crate::DataLayerError>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user