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:
fawney19
2026-04-11 17:39:02 +08:00
parent a570a77cca
commit 801e16c988
30 changed files with 867 additions and 3929 deletions

View File

@@ -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 {

View File

@@ -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 {

View 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()
);
}
}

View File

@@ -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,