mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 17:30:23 +08:00
refactor(gateway): 统一 AETHER_GATEWAY_BIND 为 APP_PORT,新增 API Key 前缀配置和启动自举管理员
- 绑定地址固定 0.0.0.0,仅通过 APP_PORT 控制端口,简化 CLI/Docker/systemd/dev.sh/前端代理全链路 - 新增 API_KEY_PREFIX 环境变量,抽取 handlers/shared/api_keys.rs 消除 admin/public 重复逻辑 - 新增 bootstrap_admin.rs,启动时通过 ADMIN_* 环境变量在无管理员时自动创建首个本地管理员 - 前端密码输入改用 type=password,API Key 占位符改为动态前缀 - 删除过时的 pyproject.toml/uv.lock 和旧部署文档 - 更新 .env.example/README 反映新配置项
This commit is contained in:
@@ -2,6 +2,9 @@ use crate::handlers::admin::request::AdminAppState;
|
||||
use crate::handlers::admin::shared::{
|
||||
attach_admin_audit_response, decrypt_catalog_secret_with_fallbacks,
|
||||
};
|
||||
use crate::handlers::shared::{
|
||||
api_key_placeholder_display, generate_gateway_api_key_plaintext, masked_gateway_api_key_display,
|
||||
};
|
||||
use axum::{body::Body, response::Response};
|
||||
use serde_json::json;
|
||||
use std::collections::BTreeSet;
|
||||
@@ -17,20 +20,13 @@ pub(crate) fn masked_user_api_key_display(
|
||||
ciphertext: Option<&str>,
|
||||
) -> String {
|
||||
let Some(ciphertext) = ciphertext.map(str::trim).filter(|value| !value.is_empty()) else {
|
||||
return "sk-****".to_string();
|
||||
return api_key_placeholder_display();
|
||||
};
|
||||
let Some(full_key) = decrypt_catalog_secret_with_fallbacks(state.encryption_key(), ciphertext)
|
||||
else {
|
||||
return "sk-****".to_string();
|
||||
return api_key_placeholder_display();
|
||||
};
|
||||
let prefix_len = full_key.len().min(10);
|
||||
let prefix = &full_key[..prefix_len];
|
||||
let suffix = if full_key.len() >= 4 {
|
||||
&full_key[full_key.len() - 4..]
|
||||
} else {
|
||||
""
|
||||
};
|
||||
format!("{prefix}...{suffix}")
|
||||
masked_gateway_api_key_display(Some(full_key.as_str()))
|
||||
}
|
||||
|
||||
pub(super) fn build_admin_user_api_key_detail_payload(
|
||||
@@ -89,9 +85,7 @@ pub(super) fn normalize_admin_api_key_providers(
|
||||
}
|
||||
|
||||
pub(crate) fn generate_admin_user_api_key_plaintext() -> String {
|
||||
let first = uuid::Uuid::new_v4().simple().to_string();
|
||||
let second = uuid::Uuid::new_v4().simple().to_string();
|
||||
format!("sk-{}{}", first, &second[..16])
|
||||
generate_gateway_api_key_plaintext()
|
||||
}
|
||||
|
||||
pub(crate) fn hash_admin_user_api_key(value: &str) -> String {
|
||||
|
||||
@@ -9,6 +9,10 @@ use axum::{
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::handlers::shared::{
|
||||
api_key_placeholder_display, generate_gateway_api_key_plaintext, masked_gateway_api_key_display,
|
||||
};
|
||||
|
||||
use super::{
|
||||
build_auth_error_response, decrypt_catalog_secret_with_fallbacks,
|
||||
encrypt_catalog_secret_with_fallbacks, format_users_me_optional_unix_secs_iso8601,
|
||||
@@ -111,20 +115,13 @@ pub(super) fn users_me_api_key_capabilities_path_matches(request_path: &str) ->
|
||||
|
||||
fn users_me_masked_api_key_display(state: &AppState, ciphertext: Option<&str>) -> String {
|
||||
let Some(ciphertext) = ciphertext.map(str::trim).filter(|value| !value.is_empty()) else {
|
||||
return "sk-****".to_string();
|
||||
return api_key_placeholder_display();
|
||||
};
|
||||
let Some(full_key) = decrypt_catalog_secret_with_fallbacks(state.encryption_key(), ciphertext)
|
||||
else {
|
||||
return "sk-****".to_string();
|
||||
return api_key_placeholder_display();
|
||||
};
|
||||
let prefix_len = full_key.len().min(10);
|
||||
let prefix = &full_key[..prefix_len];
|
||||
let suffix = if full_key.len() >= 4 {
|
||||
&full_key[full_key.len() - 4..]
|
||||
} else {
|
||||
""
|
||||
};
|
||||
format!("{prefix}...{suffix}")
|
||||
masked_gateway_api_key_display(Some(full_key.as_str()))
|
||||
}
|
||||
|
||||
fn build_users_me_api_key_writer_unavailable_response() -> Response<Body> {
|
||||
@@ -185,9 +182,7 @@ fn normalize_users_me_required_api_key_name(value: &str) -> Result<String, Strin
|
||||
}
|
||||
|
||||
fn generate_users_me_api_key_plaintext() -> String {
|
||||
let first = uuid::Uuid::new_v4().simple().to_string();
|
||||
let second = uuid::Uuid::new_v4().simple().to_string();
|
||||
format!("sk-{}{}", first, &second[..16])
|
||||
generate_gateway_api_key_plaintext()
|
||||
}
|
||||
|
||||
fn hash_users_me_api_key(value: &str) -> String {
|
||||
|
||||
108
apps/aether-gateway/src/handlers/shared/api_keys.rs
Normal file
108
apps/aether-gateway/src/handlers/shared/api_keys.rs
Normal file
@@ -0,0 +1,108 @@
|
||||
const DEFAULT_API_KEY_PREFIX: &str = "sk";
|
||||
|
||||
fn configured_api_key_prefix_from_lookup<F>(lookup: F) -> String
|
||||
where
|
||||
F: Fn(&str) -> Option<String>,
|
||||
{
|
||||
lookup("API_KEY_PREFIX")
|
||||
.as_deref()
|
||||
.map(normalize_api_key_prefix)
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or_else(|| DEFAULT_API_KEY_PREFIX.to_string())
|
||||
}
|
||||
|
||||
fn normalize_api_key_prefix(value: &str) -> String {
|
||||
let normalized = value.trim().trim_end_matches('-').trim();
|
||||
if normalized.is_empty() {
|
||||
return DEFAULT_API_KEY_PREFIX.to_string();
|
||||
}
|
||||
normalized.to_string()
|
||||
}
|
||||
|
||||
fn api_key_placeholder_display_with_prefix(prefix: &str) -> String {
|
||||
format!("{prefix}-****")
|
||||
}
|
||||
|
||||
fn generate_gateway_api_key_plaintext_with_prefix(prefix: &str) -> String {
|
||||
let first = uuid::Uuid::new_v4().simple().to_string();
|
||||
let second = uuid::Uuid::new_v4().simple().to_string();
|
||||
format!("{prefix}-{}{}", first, &second[..16])
|
||||
}
|
||||
|
||||
pub(crate) fn configured_api_key_prefix() -> String {
|
||||
configured_api_key_prefix_from_lookup(|key| {
|
||||
std::env::var(key)
|
||||
.ok()
|
||||
.map(|value| value.trim().to_string())
|
||||
.filter(|value| !value.is_empty())
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn api_key_placeholder_display() -> String {
|
||||
api_key_placeholder_display_with_prefix(&configured_api_key_prefix())
|
||||
}
|
||||
|
||||
pub(crate) fn generate_gateway_api_key_plaintext() -> String {
|
||||
generate_gateway_api_key_plaintext_with_prefix(&configured_api_key_prefix())
|
||||
}
|
||||
|
||||
pub(crate) fn masked_gateway_api_key_display(full_key: Option<&str>) -> String {
|
||||
let Some(full_key) = full_key.map(str::trim).filter(|value| !value.is_empty()) else {
|
||||
return api_key_placeholder_display();
|
||||
};
|
||||
let prefix_len = full_key.len().min(10);
|
||||
let prefix = &full_key[..prefix_len];
|
||||
let suffix = if full_key.len() >= 4 {
|
||||
&full_key[full_key.len().saturating_sub(4)..]
|
||||
} else {
|
||||
""
|
||||
};
|
||||
format!("{prefix}...{suffix}")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
api_key_placeholder_display_with_prefix, configured_api_key_prefix_from_lookup,
|
||||
generate_gateway_api_key_plaintext_with_prefix, masked_gateway_api_key_display,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn defaults_api_key_prefix_to_sk() {
|
||||
assert_eq!(
|
||||
configured_api_key_prefix_from_lookup(|_| None),
|
||||
"sk".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalizes_api_key_prefix_whitespace_and_trailing_dash() {
|
||||
assert_eq!(
|
||||
configured_api_key_prefix_from_lookup(|_| Some(" ak- ".to_string())),
|
||||
"ak".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generates_plaintext_api_key_with_configured_prefix() {
|
||||
let value = generate_gateway_api_key_plaintext_with_prefix("ak");
|
||||
assert!(value.starts_with("ak-"));
|
||||
assert_eq!(value.len(), 3 + 32 + 16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn uses_configured_prefix_in_placeholder_display() {
|
||||
assert_eq!(
|
||||
api_key_placeholder_display_with_prefix("ak"),
|
||||
"ak-****".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn masks_plaintext_api_key_without_changing_prefix() {
|
||||
assert_eq!(
|
||||
masked_gateway_api_key_display(Some("ak-1234567890abcdef")),
|
||||
"ak-1234567...cdef".to_string()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
mod admin_proxy;
|
||||
mod api_keys;
|
||||
mod catalog;
|
||||
mod email_templates;
|
||||
mod external_models;
|
||||
@@ -12,6 +13,10 @@ pub(crate) use self::admin_proxy::{
|
||||
attach_admin_audit_response, build_admin_proxy_auth_required_response,
|
||||
build_unhandled_admin_proxy_response,
|
||||
};
|
||||
pub(crate) use self::api_keys::{
|
||||
api_key_placeholder_display, configured_api_key_prefix, generate_gateway_api_key_plaintext,
|
||||
masked_gateway_api_key_display,
|
||||
};
|
||||
pub(crate) use self::catalog::{
|
||||
build_admin_provider_key_response, decrypt_catalog_secret_with_fallbacks,
|
||||
default_provider_key_status_snapshot, effective_catalog_encryption_key,
|
||||
|
||||
Reference in New Issue
Block a user