Commit bd4bf008 authored by yangjianbo's avatar yangjianbo
Browse files

feat(安全): 强化安全策略与配置校验

- 增加 CORS/CSP/安全响应头与代理信任配置

- 引入 URL 白名单与私网开关,校验上游与价格源

- 改善 API Key 处理与网关错误返回

- 管理端设置隐藏敏感字段并优化前端提示

- 增加计费熔断与相关配置示例

测试: go test ./...
parent 3fd9bd4a
...@@ -197,6 +197,7 @@ export default { ...@@ -197,6 +197,7 @@ export default {
registrationFailed: 'Registration failed. Please try again.', registrationFailed: 'Registration failed. Please try again.',
loginSuccess: 'Login successful! Welcome back.', loginSuccess: 'Login successful! Welcome back.',
accountCreatedSuccess: 'Account created successfully! Welcome to {siteName}.', accountCreatedSuccess: 'Account created successfully! Welcome to {siteName}.',
reloginRequired: 'Session expired. Please log in again.',
turnstileExpired: 'Verification expired, please try again', turnstileExpired: 'Verification expired, please try again',
turnstileFailed: 'Verification failed, please try again', turnstileFailed: 'Verification failed, please try again',
completeVerification: 'Please complete the verification', completeVerification: 'Please complete the verification',
...@@ -991,13 +992,13 @@ export default { ...@@ -991,13 +992,13 @@ export default {
'One sessionKey per line, e.g.:\nsk-ant-sid01-xxxxx...\nsk-ant-sid01-yyyyy...', 'One sessionKey per line, e.g.:\nsk-ant-sid01-xxxxx...\nsk-ant-sid01-yyyyy...',
sessionKeyPlaceholderSingle: 'sk-ant-sid01-xxxxx...', sessionKeyPlaceholderSingle: 'sk-ant-sid01-xxxxx...',
howToGetSessionKey: 'How to get sessionKey', howToGetSessionKey: 'How to get sessionKey',
step1: 'Login to <strong>claude.ai</strong> in your browser', step1: 'Login to claude.ai in your browser',
step2: 'Press <kbd>F12</kbd> to open Developer Tools', step2: 'Press F12 to open Developer Tools',
step3: 'Go to <strong>Application</strong> tab', step3: 'Go to Application tab',
step4: 'Find <strong>Cookies</strong> → <strong>https://claude.ai</strong>', step4: 'Find Cookies → https://claude.ai',
step5: 'Find the row with key <strong>sessionKey</strong>', step5: 'Find the row with key sessionKey',
step6: 'Copy the <strong>Value</strong>', step6: 'Copy the Value',
sessionKeyFormat: 'sessionKey usually starts with <code>sk-ant-sid01-</code>', sessionKeyFormat: 'sessionKey usually starts with sk-ant-sid01-',
startAutoAuth: 'Start Auto-Auth', startAutoAuth: 'Start Auto-Auth',
authorizing: 'Authorizing...', authorizing: 'Authorizing...',
followSteps: 'Follow these steps to authorize your Claude account:', followSteps: 'Follow these steps to authorize your Claude account:',
...@@ -1009,10 +1010,10 @@ export default { ...@@ -1009,10 +1010,10 @@ export default {
openUrlDesc: openUrlDesc:
'Open the authorization URL in a new tab, log in to your Claude account and authorize.', 'Open the authorization URL in a new tab, log in to your Claude account and authorize.',
proxyWarning: proxyWarning:
'<strong>Note:</strong> If you configured a proxy, make sure your browser uses the same proxy to access the authorization page.', 'Note: If you configured a proxy, make sure your browser uses the same proxy to access the authorization page.',
step3EnterCode: 'Enter the Authorization Code', step3EnterCode: 'Enter the Authorization Code',
authCodeDesc: authCodeDesc:
'After authorization is complete, the page will display an <strong>Authorization Code</strong>. Copy and paste it below:', 'After authorization is complete, the page will display an Authorization Code. Copy and paste it below:',
authCode: 'Authorization Code', authCode: 'Authorization Code',
authCodePlaceholder: 'Paste the Authorization Code from Claude page...', authCodePlaceholder: 'Paste the Authorization Code from Claude page...',
authCodeHint: 'Paste the Authorization Code copied from the Claude page', authCodeHint: 'Paste the Authorization Code copied from the Claude page',
...@@ -1033,10 +1034,10 @@ export default { ...@@ -1033,10 +1034,10 @@ export default {
openUrlDesc: openUrlDesc:
'Open the authorization URL in a new tab, log in to your OpenAI account and authorize.', 'Open the authorization URL in a new tab, log in to your OpenAI account and authorize.',
importantNotice: importantNotice:
'<strong>Important:</strong> The page may take a while to load after authorization. Please wait patiently. When the browser address bar changes to <code>http://localhost...</code>, the authorization is complete.', 'Important: The page may take a while to load after authorization. Please wait patiently. When the browser address bar changes to http://localhost..., the authorization is complete.',
step3EnterCode: 'Enter Authorization URL or Code', step3EnterCode: 'Enter Authorization URL or Code',
authCodeDesc: authCodeDesc:
'After authorization is complete, when the page URL becomes <code>http://localhost:xxx/auth/callback?code=...</code>:', 'After authorization is complete, when the page URL becomes http://localhost:xxx/auth/callback?code=...:',
authCode: 'Authorization URL or Code', authCode: 'Authorization URL or Code',
authCodePlaceholder: authCodePlaceholder:
'Option 1: Copy the complete URL\n(http://localhost:xxx/auth/callback?code=...)\nOption 2: Copy only the code parameter value', 'Option 1: Copy the complete URL\n(http://localhost:xxx/auth/callback?code=...)\nOption 2: Copy only the code parameter value',
...@@ -1059,7 +1060,7 @@ export default { ...@@ -1059,7 +1060,7 @@ export default {
'Open the authorization URL in a new tab, log in to your Google account and authorize.', 'Open the authorization URL in a new tab, log in to your Google account and authorize.',
step3EnterCode: 'Enter Authorization URL or Code', step3EnterCode: 'Enter Authorization URL or Code',
authCodeDesc: authCodeDesc:
'After authorization, copy the callback URL (recommended) or just the <code>code</code> and paste it below.', 'After authorization, copy the callback URL (recommended) or just the code and paste it below.',
authCode: 'Callback URL or Code', authCode: 'Callback URL or Code',
authCodePlaceholder: authCodePlaceholder:
'Option 1 (recommended): Paste the callback URL\nOption 2: Paste only the code value', 'Option 1 (recommended): Paste the callback URL\nOption 2: Paste only the code value',
...@@ -1100,10 +1101,10 @@ export default { ...@@ -1100,10 +1101,10 @@ export default {
step2OpenUrl: 'Open the URL in your browser and complete authorization', step2OpenUrl: 'Open the URL in your browser and complete authorization',
openUrlDesc: 'Open the authorization URL in a new tab, log in to your Google account and authorize.', openUrlDesc: 'Open the authorization URL in a new tab, log in to your Google account and authorize.',
importantNotice: importantNotice:
'<strong>Important:</strong> The page may take a while to load after authorization. Please wait patiently. When the browser address bar shows <code>http://localhost...</code>, authorization is complete.', 'Important: The page may take a while to load after authorization. Please wait patiently. When the browser address bar shows http://localhost..., authorization is complete.',
step3EnterCode: 'Enter Authorization URL or Code', step3EnterCode: 'Enter Authorization URL or Code',
authCodeDesc: authCodeDesc:
'After authorization, when the page URL becomes <code>http://localhost:xxx/auth/callback?code=...</code>:', 'After authorization, when the page URL becomes http://localhost:xxx/auth/callback?code=...:',
authCode: 'Authorization URL or Code', authCode: 'Authorization URL or Code',
authCodePlaceholder: authCodePlaceholder:
'Option 1: Copy the complete URL\n(http://localhost:xxx/auth/callback?code=...)\nOption 2: Copy only the code parameter value', 'Option 1: Copy the complete URL\n(http://localhost:xxx/auth/callback?code=...)\nOption 2: Copy only the code parameter value',
...@@ -1377,7 +1378,8 @@ export default { ...@@ -1377,7 +1378,8 @@ export default {
siteKey: 'Site Key', siteKey: 'Site Key',
secretKey: 'Secret Key', secretKey: 'Secret Key',
siteKeyHint: 'Get this from your Cloudflare Dashboard', siteKeyHint: 'Get this from your Cloudflare Dashboard',
secretKeyHint: 'Server-side verification key (keep this secret)' secretKeyHint: 'Server-side verification key (keep this secret)',
secretKeyConfiguredHint: 'Secret key configured. Leave empty to keep the current value.'
}, },
defaults: { defaults: {
title: 'Default User Settings', title: 'Default User Settings',
...@@ -1428,6 +1430,8 @@ export default { ...@@ -1428,6 +1430,8 @@ export default {
password: 'SMTP Password', password: 'SMTP Password',
passwordPlaceholder: '********', passwordPlaceholder: '********',
passwordHint: 'Leave empty to keep existing password', passwordHint: 'Leave empty to keep existing password',
passwordConfiguredPlaceholder: '********',
passwordConfiguredHint: 'Password configured. Leave empty to keep the current value.',
fromEmail: 'From Email', fromEmail: 'From Email',
fromEmailPlaceholder: "noreply{'@'}example.com", fromEmailPlaceholder: "noreply{'@'}example.com",
fromName: 'From Name', fromName: 'From Name',
......
...@@ -194,6 +194,7 @@ export default { ...@@ -194,6 +194,7 @@ export default {
registrationFailed: '注册失败,请重试。', registrationFailed: '注册失败,请重试。',
loginSuccess: '登录成功!欢迎回来。', loginSuccess: '登录成功!欢迎回来。',
accountCreatedSuccess: '账户创建成功!欢迎使用 {siteName}。', accountCreatedSuccess: '账户创建成功!欢迎使用 {siteName}。',
reloginRequired: '会话已过期,请重新登录。',
turnstileExpired: '验证已过期,请重试', turnstileExpired: '验证已过期,请重试',
turnstileFailed: '验证失败,请重试', turnstileFailed: '验证失败,请重试',
completeVerification: '请完成验证', completeVerification: '请完成验证',
...@@ -1137,13 +1138,13 @@ export default { ...@@ -1137,13 +1138,13 @@ export default {
'每行一个 sessionKey,例如:\nsk-ant-sid01-xxxxx...\nsk-ant-sid01-yyyyy...', '每行一个 sessionKey,例如:\nsk-ant-sid01-xxxxx...\nsk-ant-sid01-yyyyy...',
sessionKeyPlaceholderSingle: 'sk-ant-sid01-xxxxx...', sessionKeyPlaceholderSingle: 'sk-ant-sid01-xxxxx...',
howToGetSessionKey: '如何获取 sessionKey', howToGetSessionKey: '如何获取 sessionKey',
step1: '在浏览器中登录 <strong>claude.ai</strong>', step1: '在浏览器中登录 claude.ai',
step2: '<kbd>F12</kbd> 打开开发者工具', step2: 'F12 打开开发者工具',
step3: '切换到 <strong>Application</strong> 标签', step3: '切换到 Application 标签',
step4: '找到 <strong>Cookies</strong> → <strong>https://claude.ai</strong>', step4: '找到 Cookies → https://claude.ai',
step5: '找到 <strong>sessionKey</strong> 所在行', step5: '找到 sessionKey 所在行',
step6: '复制 <strong>Value</strong> 列的值', step6: '复制 Value 列的值',
sessionKeyFormat: 'sessionKey 通常以 <code>sk-ant-sid01-</code> 开头', sessionKeyFormat: 'sessionKey 通常以 sk-ant-sid01- 开头',
startAutoAuth: '开始自动授权', startAutoAuth: '开始自动授权',
authorizing: '授权中...', authorizing: '授权中...',
followSteps: '按照以下步骤授权您的 Claude 账号:', followSteps: '按照以下步骤授权您的 Claude 账号:',
...@@ -1154,9 +1155,9 @@ export default { ...@@ -1154,9 +1155,9 @@ export default {
step2OpenUrl: '在浏览器中打开 URL 并完成授权', step2OpenUrl: '在浏览器中打开 URL 并完成授权',
openUrlDesc: '在新标签页中打开授权 URL,登录您的 Claude 账号并授权。', openUrlDesc: '在新标签页中打开授权 URL,登录您的 Claude 账号并授权。',
proxyWarning: proxyWarning:
'<strong>注意:</strong>如果您配置了代理,请确保浏览器使用相同的代理访问授权页面。', '注意:如果您配置了代理,请确保浏览器使用相同的代理访问授权页面。',
step3EnterCode: '输入授权码', step3EnterCode: '输入授权码',
authCodeDesc: '授权完成后,页面会显示一个 <strong>授权码</strong>。复制并粘贴到下方:', authCodeDesc: '授权完成后,页面会显示一个授权码。复制并粘贴到下方:',
authCode: '授权码', authCode: '授权码',
authCodePlaceholder: '粘贴 Claude 页面的授权码...', authCodePlaceholder: '粘贴 Claude 页面的授权码...',
authCodeHint: '粘贴从 Claude 页面复制的授权码', authCodeHint: '粘贴从 Claude 页面复制的授权码',
...@@ -1176,10 +1177,10 @@ export default { ...@@ -1176,10 +1177,10 @@ export default {
step2OpenUrl: '在浏览器中打开链接并完成授权', step2OpenUrl: '在浏览器中打开链接并完成授权',
openUrlDesc: '请在新标签页中打开授权链接,登录您的 OpenAI 账户并授权。', openUrlDesc: '请在新标签页中打开授权链接,登录您的 OpenAI 账户并授权。',
importantNotice: importantNotice:
'<strong>重要提示:</strong>授权后页面可能会加载较长时间,请耐心等待。当浏览器地址栏变为 <code>http://localhost...</code> 开头时,表示授权已完成。', '重要提示:授权后页面可能会加载较长时间,请耐心等待。当浏览器地址栏变为 http://localhost... 开头时,表示授权已完成。',
step3EnterCode: '输入授权链接或 Code', step3EnterCode: '输入授权链接或 Code',
authCodeDesc: authCodeDesc:
'授权完成后,当页面地址变为 <code>http://localhost:xxx/auth/callback?code=...</code> 时:', '授权完成后,当页面地址变为 http://localhost:xxx/auth/callback?code=... 时:',
authCode: '授权链接或 Code', authCode: '授权链接或 Code',
authCodePlaceholder: authCodePlaceholder:
'方式1:复制完整的链接\n(http://localhost:xxx/auth/callback?code=...)\n方式2:仅复制 code 参数的值', '方式1:复制完整的链接\n(http://localhost:xxx/auth/callback?code=...)\n方式2:仅复制 code 参数的值',
...@@ -1198,7 +1199,7 @@ export default { ...@@ -1198,7 +1199,7 @@ export default {
step2OpenUrl: '在浏览器中打开链接并完成授权', step2OpenUrl: '在浏览器中打开链接并完成授权',
openUrlDesc: '请在新标签页中打开授权链接,登录您的 Google 账户并授权。', openUrlDesc: '请在新标签页中打开授权链接,登录您的 Google 账户并授权。',
step3EnterCode: '输入回调链接或 Code', step3EnterCode: '输入回调链接或 Code',
authCodeDesc: '授权完成后,复制浏览器跳转后的回调链接(推荐)或仅复制 <code>code</code>,粘贴到下方即可。', authCodeDesc: '授权完成后,复制浏览器跳转后的回调链接(推荐)或仅复制 code,粘贴到下方即可。',
authCode: '回调链接或 Code', authCode: '回调链接或 Code',
authCodePlaceholder: '方式1(推荐):粘贴回调链接\n方式2:仅粘贴 code 参数的值', authCodePlaceholder: '方式1(推荐):粘贴回调链接\n方式2:仅粘贴 code 参数的值',
authCodeHint: '系统会自动从链接中解析 code/state。', authCodeHint: '系统会自动从链接中解析 code/state。',
...@@ -1233,10 +1234,10 @@ export default { ...@@ -1233,10 +1234,10 @@ export default {
step2OpenUrl: '在浏览器中打开链接并完成授权', step2OpenUrl: '在浏览器中打开链接并完成授权',
openUrlDesc: '请在新标签页中打开授权链接,登录您的 Google 账户并授权。', openUrlDesc: '请在新标签页中打开授权链接,登录您的 Google 账户并授权。',
importantNotice: importantNotice:
'<strong>重要提示:</strong>授权后页面可能会加载较长时间,请耐心等待。当浏览器地址栏变为 <code>http://localhost...</code> 开头时,表示授权已完成。', '重要提示:授权后页面可能会加载较长时间,请耐心等待。当浏览器地址栏变为 http://localhost... 开头时,表示授权已完成。',
step3EnterCode: '输入授权链接或 Code', step3EnterCode: '输入授权链接或 Code',
authCodeDesc: authCodeDesc:
'授权完成后,当页面地址变为 <code>http://localhost:xxx/auth/callback?code=...</code> 时:', '授权完成后,当页面地址变为 http://localhost:xxx/auth/callback?code=... 时:',
authCode: '授权链接或 Code', authCode: '授权链接或 Code',
authCodePlaceholder: authCodePlaceholder:
'方式1:复制完整的链接\n(http://localhost:xxx/auth/callback?code=...)\n方式2:仅复制 code 参数的值', '方式1:复制完整的链接\n(http://localhost:xxx/auth/callback?code=...)\n方式2:仅复制 code 参数的值',
...@@ -1576,7 +1577,8 @@ export default { ...@@ -1576,7 +1577,8 @@ export default {
siteKey: '站点密钥', siteKey: '站点密钥',
secretKey: '私密密钥', secretKey: '私密密钥',
siteKeyHint: '从 Cloudflare Dashboard 获取', siteKeyHint: '从 Cloudflare Dashboard 获取',
secretKeyHint: '服务端验证密钥(请保密)' secretKeyHint: '服务端验证密钥(请保密)',
secretKeyConfiguredHint: '密钥已配置,留空以保留当前值。'
}, },
defaults: { defaults: {
title: '用户默认设置', title: '用户默认设置',
...@@ -1626,6 +1628,8 @@ export default { ...@@ -1626,6 +1628,8 @@ export default {
password: 'SMTP 密码', password: 'SMTP 密码',
passwordPlaceholder: '********', passwordPlaceholder: '********',
passwordHint: '留空以保留现有密码', passwordHint: '留空以保留现有密码',
passwordConfiguredPlaceholder: '********',
passwordConfiguredHint: '密码已配置,留空以保留当前值。',
fromEmail: '发件人邮箱', fromEmail: '发件人邮箱',
fromEmailPlaceholder: "noreply{'@'}example.com", fromEmailPlaceholder: "noreply{'@'}example.com",
fromName: '发件人名称', fromName: '发件人名称',
......
/**
* 验证并规范化 URL
* 默认只接受绝对 URL(以 http:// 或 https:// 开头),可按需允许相对路径
* @param value 用户输入的 URL
* @returns 规范化后的 URL,如果无效则返回空字符串
*/
type SanitizeOptions = {
allowRelative?: boolean
}
export function sanitizeUrl(value: string, options: SanitizeOptions = {}): string {
const trimmed = value.trim()
if (!trimmed) {
return ''
}
if (options.allowRelative && trimmed.startsWith('/')) {
return trimmed
}
// 只接受绝对 URL,不使用 base URL 来避免相对路径被解析为当前域名
// 检查是否以 http:// 或 https:// 开头
if (!trimmed.match(/^https?:\/\//i)) {
return ''
}
try {
const parsed = new URL(trimmed)
const protocol = parsed.protocol.toLowerCase()
if (protocol !== 'http:' && protocol !== 'https:') {
return ''
}
return parsed.toString()
} catch {
return ''
}
}
...@@ -493,6 +493,7 @@ import { useI18n } from 'vue-i18n' ...@@ -493,6 +493,7 @@ import { useI18n } from 'vue-i18n'
import { getPublicSettings } from '@/api/auth' import { getPublicSettings } from '@/api/auth'
import { useAuthStore } from '@/stores' import { useAuthStore } from '@/stores'
import LocaleSwitcher from '@/components/common/LocaleSwitcher.vue' import LocaleSwitcher from '@/components/common/LocaleSwitcher.vue'
import { sanitizeUrl } from '@/utils/url'
const { t } = useI18n() const { t } = useI18n()
...@@ -549,9 +550,9 @@ onMounted(async () => { ...@@ -549,9 +550,9 @@ onMounted(async () => {
try { try {
const settings = await getPublicSettings() const settings = await getPublicSettings()
siteName.value = settings.site_name || 'Sub2API' siteName.value = settings.site_name || 'Sub2API'
siteLogo.value = settings.site_logo || '' siteLogo.value = sanitizeUrl(settings.site_logo || '', { allowRelative: true })
siteSubtitle.value = settings.site_subtitle || 'AI API Gateway Platform' siteSubtitle.value = settings.site_subtitle || 'AI API Gateway Platform'
docUrl.value = settings.doc_url || '' docUrl.value = sanitizeUrl(settings.doc_url || '', { allowRelative: true })
} catch (error) { } catch (error) {
console.error('Failed to load public settings:', error) console.error('Failed to load public settings:', error)
} }
......
...@@ -255,7 +255,11 @@ ...@@ -255,7 +255,11 @@
placeholder="0x4AAAAAAA..." placeholder="0x4AAAAAAA..."
/> />
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400"> <p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
{{ t('admin.settings.turnstile.secretKeyHint') }} {{
form.turnstile_secret_key_configured
? t('admin.settings.turnstile.secretKeyConfiguredHint')
: t('admin.settings.turnstile.secretKeyHint')
}}
</p> </p>
</div> </div>
</div> </div>
...@@ -577,10 +581,18 @@ ...@@ -577,10 +581,18 @@
v-model="form.smtp_password" v-model="form.smtp_password"
type="password" type="password"
class="input" class="input"
:placeholder="t('admin.settings.smtp.passwordPlaceholder')" :placeholder="
form.smtp_password_configured
? t('admin.settings.smtp.passwordConfiguredPlaceholder')
: t('admin.settings.smtp.passwordPlaceholder')
"
/> />
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400"> <p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
{{ t('admin.settings.smtp.passwordHint') }} {{
form.smtp_password_configured
? t('admin.settings.smtp.passwordConfiguredHint')
: t('admin.settings.smtp.passwordHint')
}}
</p> </p>
</div> </div>
<div> <div>
...@@ -713,7 +725,7 @@ ...@@ -713,7 +725,7 @@
import { ref, reactive, onMounted } from 'vue' import { ref, reactive, onMounted } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { adminAPI } from '@/api' import { adminAPI } from '@/api'
import type { SystemSettings } from '@/api/admin/settings' import type { SystemSettings, UpdateSettingsRequest } from '@/api/admin/settings'
import AppLayout from '@/components/layout/AppLayout.vue' import AppLayout from '@/components/layout/AppLayout.vue'
import Toggle from '@/components/common/Toggle.vue' import Toggle from '@/components/common/Toggle.vue'
import { useAppStore } from '@/stores' import { useAppStore } from '@/stores'
...@@ -735,7 +747,12 @@ const adminApiKeyMasked = ref('') ...@@ -735,7 +747,12 @@ const adminApiKeyMasked = ref('')
const adminApiKeyOperating = ref(false) const adminApiKeyOperating = ref(false)
const newAdminApiKey = ref('') const newAdminApiKey = ref('')
const form = reactive<SystemSettings>({ type SettingsForm = SystemSettings & {
smtp_password: string
turnstile_secret_key: string
}
const form = reactive<SettingsForm>({
registration_enabled: true, registration_enabled: true,
email_verify_enabled: false, email_verify_enabled: false,
default_balance: 0, default_balance: 0,
...@@ -750,13 +767,15 @@ const form = reactive<SystemSettings>({ ...@@ -750,13 +767,15 @@ const form = reactive<SystemSettings>({
smtp_port: 587, smtp_port: 587,
smtp_username: '', smtp_username: '',
smtp_password: '', smtp_password: '',
smtp_password_configured: false,
smtp_from_email: '', smtp_from_email: '',
smtp_from_name: '', smtp_from_name: '',
smtp_use_tls: true, smtp_use_tls: true,
// Cloudflare Turnstile // Cloudflare Turnstile
turnstile_enabled: false, turnstile_enabled: false,
turnstile_site_key: '', turnstile_site_key: '',
turnstile_secret_key: '' turnstile_secret_key: '',
turnstile_secret_key_configured: false
}) })
function handleLogoUpload(event: Event) { function handleLogoUpload(event: Event) {
...@@ -802,6 +821,8 @@ async function loadSettings() { ...@@ -802,6 +821,8 @@ async function loadSettings() {
try { try {
const settings = await adminAPI.settings.getSettings() const settings = await adminAPI.settings.getSettings()
Object.assign(form, settings) Object.assign(form, settings)
form.smtp_password = ''
form.turnstile_secret_key = ''
} catch (error: any) { } catch (error: any) {
appStore.showError( appStore.showError(
t('admin.settings.failedToLoad') + ': ' + (error.message || t('common.unknownError')) t('admin.settings.failedToLoad') + ': ' + (error.message || t('common.unknownError'))
...@@ -814,7 +835,32 @@ async function loadSettings() { ...@@ -814,7 +835,32 @@ async function loadSettings() {
async function saveSettings() { async function saveSettings() {
saving.value = true saving.value = true
try { try {
await adminAPI.settings.updateSettings(form) const payload: UpdateSettingsRequest = {
registration_enabled: form.registration_enabled,
email_verify_enabled: form.email_verify_enabled,
default_balance: form.default_balance,
default_concurrency: form.default_concurrency,
site_name: form.site_name,
site_logo: form.site_logo,
site_subtitle: form.site_subtitle,
api_base_url: form.api_base_url,
contact_info: form.contact_info,
doc_url: form.doc_url,
smtp_host: form.smtp_host,
smtp_port: form.smtp_port,
smtp_username: form.smtp_username,
smtp_password: form.smtp_password || undefined,
smtp_from_email: form.smtp_from_email,
smtp_from_name: form.smtp_from_name,
smtp_use_tls: form.smtp_use_tls,
turnstile_enabled: form.turnstile_enabled,
turnstile_site_key: form.turnstile_site_key,
turnstile_secret_key: form.turnstile_secret_key || undefined
}
const updated = await adminAPI.settings.updateSettings(payload)
Object.assign(form, updated)
form.smtp_password = ''
form.turnstile_secret_key = ''
// Refresh cached public settings so sidebar/header update immediately // Refresh cached public settings so sidebar/header update immediately
await appStore.fetchPublicSettings(true) await appStore.fetchPublicSettings(true)
appStore.showSuccess(t('admin.settings.settingsSaved')) appStore.showSuccess(t('admin.settings.settingsSaved'))
......
...@@ -277,6 +277,14 @@ const errors = reactive({ ...@@ -277,6 +277,14 @@ const errors = reactive({
// ==================== Lifecycle ==================== // ==================== Lifecycle ====================
onMounted(async () => { onMounted(async () => {
const expiredFlag = sessionStorage.getItem('auth_expired')
if (expiredFlag) {
sessionStorage.removeItem('auth_expired')
const message = t('auth.reloginRequired')
errorMessage.value = message
appStore.showWarning(message)
}
try { try {
const settings = await getPublicSettings() const settings = await getPublicSettings()
turnstileEnabled.value = settings.turnstile_enabled turnstileEnabled.value = settings.turnstile_enabled
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment