Smail/frontend/src/views/components/UserManagement/UserManagement.js
smail 60ef7a74ec 修复管理员成员分组权限检查点
提交分组成员管理权限和前端入口调整,作为继续开发前的安全检查点。
2026-10-02 03:48:27 +08:00

331 lines
10 KiB
JavaScript

import {computed, reactive, ref} from 'vue'
import lang from '../../../i18n/i18n';
import {http} from "@/utils/axios";
import {ElNotification} from "element-plus";
import {Plus} from "@element-plus/icons-vue";
import {useGlobalStatusStore} from "@/stores/useGlobalStatusStore";
import {confirmRecentAuthentication, isRecentAuthenticationCancelled, requestWithRecentAuthentication} from '@/utils/recentAuthentication'
import {roleMeta, assignableRoles, canManageUser} from '@/utils/userRoles.mjs'
import {useAccountGroups} from './AccountGroups.js'
export function recoveryCodeMeta(configured) {
return configured
? { label: '已开启', type: 'success' }
: { label: '未开启', type: 'info' }
}
export function externalEmailMeta(configured) {
return configured
? { label: '已绑定', type: 'success' }
: { label: '未绑定', type: 'info' }
}
export function accountStatusMeta(disabled) {
return disabled === 1
? { label: '已禁用', type: 'danger' }
: { label: '正常', type: 'success' }
}
export function isSameUser(selectedUser, currentUser) {
const selectedID = Number(selectedUser?.ID || 0)
const currentID = Number(currentUser?.id || 0)
if (selectedID > 0 && currentID > 0) {
return selectedID === currentID
}
return Boolean(selectedUser?.Account && currentUser?.account)
&& selectedUser.Account === currentUser.account
}
export function canManageRecoveryCode(user) {
return Boolean(user?.ID) && user.Role !== 'super_admin'
}
export function usePage() {
const globalStatus = useGlobalStatusStore()
const groups = useAccountGroups(globalStatus, user => {
if (selectedUser.value?.ID === user.ID) selectedUser.value = user
})
const userInfoDialog = ref(false)
const editModel = ref("edit")
const selectedUser = ref(null)
const editingGroupIDs = ref([0])
const editingMembershipVersion = ref(0)
const membershipChanged = computed(() => {
const saved = selectedUser.value?.GroupIDs || [0]
return saved.length !== editingGroupIDs.value.length
|| saved.some(id => !editingGroupIDs.value.includes(id))
})
const selectedMembershipGroups = computed(() => {
const user = selectedUser.value
if (!user && editModel.value !== 'create') return []
return groups.accountGroups.value.filter(group =>
groups.canManageMembership.value
? editingGroupIDs.value.includes(group.id)
: group.rows.some(member => member.ID === user.ID)
)
})
const openMembershipDialog = () => {
if (submitting.value || (!selectedUser.value && editModel.value !== 'create')) return
const membershipUser = selectedUser.value || {
Account: editUserInfo.account,
Role: editUserInfo.role
}
groups.editMembership(membershipUser, {
role: editUserInfo.role,
groupIDs: editingGroupIDs.value,
onApply: ids => { editingGroupIDs.value = ids }
})
}
const recoveryCodeDialogVisible = ref(false)
const externalEmailDialogVisible = ref(false)
const notificationDialogVisible = ref(false)
const notificationPanel = ref(null)
const canManageNotification = computed(() => globalStatus.hasCapability('notification.manage'))
const closeNotificationDialog = async done => {
if (await notificationPanel.value?.beforeLeave()) done()
}
const securityDialogOpening = ref('')
const submitting = ref(false)
const availableRoles = computed(() => assignableRoles(globalStatus.role))
const canCreateUser = computed(() => globalStatus.hasCapability('user.create'))
const canEditUser = user => canManageUser(globalStatus.role, user?.Role)
const canEditSelectedUser = computed(() => editModel.value === 'create'
? canCreateUser.value && availableRoles.value.length > 0
: canManageUser(globalStatus.role, selectedUser.value?.Role))
const canSelectRole = computed(() => canEditSelectedUser.value
&& (editModel.value === 'create' || globalStatus.hasCapability('role.manage')))
const editingCurrentUser = computed(() => isSameUser(selectedUser.value, globalStatus.userInfos))
const recoveryCodeWebManaged = computed(() => canManageRecoveryCode(selectedUser.value))
const editUserInfo = reactive({
"account": "",
"name": "",
"password": "",
"disabled": false,
"role": "user"
})
const roleChanged = computed(() => editModel.value === 'edit'
&& editUserInfo.role !== selectedUser.value?.Role)
const title = ref(lang.editUser)
const reflushList = async function () {
const selectedID = selectedUser.value?.ID
await groups.refreshGroups()
if (selectedID && selectedUser.value?.ID === selectedID) {
const users = groups.accountGroups.value.flatMap(group => group.rows)
let current = users.find(item => item.ID === selectedID)
if (!current) {
const response = await http.post('/api/v1/user/list', { user_id: selectedID, page_size: 1 })
if (response?.errorNo !== 0) throw new Error(response?.errorMsg || lang.fail)
current = response.data?.list?.find(item => item.ID === selectedID)
}
if (selectedUser.value?.ID !== selectedID) return
if (!current) {
selectedUser.value = null
userInfoDialog.value = false
return
}
const savedGroups = selectedUser.value.GroupIDs || [0]
const currentGroups = current.GroupIDs || [0]
if (current.Role === selectedUser.value.Role
&& currentGroups.length === savedGroups.length
&& savedGroups.every(id => currentGroups.includes(id))) {
editingMembershipVersion.value = current.MembershipVersion
}
selectedUser.value = current
}
}
const handleEdit = function (idx, row) {
if (!canEditUser(row)) return
selectedUser.value = row
editingGroupIDs.value = [...(row.GroupIDs || [0])]
editingMembershipVersion.value = row.MembershipVersion
editUserInfo.account = row.Account
editUserInfo.name = row.Name
editUserInfo.disabled = row.Disabled === 1
editUserInfo.password = ""
editUserInfo.role = row.Role
editModel.value = "edit"
title.value = lang.editUser
userInfoDialog.value = true
}
const createUser = function () {
if (!canCreateUser.value) return
selectedUser.value = null
editingGroupIDs.value = [0]
editingMembershipVersion.value = 0
editUserInfo.account = ""
editUserInfo.name = ""
editUserInfo.disabled = false
editUserInfo.password = ""
editUserInfo.role = "user"
editModel.value = "create"
title.value = lang.newUser
userInfoDialog.value = true
}
const confirmSecurityDialogAccess = async function (dialogName) {
if (securityDialogOpening.value) return false
securityDialogOpening.value = dialogName
try {
await confirmRecentAuthentication()
return true
} catch (error) {
if (!isRecentAuthenticationCancelled(error)) {
ElNotification({
title: lang.fail,
message: error?.message || '身份确认失败',
type: 'error',
})
}
return false
} finally {
securityDialogOpening.value = ''
}
}
const openRecoveryCodeDialog = async function () {
if (!recoveryCodeWebManaged.value) return
if (!await confirmSecurityDialogAccess('recovery-code')) return
recoveryCodeDialogVisible.value = true
}
const openExternalEmailDialog = async function () {
if (!selectedUser.value?.ID) return
if (!await confirmSecurityDialogAccess('external-email')) return
externalEmailDialogVisible.value = true
}
const handleSecurityChanged = async function () {
try {
await reflushList()
} catch (error) {
ElNotification({ title: lang.fail, message: error?.message || lang.fail, type: 'error' })
}
}
const submit = async function () {
if (submitting.value || !canEditSelectedUser.value) return
submitting.value = true
try {
const creating = editModel.value === 'create'
const newData = {
"account": editUserInfo.account,
"username": editUserInfo.name,
"disabled": editUserInfo.disabled ? 1 : 0
}
if (creating || roleChanged.value) newData.role = editUserInfo.role
if (creating || editUserInfo.password !== '') newData.password = editUserInfo.password
if (groups.canManageMembership.value && (creating || membershipChanged.value || roleChanged.value)) {
if (editUserInfo.role !== 'admin' && editingGroupIDs.value.length !== 1) {
throw new Error('普通用户只能属于一个分组,请在分组管理中选择一个分组')
}
newData.group_ids = [...editingGroupIDs.value]
if (!creating) newData.membership_version = editingMembershipVersion.value
}
const res = await requestWithRecentAuthentication(() => creating
? http.post('/api/v1/user/create', newData)
: http.post('/api/v1/user/edit', newData))
if (!res || res.errorNo !== 0) {
throw new Error(res?.errorMsg || '保存用户失败')
}
ElNotification({ title: lang.succ, message: '用户信息已保存', type: 'success' })
editUserInfo.password = ''
if (creating) {
handleEdit(0, res.data)
} else {
userInfoDialog.value = false
}
await reflushList()
} catch (error) {
if (!isRecentAuthenticationCancelled(error)) {
ElNotification({
title: lang.fail,
message: error?.message || '保存用户失败,请稍后重试',
type: 'error'
})
}
} finally {
submitting.value = false
}
}
reflushList()
return {
reactive,
ref,
lang,
http,
ElNotification,
Plus,
...groups,
userInfoDialog,
editModel,
editUserInfo,
availableRoles,
canCreateUser,
canEditSelectedUser,
canSelectRole,
roleChanged,
submitting,
selectedUser,
selectedMembershipGroups,
openMembershipDialog,
editingCurrentUser,
recoveryCodeWebManaged,
recoveryCodeDialogVisible,
externalEmailDialogVisible,
notificationDialogVisible,
notificationPanel,
canManageNotification,
closeNotificationDialog,
securityDialogOpening,
openRecoveryCodeDialog,
openExternalEmailDialog,
handleSecurityChanged,
title,
reflushList,
handleEdit,
canEditUser,
createUser,
submit,
roleMeta,
recoveryCodeMeta,
externalEmailMeta,
accountStatusMeta
}
}
export {
Plus,
roleMeta
}