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
+35 -4
View File
@@ -228,8 +228,21 @@
<!-- Settings -->
<div v-if="tab==='settings'">
<div class="card">
<div class="card-title">火山 TTS 凭证</div>
<div class="card-desc" style="color: var(--text-dim); font-size: 12px; margin-bottom: 16px;">改完点保存,立即生效,无需重启服务。</div>
<div class="card-title">鉴权 Key (登录 + 客户端调用)</div>
<div class="card-desc" style="color: var(--text-dim); font-size: 12px; margin-bottom: 16px;">客户端用这个 Key 调 <code>/v1/audio/speech</code>,也是登录 <code>/admin</code> 的密码。改完下一次请求立即生效(改完会被当前会话自踢,需重新登录)。</div>
<div class="field">
<label>当前 Key (打码: <span style="color: var(--accent); font-family: monospace;">{{ settings.auth_key || '(未设置)' }}</span>)</label>
<div style="display: flex; gap: 8px;">
<input type="password" v-model="authKeyInput" :placeholder="settings.auth_key_set ? '不改留空' : '输入新 Key'" style="flex: 1;">
<button class="btn btn-primary" @click="saveAuthKey" :disabled="!authKeyInput">更新 Key</button>
</div>
<div class="hint">独立端点 /api/settings/auth-key。改了之后,你的浏览器 401 自动跳回登录页,用新 Key 重登。</div>
</div>
</div>
<div class="card">
<div class="card-title">火山 TTS 上游凭证</div>
<div class="card-desc" style="color: var(--text-dim); font-size: 12px; margin-bottom: 16px;">调火山 v3 上游用的凭证。改完点保存,立即生效,无需重启服务。</div>
<div class="field">
<label>API Key (打码显示: <span style="color: var(--accent); font-family: monospace;">{{ settings.api_key || '(未设置)' }}</span>)</label>
@@ -331,6 +344,7 @@
const settings = ref({});
const settingsForm = ref({ default_resource_id: '', default_speaker: '', default_format: 'mp3', sample_rate: 24000, model: '' });
const apiKeyInput = ref('');
const authKeyInput = ref('');
const savingSettings = ref(false);
const settingsErr = ref('');
const settingsOk = ref(false);
@@ -422,6 +436,23 @@
settingsErr.value = e.response?.data?.error?.message || e.message;
}
};
const saveAuthKey = async () => {
settingsErr.value = ''; settingsOk.value = false;
if (!authKeyInput.value) { settingsErr.value = 'Auth Key 不能为空'; return; }
try {
await http.put('/settings/auth-key', { auth_key: authKeyInput.value });
authKeyInput.value = '';
// 改完自己会 401,清掉 session key 让用户重新登录
setTimeout(() => {
sessionStorage.removeItem('ttsAdminKey');
apiKey.value = '';
}, 200);
settingsOk.value = true;
setTimeout(() => settingsOk.value = false, 3000);
} catch (e) {
settingsErr.value = e.response?.data?.error?.message || e.message;
}
};
const resetSettingsForm = () => { loadSettings(); };
const openAdd = () => {
@@ -466,8 +497,8 @@
return { apiKey, keyInput, loginErr, login, logout, tab, overview, voices, actionErr,
showAdd, form, addErr, adding, openAdd, submitAdd, toggle, remove,
settings, settingsForm, apiKeyInput, savingSettings, settingsErr, settingsOk,
loadSettings, saveSettings, saveApiKey, resetSettingsForm,
settings, settingsForm, apiKeyInput, authKeyInput, savingSettings, settingsErr, settingsOk,
loadSettings, saveSettings, saveApiKey, saveAuthKey, resetSettingsForm,
formatUptime, shortPath, reloadAll };
},
}).mount('#app');
+1
View File
@@ -84,6 +84,7 @@ func Setup() *mux.Router {
r.Handle("/api/settings", middleware.RequireAdmin(http.HandlerFunc(controller.SettingsGetHandler))).Methods("GET")
r.Handle("/api/settings", middleware.RequireAdmin(http.HandlerFunc(controller.SettingsUpdateHandler))).Methods("PUT")
r.Handle("/api/settings/api-key", middleware.RequireAdmin(http.HandlerFunc(controller.SettingsAPIKeyHandler))).Methods("PUT")
r.Handle("/api/settings/auth-key", middleware.RequireAdmin(http.HandlerFunc(controller.SettingsAuthKeyHandler))).Methods("PUT")
// 业务路由
r.HandleFunc("/v1/audio/speech", controller.OpenaiTTSHandler).Methods("POST", "OPTIONS")
+13 -5
View File
@@ -96,11 +96,17 @@
<div v-if="errorMsg" class="banner">⚠ {{ errorMsg }}</div>
<div class="card">
<div class="card-title">火山引擎 TTS 凭证</div>
<div class="card-desc">从火山引擎控制台获取,API Key 仅在此页面输入,不会回显。</div>
<div class="card-title">OpenAI 鉴权 + 火山引擎 TTS 凭证</div>
<div class="card-desc">从控制台/面板获取。两个 Key 仅在此页面输入,不会回显,装完不用再碰 env。</div>
<div class="field">
<label>API Key <span class="req">*</span></label>
<label>OpenAI 鉴权 Key <span class="req">*</span></label>
<input type="password" v-model="form.auth_key" placeholder="自定,例如 my-secret-key-2024" autocomplete="off">
<div class="hint">客户端用这个 Key 调 <code>/v1/audio/speech</code>,也是登录 <code>/admin</code> 的密码。装完可在 <code>/admin → 设置</code> 改。</div>
</div>
<div class="field">
<label>火山 TTS API Key <span class="req">*</span></label>
<input type="password" v-model="form.api_key" placeholder="例如: 5b4d7c2a-..." autocomplete="off">
<div class="hint">调火山 v3 上游用的凭证,从 <a href="https://console.volcengine.com/" target="_blank">火山控制台</a> 获取。</div>
</div>
<div class="field-row">
<div class="field">
@@ -166,6 +172,7 @@
createApp({
setup() {
const form = ref({
auth_key: '',
api_key: '',
default_resource_id: 'volc.megatts.default',
default_speaker: '',
@@ -200,8 +207,8 @@
const submit = async () => {
errorMsg.value = '';
if (!form.value.api_key || !form.value.default_resource_id || !form.value.default_speaker || !form.value.token) {
errorMsg.value = '请填写所有必填项(API Key / 资源 ID / 默认音色 / Token)';
if (!form.value.auth_key || !form.value.api_key || !form.value.default_resource_id || !form.value.default_speaker || !form.value.token) {
errorMsg.value = '请填写所有必填项(OpenAI Key / 火山 API Key / 资源 ID / 默认音色 / Token)';
return;
}
if (!form.value.voices.length || form.value.voices.some(v => !v.name || !v.speaker || !v.resource_id)) {
@@ -213,6 +220,7 @@
const r = await axios.post('/api/setup', {
token: form.value.token,
settings: {
auth_key: form.value.auth_key,
api_key: form.value.api_key,
default_resource_id: form.value.default_resource_id,
default_speaker: form.value.default_speaker,