mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-11-02 00:30:32 +00:00
feat(security): gate admin role promotion behind step-up 2FA and harden admin TOTP verification
- 提升用户为管理员 / 创建管理员账号纳入敏感操作:handler 级 EnforceStepUp 门控 (admin API key 拒绝、未启用 TOTP 拒绝、无 grant 返回 STEP_UP_REQUIRED), 目标已是管理员的日常编辑不触发 - 管理员启用/停用 2FA 一律使用密码验证(默认通知邮箱常收不到验证码), verification-method 按用户角色返回;普通用户行为不变 - 用户编辑/创建弹窗接入 useStepUp:命中 STEP_UP_REQUIRED 弹 TOTP 验证并自动重试 - 审计日志清理入口与其他敏感操作对齐:未启用 2FA 时直接提示先启用 TOTP, 不再弹出无法完成的验证码输入框(后端强制现场 TOTP 语义不变) - 审计日志页重构:DataTable 布局、详情弹窗分区展示、时间范围改为 ops 同款 下拉(预设窗口 + 自定义起止支持时分)
This commit is contained in:
@@ -64,34 +64,57 @@
|
||||
</div>
|
||||
</template>
|
||||
</BaseDialog>
|
||||
|
||||
<!-- 创建管理员账号时后端要求 step-up 2FA,弹出 TOTP 验证后自动重试 -->
|
||||
<TotpStepUpDialog :controller="stepUp" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, watch } from 'vue'
|
||||
import { reactive, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'; import { adminAPI } from '@/api/admin'
|
||||
import { useForm } from '@/composables/useForm'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import BaseDialog from '@/components/common/BaseDialog.vue'
|
||||
import Icon from '@/components/icons/Icon.vue'
|
||||
import { useStepUp, isStepUpBlocked, isStepUpCancelled, stepUpBlockReason } from '@/composables/useStepUp'
|
||||
import TotpStepUpDialog from '@/components/auth/TotpStepUpDialog.vue'
|
||||
|
||||
const props = defineProps<{ show: boolean }>()
|
||||
const emit = defineEmits(['close', 'success']); const { t } = useI18n()
|
||||
const appStore = useAppStore()
|
||||
|
||||
const form = reactive({ email: '', password: '', username: '', notes: '', role: 'user' as 'user' | 'admin', balance: '', concurrency: 1, rpm_limit: 0 })
|
||||
|
||||
const { loading, submit } = useForm({
|
||||
form,
|
||||
submitFn: async (data) => {
|
||||
const { balance: rawBalance, ...rest } = data
|
||||
const stepUp = useStepUp()
|
||||
const loading = ref(false)
|
||||
|
||||
const submit = async () => {
|
||||
if (loading.value) return
|
||||
loading.value = true
|
||||
try {
|
||||
const { balance: rawBalance, ...rest } = { ...form }
|
||||
const balance = String(rawBalance).trim()
|
||||
const payload: typeof rest & { balance?: number } = { ...rest }
|
||||
if (balance !== '') {
|
||||
payload.balance = Number(balance)
|
||||
}
|
||||
await adminAPI.users.create(payload)
|
||||
// 创建管理员属敏感操作:后端返回 STEP_UP_REQUIRED 时弹 TOTP 验证并重试
|
||||
await stepUp.run(() => adminAPI.users.create(payload))
|
||||
appStore.showSuccess(t('admin.users.userCreated'))
|
||||
emit('success'); emit('close')
|
||||
},
|
||||
successMsg: t('admin.users.userCreated')
|
||||
})
|
||||
} catch (e: any) {
|
||||
if (isStepUpCancelled(e)) {
|
||||
// 用户主动取消二次验证:静默返回,表单保持打开。
|
||||
} else if (isStepUpBlocked(e)) {
|
||||
appStore.showError(
|
||||
stepUpBlockReason(e) === 'STEP_UP_ADMIN_API_KEY_FORBIDDEN'
|
||||
? t('stepUp.adminApiKeyForbidden')
|
||||
: t('stepUp.notEnabled')
|
||||
)
|
||||
} else {
|
||||
appStore.showError(e?.message || t('admin.users.failedToCreate'))
|
||||
}
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
watch(() => props.show, (v) => { if(v) Object.assign(form, { email: '', password: '', username: '', notes: '', role: 'user', balance: '', concurrency: 1, rpm_limit: 0 }) })
|
||||
|
||||
|
||||
@@ -67,6 +67,9 @@
|
||||
</div>
|
||||
</template>
|
||||
</BaseDialog>
|
||||
|
||||
<!-- 角色提升为管理员时后端要求 step-up 2FA,弹出 TOTP 验证后自动重试 -->
|
||||
<TotpStepUpDialog :controller="stepUp" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -79,6 +82,8 @@ import type { AdminUser, UserAttributeValuesMap } from '@/types'
|
||||
import BaseDialog from '@/components/common/BaseDialog.vue'
|
||||
import UserAttributeForm from '@/components/user/UserAttributeForm.vue'
|
||||
import Icon from '@/components/icons/Icon.vue'
|
||||
import { useStepUp, isStepUpBlocked, isStepUpCancelled, stepUpBlockReason } from '@/composables/useStepUp'
|
||||
import TotpStepUpDialog from '@/components/auth/TotpStepUpDialog.vue'
|
||||
|
||||
const props = defineProps<{ show: boolean, user: AdminUser | null }>()
|
||||
const emit = defineEmits(['close', 'success'])
|
||||
@@ -104,6 +109,8 @@ const copyPassword = async () => {
|
||||
passwordCopied.value = true; setTimeout(() => passwordCopied.value = false, 2000)
|
||||
}
|
||||
}
|
||||
const stepUp = useStepUp()
|
||||
|
||||
const handleUpdateUser = async () => {
|
||||
if (!props.user) return
|
||||
if (!form.email.trim()) {
|
||||
@@ -114,16 +121,28 @@ const handleUpdateUser = async () => {
|
||||
appStore.showError(t('admin.users.concurrencyMin'))
|
||||
return
|
||||
}
|
||||
const userId = props.user.id
|
||||
submitting.value = true
|
||||
try {
|
||||
const data: any = { email: form.email, username: form.username, notes: form.notes, role: form.role, concurrency: form.concurrency, rpm_limit: form.rpm_limit }
|
||||
if (form.password.trim()) data.password = form.password.trim()
|
||||
await adminAPI.users.update(props.user.id, data)
|
||||
if (Object.keys(form.customAttributes).length > 0) await adminAPI.userAttributes.updateUserAttributeValues(props.user.id, form.customAttributes)
|
||||
// 提升为管理员属敏感操作:后端返回 STEP_UP_REQUIRED 时弹 TOTP 验证并重试
|
||||
await stepUp.run(() => adminAPI.users.update(userId, data))
|
||||
if (Object.keys(form.customAttributes).length > 0) await adminAPI.userAttributes.updateUserAttributeValues(userId, form.customAttributes)
|
||||
appStore.showSuccess(t('admin.users.userUpdated'))
|
||||
emit('success'); emit('close')
|
||||
} catch (e: any) {
|
||||
appStore.showError(e.response?.data?.detail || t('admin.users.failedToUpdate'))
|
||||
if (isStepUpCancelled(e)) {
|
||||
// 用户主动取消二次验证:静默返回,表单保持打开。
|
||||
} else if (isStepUpBlocked(e)) {
|
||||
appStore.showError(
|
||||
stepUpBlockReason(e) === 'STEP_UP_ADMIN_API_KEY_FORBIDDEN'
|
||||
? t('stepUp.adminApiKeyForbidden')
|
||||
: t('stepUp.notEnabled')
|
||||
)
|
||||
} else {
|
||||
appStore.showError(e?.message || t('admin.users.failedToUpdate'))
|
||||
}
|
||||
} finally { submitting.value = false }
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user