fix: persist runtime proxy token

This commit is contained in:
boos
2026-06-06 17:11:51 +08:00
parent b3eb86d5ea
commit f026582008
17 changed files with 276 additions and 27 deletions
+115 -2
View File
@@ -966,6 +966,8 @@ async fn change_auth_token() -> Json<Value> {
async fn runtime_settings(State(state): State<AppState>) -> Json<Value> {
Json(json!({
"proxyTokenMasked": state.runtime_auth.proxy_token_masked(),
"proxyTokenSource": if state.runtime_auth.using_default_proxy_token() { "config" } else { "runtime" },
"server": {
"host": state.config.server.host,
"port": state.config.server.port,
@@ -989,8 +991,34 @@ async fn runtime_settings(State(state): State<AppState>) -> Json<Value> {
}))
}
async fn update_runtime_settings() -> Json<Value> {
Json(json!({ "success": true }))
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct RuntimeSettingsUpdateRequest {
proxy_token: Option<String>,
}
async fn update_runtime_settings(
State(state): State<AppState>,
Json(req): Json<RuntimeSettingsUpdateRequest>,
) -> AppResult<Json<Value>> {
if let Some(proxy_token) = req.proxy_token {
let updated = state
.runtime_auth
.set_proxy_token(&state.db, proxy_token)
.await
.map_err(|err| AppError::BadRequest(err.to_string()))?;
return Ok(Json(json!({
"success": true,
"proxyTokenMasked": crate::services::runtime_auth::mask_token(&updated),
"proxyTokenSource": if state.runtime_auth.using_default_proxy_token() { "config" } else { "runtime" },
})));
}
Ok(Json(json!({
"success": true,
"proxyTokenMasked": state.runtime_auth.proxy_token_masked(),
"proxyTokenSource": if state.runtime_auth.using_default_proxy_token() { "config" } else { "runtime" },
})))
}
async fn brand_list() -> Json<Value> {
@@ -1166,3 +1194,88 @@ async fn clear_usage_data(State(state): State<AppState>) -> AppResult<Json<Value
async fn test_notification() -> Json<Value> {
Json(json!({ "success": true, "message": "notification endpoint available" }))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
config::{AppConfig, AuthConfig, DbConfig, ProxyConfig, ServerConfig},
services::{builtin_keys::BuiltinKeys, runtime_auth::RuntimeAuthSettings},
};
use sqlx::sqlite::SqlitePoolOptions;
async fn test_state() -> AppState {
let pool = SqlitePoolOptions::new()
.max_connections(1)
.connect("sqlite::memory:")
.await
.unwrap();
sqlx::query("CREATE TABLE settings (key TEXT PRIMARY KEY NOT NULL, value TEXT)")
.execute(&pool)
.await
.unwrap();
let runtime_auth = RuntimeAuthSettings::load(&pool, "sk-default-token".to_string())
.await
.unwrap();
AppState::new(
AppConfig {
server: ServerConfig {
host: "127.0.0.1".into(),
port: 0,
log_level: "info".into(),
cors_origins: vec![],
},
db: DbConfig {
url: "sqlite::memory:".into(),
max_connections: 1,
ssl: false,
runtime_db_url: None,
runtime_db_dialect: None,
runtime_db_ssl: None,
},
auth: AuthConfig {
jwt_secret: "secret".into(),
jwt_expiry_hours: 1,
auth_token: "auth".into(),
proxy_token: "sk-default-token".into(),
},
proxy: ProxyConfig {
upstream_timeout_secs: 30,
first_byte_timeout_secs: 30,
max_retries: 3,
proxy_log_retention_days: 7,
template_proxy_url: None,
},
comfyui: Default::default(),
},
pool,
BuiltinKeys {
comfyui_agent: (1, "sk-comfyui-test".into()),
},
runtime_auth,
)
}
#[tokio::test]
async fn runtime_settings_reports_and_updates_proxy_token() {
let state = test_state().await;
let Json(initial) = runtime_settings(State(state.clone())).await;
assert_eq!(initial["proxyTokenMasked"], "••••oken");
assert_eq!(initial["proxyTokenSource"], "config");
let Json(updated) = update_runtime_settings(
State(state.clone()),
Json(RuntimeSettingsUpdateRequest {
proxy_token: Some("sk-new-runtime-token".into()),
}),
)
.await
.unwrap();
assert_eq!(updated["success"], true);
assert_eq!(updated["proxyTokenMasked"], "••••oken");
assert_eq!(updated["proxyTokenSource"], "runtime");
assert_eq!(state.runtime_proxy_token(), "sk-new-runtime-token");
}
}
+1
View File
@@ -417,6 +417,7 @@ mod tests {
BuiltinKeys {
comfyui_agent: (1, "sk-comfyui-abcdef0123456789".into()),
},
crate::services::runtime_auth::RuntimeAuthSettings::from_token_for_test("proxy"),
)
}
+14 -2
View File
@@ -217,6 +217,7 @@ const HIDDEN_SETTINGS_PREFIX: &str = "builtin_key.";
fn is_hidden_setting_key(key: &str) -> bool {
key.starts_with(HIDDEN_SETTINGS_PREFIX)
|| key == crate::services::runtime_auth::PROXY_TOKEN_SETTING_KEY
}
/// GET /api/settings — return all settings as a key-value map.
@@ -313,6 +314,12 @@ mod tests {
.execute(&pool)
.await
.unwrap();
sqlx::query(
"INSERT INTO settings (key, value) VALUES ('runtime.auth.proxy_token', 'sk-runtime-secret')",
)
.execute(&pool)
.await
.unwrap();
AppState::new(
AppConfig {
@@ -349,11 +356,12 @@ mod tests {
BuiltinKeys {
comfyui_agent: (1, "sk-comfyui-secret".into()),
},
crate::services::runtime_auth::RuntimeAuthSettings::from_token_for_test("proxy"),
)
}
/// 5.2 — `GET /api/settings` returns regular settings but never any
/// `builtin_key.*` rows.
/// 5.2 — `GET /api/settings` returns regular settings but never protected
/// secret rows.
#[tokio::test]
async fn list_settings_filters_builtin_key_prefix() {
let state = settings_state().await;
@@ -364,6 +372,10 @@ mod tests {
!data.contains_key("builtin_key.comfyui_agent.secret"),
"builtin_key.* rows must not appear in /api/settings response"
);
assert!(
!data.contains_key("runtime.auth.proxy_token"),
"runtime proxy token must not appear in /api/settings response"
);
}
/// 5.3 — `PUT /api/settings` rejects writes targeting `builtin_key.*`
+2 -1
View File
@@ -49,8 +49,9 @@ async fn check_auth(state: &AppState, headers: &HeaderMap) -> Result<(), AppErro
// Downstream API key
if auth.starts_with("sk-") {
let proxy_token = state.runtime_proxy_token();
let result =
authorize_downstream_token(auth, &state.db, Some(&state.config.auth.proxy_token))
authorize_downstream_token(auth, &state.db, Some(&proxy_token))
.await
.map_err(AppError::Unauthorized)?;
+5 -3
View File
@@ -243,11 +243,12 @@ async fn resolve_agent_auth(
)
})?;
if token == state.config.auth.auth_token || token == state.config.auth.proxy_token {
let proxy_token = state.runtime_proxy_token();
if token == state.config.auth.auth_token || token == proxy_token {
return Ok(AgentAuthContext {
user_id: None,
role: "admin".to_string(),
outbound_key: state.config.auth.proxy_token.clone(),
outbound_key: proxy_token,
key_name: "global-proxy".to_string(),
});
}
@@ -282,7 +283,7 @@ async fn resolve_agent_auth(
Err(_) if claims.role == "admin" => Ok(AgentAuthContext {
user_id: Some(user_id),
role: claims.role,
outbound_key: state.config.auth.proxy_token.clone(),
outbound_key: state.runtime_proxy_token(),
key_name: "global-proxy".to_string(),
}),
Err(message) => Err((
@@ -1245,6 +1246,7 @@ mod tests {
BuiltinKeys {
comfyui_agent: (1, "sk-comfyui-test".into()),
},
crate::services::runtime_auth::RuntimeAuthSettings::from_token_for_test("proxy"),
)
}
+4 -3
View File
@@ -104,11 +104,12 @@ async fn resolve_auth(headers: &HeaderMap, state: &AppState) -> Result<DigitalHu
let token = extract_bearer_token(headers)
.ok_or_else(|| AppError::Unauthorized("Missing Authorization header".into()))?;
if token == state.config.auth.auth_token || token == state.config.auth.proxy_token {
let proxy_token = state.runtime_proxy_token();
if token == state.config.auth.auth_token || token == proxy_token {
return Ok(DigitalHumanAuth {
user_id: None,
role: "admin".to_string(),
outbound_key: state.config.auth.proxy_token.clone(),
outbound_key: proxy_token,
});
}
@@ -131,7 +132,7 @@ async fn resolve_auth(headers: &HeaderMap, state: &AppState) -> Result<DigitalHu
Err(_) if claims.role == "admin" => Ok(DigitalHumanAuth {
user_id: Some(user_id),
role: claims.role,
outbound_key: state.config.auth.proxy_token.clone(),
outbound_key: state.runtime_proxy_token(),
}),
Err(message) => Err(AppError::Forbidden(message)),
}
+4 -3
View File
@@ -88,11 +88,12 @@ async fn resolve_auth(headers: &HeaderMap, state: &AppState) -> Result<MoneyPrin
let token = extract_bearer_token(headers)
.ok_or_else(|| AppError::Unauthorized("Missing Authorization header".into()))?;
if token == state.config.auth.auth_token || token == state.config.auth.proxy_token {
let proxy_token = state.runtime_proxy_token();
if token == state.config.auth.auth_token || token == proxy_token {
return Ok(MoneyPrinterAuth {
user_id: None,
role: "admin".to_string(),
outbound_key: state.config.auth.proxy_token.clone(),
outbound_key: proxy_token,
key_name: "global-proxy".to_string(),
});
}
@@ -117,7 +118,7 @@ async fn resolve_auth(headers: &HeaderMap, state: &AppState) -> Result<MoneyPrin
Err(_) if claims.role == "admin" => Ok(MoneyPrinterAuth {
user_id: Some(user_id),
role: claims.role,
outbound_key: state.config.auth.proxy_token.clone(),
outbound_key: state.runtime_proxy_token(),
key_name: "global-proxy".to_string(),
}),
Err(message) => Err(AppError::Forbidden(message)),
+4 -3
View File
@@ -123,11 +123,12 @@ async fn resolve_picture_agent_auth(
let token = extract_bearer_token(headers)
.ok_or_else(|| AppError::Unauthorized("Missing Authorization header".into()))?;
if token == state.config.auth.auth_token || token == state.config.auth.proxy_token {
let proxy_token = state.runtime_proxy_token();
if token == state.config.auth.auth_token || token == proxy_token {
return Ok(PictureAgentAuth {
user_id: None,
role: "admin".to_string(),
outbound_key: state.config.auth.proxy_token.clone(),
outbound_key: proxy_token,
key_name: "global-proxy".to_string(),
});
}
@@ -153,7 +154,7 @@ async fn resolve_picture_agent_auth(
Err(_) if claims.role == "admin" => Ok(PictureAgentAuth {
user_id: Some(user_id),
role: claims.role,
outbound_key: state.config.auth.proxy_token.clone(),
outbound_key: state.runtime_proxy_token(),
key_name: "global-proxy".to_string(),
}),
Err(message) => Err(AppError::Forbidden(message)),
+3 -1
View File
@@ -610,7 +610,8 @@ async fn check_auth(state: &AppState, headers: &HeaderMap) -> Result<Option<i64>
.and_then(|v| v.split_whitespace().next())
.ok_or_else(|| AppError::Unauthorized("Missing Authorization header".into()))?;
let result = authorize_downstream_token(auth, &state.db, Some(&state.config.auth.proxy_token))
let proxy_token = state.runtime_proxy_token();
let result = authorize_downstream_token(auth, &state.db, Some(&proxy_token))
.await
.map_err(AppError::Unauthorized)?;
@@ -1693,6 +1694,7 @@ mod tests {
crate::services::builtin_keys::BuiltinKeys {
comfyui_agent: (0, "sk-comfyui-test".to_string()),
},
crate::services::runtime_auth::RuntimeAuthSettings::from_token_for_test("proxy"),
)
}
+3 -3
View File
@@ -273,7 +273,7 @@ async fn call_llm(state: &AppState, prompt: &str, system: &str) -> AppResult<Str
let resp = client
.post(url)
.bearer_auth(&state.config.auth.proxy_token)
.bearer_auth(state.runtime_proxy_token())
.json(&json!({
"model": &state.config.comfyui.agent_model,
"messages": [
@@ -319,7 +319,7 @@ async fn call_image_gen(state: &AppState, prompt: &str) -> AppResult<String> {
let resp = client
.post(url)
.bearer_auth(&state.config.auth.proxy_token)
.bearer_auth(state.runtime_proxy_token())
.json(&json!({
"model": "gpt-image-2",
"prompt": prompt,
@@ -362,7 +362,7 @@ async fn call_tts(state: &AppState, text: &str, voice: &str) -> AppResult<Vec<u8
let resp = client
.post(url)
.bearer_auth(&state.config.auth.proxy_token)
.bearer_auth(state.runtime_proxy_token())
.json(&json!({
"model": "tts-1",
"input": text,
+2 -1
View File
@@ -307,7 +307,8 @@ fn caller_from_headers(state: &AppState, headers: &HeaderMap) -> AppResult<Workf
.filter(|value| !value.is_empty())
.ok_or_else(|| AppError::Unauthorized("Missing authorization token".into()))?;
if token == state.config.auth.auth_token || token == state.config.auth.proxy_token {
let proxy_token = state.runtime_proxy_token();
if token == state.config.auth.auth_token || token == proxy_token {
return Ok(WorkflowCaller::Admin);
}
+2 -1
View File
@@ -91,7 +91,8 @@ fn resolve_caller(
let token = bearer(headers)
.ok_or_else(|| AppError::Unauthorized("Missing Authorization header".into()))?;
if token == state.config.auth.auth_token || token == state.config.auth.proxy_token {
let proxy_token = state.runtime_proxy_token();
if token == state.config.auth.auth_token || token == proxy_token {
return Ok(WorkspaceCaller::Admin);
}