mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-29 16:20:27 +00:00
feat(security): 敏感操作 step-up 2FA 开关化,安全开关默认关闭
新增系统设置 step_up_enabled(默认关闭),把敏感操作 2FA 门控做成可开关; 同时将会话 IP/UA 绑定默认值从开启改为关闭,避免用户因 IP 变动登录后掉线。 ## 新增功能 - 敏感操作 step-up 2FA 总开关 step_up_enabled(默认关闭):关闭时账号/代理导出、 备份创建/下载、S3 配置修改、提升管理员等操作恢复门控引入前的直接放行行为; 开启后要求当前会话在 15 分钟内完成过 TOTP step-up 验证。 ## 优化改进 - 会话 IP/UA 绑定默认改为关闭(功能保留,可在设置页按需开启)。 - 开启 step-up 开关需操作者本人已启用 TOTP(防自锁);关闭开关本身作为敏感操作, 需通过 step-up 验证(防止攻击者拿到会话后先关闸再导出/备份)。 - 两个安全开关请求字段改为可空指针(省略=保持现值),避免旧客户端全量保存时 静默重置安全开关。 - 备份恢复(整库覆盖可回滚安全设置)纳入 step-up 门控。 - 审计摘要 diffSettings 补记 step_up_enabled / session_binding_enabled 变更。 ## Bug 修复 - 修复 BackupView 恢复操作 409(恢复进行中)判断未适配 apiClient 扁平化错误对象。
This commit is contained in:
@@ -367,6 +367,7 @@ export interface SystemSettings {
|
||||
totp_enabled: boolean; // TOTP 双因素认证
|
||||
totp_encryption_key_configured: boolean; // TOTP 加密密钥是否已配置
|
||||
session_binding_enabled: boolean; // 会话 IP/UA 绑定
|
||||
step_up_enabled: boolean; // 敏感操作 step-up 2FA
|
||||
audit_log_retention_days: number; // 审计日志保留天数
|
||||
login_agreement_enabled: boolean;
|
||||
login_agreement_mode: "modal" | "checkbox" | string;
|
||||
@@ -672,6 +673,7 @@ export interface UpdateSettingsRequest {
|
||||
invitation_code_enabled?: boolean;
|
||||
totp_enabled?: boolean; // TOTP 双因素认证
|
||||
session_binding_enabled?: boolean; // 会话 IP/UA 绑定
|
||||
step_up_enabled?: boolean; // 敏感操作 step-up 2FA
|
||||
audit_log_retention_days?: number; // 审计日志保留天数
|
||||
login_agreement_enabled?: boolean;
|
||||
login_agreement_mode?: "modal" | "checkbox" | string;
|
||||
|
||||
@@ -127,6 +127,9 @@ export default {
|
||||
'Please configure TOTP_ENCRYPTION_KEY in environment variables first. Generate a key with: openssl rand -hex 32'
|
||||
},
|
||||
security: {
|
||||
stepUp: 'Step-up 2FA for Sensitive Operations',
|
||||
stepUpHint: 'When enabled, sensitive operations (account/proxy export, backup creation and download, S3 config changes, promoting admins) require a recent TOTP verification (valid for 15 minutes). Your own account must have 2FA enabled before turning this on; turning it off also requires step-up verification.',
|
||||
stepUpEnableRequiresTotp: 'Enable 2FA (TOTP) for your own account in Profile before turning on step-up verification.',
|
||||
sessionBinding: 'Session IP/UA Binding',
|
||||
sessionBindingHint: 'Bind login sessions to the client IP and User-Agent. Any change immediately invalidates the session and forces re-login, raising the bar for stolen-credential reuse.',
|
||||
auditRetention: 'Audit Log Retention (days)',
|
||||
|
||||
@@ -127,6 +127,9 @@ export default {
|
||||
'请先在环境变量中配置 TOTP_ENCRYPTION_KEY。使用命令 openssl rand -hex 32 生成密钥。'
|
||||
},
|
||||
security: {
|
||||
stepUp: '敏感操作二次验证 (step-up 2FA)',
|
||||
stepUpHint: '开启后,账号/代理导出、备份创建与下载、S3 配置修改、提升管理员等敏感操作需要先完成 TOTP 二次验证(15 分钟内有效)。开启前需本人已启用 2FA;关闭该开关本身也需要二次验证。',
|
||||
stepUpEnableRequiresTotp: '开启敏感操作二次验证前,请先在个人资料中为当前账号启用 2FA (TOTP)。',
|
||||
sessionBinding: '会话 IP/UA 绑定',
|
||||
sessionBindingHint: '将登录会话与客户端 IP 和 User-Agent 绑定,任一变化即强制该会话失效并需重新登录(提升被盗凭证的利用门槛)。',
|
||||
auditRetention: '操作日志保留天数',
|
||||
|
||||
@@ -586,16 +586,19 @@ async function restoreBackup(id: string) {
|
||||
if (!password) return
|
||||
restoringId.value = id
|
||||
try {
|
||||
const record = await adminAPI.backup.restoreBackup(id, password)
|
||||
const record = await backupStepUp.run(() => adminAPI.backup.restoreBackup(id, password))
|
||||
updateRecordInList(record)
|
||||
startRestorePolling(id)
|
||||
} catch (error: any) {
|
||||
if (error?.response?.status === 409) {
|
||||
restoringId.value = ''
|
||||
if (isStepUpCancelled(error)) return
|
||||
if (reportStepUpBlocked(error)) return
|
||||
// apiClient 拦截器把 HTTP 错误归一化为顶层 { status } 平面对象(无 response 字段)
|
||||
if (error?.status === 409 || error?.response?.status === 409) {
|
||||
appStore.showWarning(t('admin.backup.operations.restoreRunning'))
|
||||
} else {
|
||||
appStore.showError(error?.message || t('errors.networkError'))
|
||||
}
|
||||
restoringId.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1578,6 +1578,21 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 敏感操作 step-up 2FA -->
|
||||
<div
|
||||
class="flex items-center justify-between border-t border-gray-100 pt-4 dark:border-dark-700"
|
||||
>
|
||||
<div>
|
||||
<label class="font-medium text-gray-900 dark:text-white">{{
|
||||
t("admin.settings.security.stepUp")
|
||||
}}</label>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ t("admin.settings.security.stepUpHint") }}
|
||||
</p>
|
||||
</div>
|
||||
<Toggle v-model="form.step_up_enabled" />
|
||||
</div>
|
||||
|
||||
<!-- 会话 IP/UA 绑定 -->
|
||||
<div
|
||||
class="flex items-center justify-between border-t border-gray-100 pt-4 dark:border-dark-700"
|
||||
@@ -7466,6 +7481,8 @@
|
||||
@confirm="handleAffiliateConfirm"
|
||||
@cancel="cancelAffiliateConfirm"
|
||||
/>
|
||||
<!-- 关闭 step-up 开关等敏感保存操作触发的 TOTP 二次验证 -->
|
||||
<TotpStepUpDialog :controller="settingsStepUp" />
|
||||
</div>
|
||||
</AppLayout>
|
||||
</template>
|
||||
@@ -7519,6 +7536,13 @@ import BackupSettings from "@/views/admin/BackupView.vue";
|
||||
import EmailTemplateEditor from "@/views/admin/settings/EmailTemplateEditor.vue";
|
||||
import OpenAIFastPolicyUserSelector from "@/views/admin/settings/OpenAIFastPolicyUserSelector.vue";
|
||||
import { useClipboard } from "@/composables/useClipboard";
|
||||
import {
|
||||
useStepUp,
|
||||
isStepUpCancelled,
|
||||
isStepUpBlocked,
|
||||
stepUpBlockReason,
|
||||
} from "@/composables/useStepUp";
|
||||
import TotpStepUpDialog from "@/components/auth/TotpStepUpDialog.vue";
|
||||
import { affiliatesAPI, type AffiliateAdminEntry, type SimpleUser as AffiliateSimpleUser } from "@/api/admin/affiliates";
|
||||
import { extractApiErrorMessage, extractI18nErrorMessage } from "@/utils/apiError";
|
||||
import { useAppStore } from "@/stores";
|
||||
@@ -7539,6 +7563,8 @@ import {
|
||||
|
||||
const { t, locale } = useI18n();
|
||||
const appStore = useAppStore();
|
||||
// 关闭 step-up 开关是敏感操作:后端返回 STEP_UP_REQUIRED 时弹 TOTP 码重试
|
||||
const settingsStepUp = useStepUp();
|
||||
const adminSettingsStore = useAdminSettingsStore();
|
||||
const isZhLocale = computed(() => locale.value.startsWith("zh"));
|
||||
|
||||
@@ -8192,7 +8218,8 @@ const form = reactive<SettingsForm>({
|
||||
password_reset_enabled: false,
|
||||
totp_enabled: false,
|
||||
totp_encryption_key_configured: false,
|
||||
session_binding_enabled: true,
|
||||
session_binding_enabled: false,
|
||||
step_up_enabled: false,
|
||||
audit_log_retention_days: 180,
|
||||
login_agreement_enabled: false,
|
||||
login_agreement_mode: "modal",
|
||||
@@ -9559,6 +9586,7 @@ async function saveSettings() {
|
||||
password_reset_enabled: form.password_reset_enabled,
|
||||
totp_enabled: form.totp_enabled,
|
||||
session_binding_enabled: form.session_binding_enabled,
|
||||
step_up_enabled: form.step_up_enabled,
|
||||
// 清空数字框时 v-model.number 会得到空串,后端 int 字段解析空串会 400 拒绝整次保存;
|
||||
// 空/非法值回退默认 180(与后端 parseAuditLogRetentionDays("") 语义一致,0 仍表示永久保留)。
|
||||
audit_log_retention_days: Number.isFinite(form.audit_log_retention_days)
|
||||
@@ -9856,7 +9884,9 @@ async function saveSettings() {
|
||||
payload.default_platform_quotas = sanitizePlatformQuotasMap(form.default_platform_quotas);
|
||||
appendAuthSourceDefaultsToUpdateRequest(payload, authSourceDefaults);
|
||||
|
||||
const updated = await adminAPI.settings.updateSettings(payload);
|
||||
const updated = await settingsStepUp.run(() =>
|
||||
adminAPI.settings.updateSettings(payload),
|
||||
);
|
||||
for (const [key, value] of Object.entries(updated)) {
|
||||
if (key === "openai_fast_policy_settings") continue;
|
||||
if (value !== null && value !== undefined) {
|
||||
@@ -9930,6 +9960,25 @@ async function saveSettings() {
|
||||
appStore.showSuccess(t("admin.settings.settingsSaved"));
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
// 用户取消 step-up 验证:静默返回,不弹错误
|
||||
if (isStepUpCancelled(error)) {
|
||||
return;
|
||||
}
|
||||
if (isStepUpBlocked(error)) {
|
||||
appStore.showError(
|
||||
stepUpBlockReason(error) === "STEP_UP_ADMIN_API_KEY_FORBIDDEN"
|
||||
? t("stepUp.adminApiKeyForbidden")
|
||||
: t("stepUp.notEnabled"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
// 开启 step-up 开关但本人未启用 2FA:给出可操作的专用提示
|
||||
if (
|
||||
(error as { reason?: string })?.reason === "STEP_UP_ENABLE_REQUIRES_TOTP"
|
||||
) {
|
||||
appStore.showError(t("admin.settings.security.stepUpEnableRequiresTotp"));
|
||||
return;
|
||||
}
|
||||
appStore.showError(
|
||||
extractApiErrorMessage(error, t("admin.settings.failedToSave")),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user