fix(auth): OpenAI 端 key 也走 DB,setup 是单一配置入口

修 M2/M3 遗漏:OPENAI_TTS_API_KEY (admin + 合成鉴权) 此前只能从 env
读,setup 前端没收集。装完用户还要回去设 env,体验断,违反
'setup 是单一配置源' 承诺。

改动:
- setting/config.go: LoadRuntimeConfig 顺便把 auth_key 灌到
  setting.Auth.APIKeys。优先级 DB > env(env 仅作 fallback,首次启
  动无 DB 时仍可用,向后兼容)。
- controller/setup.go: validateSetupSettings 加 auth_key 为必填,
  写 DB 后立即 setting.Auth.APIKeys = [auth_key](本进程内立刻生效)。
- controller/settings.go: GET /api/settings 返 auth_key (打码) +
  auth_key_set;新增 SettingsAuthKeyHandler (PUT /api/settings/auth-key),
  改完单独刷新 setting.Auth.APIKeys(避免被自踢,只下次请求生效)。
- router/router.go: 挂 /api/settings/auth-key 路由。
- router/setup.html: 加 OpenAI 鉴权 Key 字段(密码框,带提示);
  表单 default + submit 都加 auth_key。
- router/admin.html: 设置 tab 拆两张卡 — 鉴权 Key(独立保存按钮,
  改完会自踢当前会话)+ 火山 TTS 凭证;vue setup 加 authKeyInput +
  saveAuthKey 函数。
- main.go: 把 LoadRuntimeConfig(st) 提到 LogStartupSummary 之前,
  让启动日志反映真实运行时状态(DB 已加载的 key 数量)而不是
  过时的 env 状态。

未 push(等用户当前 M3 测完一起或单独)
This commit is contained in:
sun
2026-08-29 23:18:51 +08:00
parent 9db038dd05
commit a4ea53bfa0
7 changed files with 137 additions and 17 deletions
+48
View File
@@ -17,6 +17,8 @@ import (
type SettingsResponse struct {
APIKey string `json:"api_key"` // 打码形式,例如 S_G8****naJ1
APIKeySet bool `json:"api_key_set"` // 是否已设置(用于前端判断要不要提示必填)
AuthKey string `json:"auth_key"` // 鉴权 key 打码(客户端访问 + admin 登录用)
AuthKeySet bool `json:"auth_key_set"`
DefaultResourceID string `json:"default_resource_id"`
DefaultSpeaker string `json:"default_speaker"`
DefaultFormat string `json:"default_format"`
@@ -50,6 +52,8 @@ func SettingsGetHandler(w http.ResponseWriter, r *http.Request) {
resp := SettingsResponse{
APIKey: maskAPIKeyField(all["api_key"]),
APIKeySet: all["api_key"] != "",
AuthKey: maskAPIKeyField(all["auth_key"]),
AuthKeySet: all["auth_key"] != "",
DefaultResourceID: all["default_resource_id"],
DefaultSpeaker: all["default_speaker"],
DefaultFormat: all["default_format"],
@@ -243,6 +247,50 @@ func SettingsAPIKeyHandler(w http.ResponseWriter, r *http.Request) {
_ = json.NewEncoder(w).Encode(map[string]any{"ok": true})
}
// SettingsAuthKeyRequest 是 PUT /api/settings/auth-key 的 body。
// auth_key 是 admin 鉴权和 /v1/audio/speech 鉴权用的 key(火山上游 key 是 api_key,这是两套)。
type SettingsAuthKeyRequest struct {
AuthKey string `json:"auth_key"`
}
// SettingsAuthKeyHandler PUT /api/settings/auth-key
// 鉴权: RequireAdmin。改完立即更新 setting.Auth.APIKeys(进程内生效),
// 下一个请求就用新 key — admin 自己改完要等下一次请求才能验证(避免改完立刻自踢)。
func SettingsAuthKeyHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPut {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
s := GetAdminStore()
if s == nil {
middleware.SendJSONError(w, http.StatusServiceUnavailable, "database not ready", "configuration_error", "db_not_ready")
return
}
r.Body = http.MaxBytesReader(w, r.Body, 1<<10)
var body SettingsAuthKeyRequest
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
middleware.SendJSONError(w, http.StatusBadRequest, "invalid JSON body", "invalid_request_error", "bad_request")
return
}
key := trimAll(body.AuthKey)
if key == "" {
middleware.SendJSONError(w, http.StatusBadRequest, "auth_key cannot be empty", "invalid_request_error", "missing_field")
return
}
if err := s.SettingsSet("auth_key", key); err != nil {
log.Printf("[settings] auth-key set: %v", err)
middleware.SendJSONError(w, http.StatusInternalServerError, "write auth_key failed", "server_error", "db_write_failed")
return
}
// 立即生效:不重新 LoadRuntimeConfig(那会覆盖其它字段),
// 只单独刷新 Auth.APIKeys
setting.Auth.APIKeys = []string{key}
log.Printf("[settings] auth_key updated, runtime active (next request uses new key)")
w.Header().Set("Content-Type", "application/json; charset=utf-8")
_ = json.NewEncoder(w).Encode(map[string]any{"ok": true})
}
// maskAPIKeyField 复用 setting 包的打码风格(前 4 后 4 中间 ****)。
// 单独导出版本避免从 setting 包拉整个 APIKeyMask 之类的工具(那个是 unexported)。
func maskAPIKeyField(s string) string {